starbash 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {starbash-0.3.0 → starbash-0.3.2}/PKG-INFO +13 -6
  2. {starbash-0.3.0 → starbash-0.3.2}/README.md +12 -5
  3. {starbash-0.3.0 → starbash-0.3.2}/pyproject.toml +1 -1
  4. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/app.py +4 -0
  5. starbash-0.3.2/src/starbash/assets/favicon.ico +0 -0
  6. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/commands/publish.py +16 -9
  7. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/defaults/starbash.toml +15 -2
  8. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/doit.py +26 -5
  9. starbash-0.3.2/src/starbash/doit_types.py +85 -0
  10. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/exception.py +12 -0
  11. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/filtering.py +8 -1
  12. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/fits.py +25 -2
  13. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/processing.py +69 -32
  14. starbash-0.3.2/src/starbash/publish/github.py +259 -0
  15. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/publish/github_service.py +80 -2
  16. starbash-0.3.2/src/starbash/recipes/crop.py +139 -0
  17. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/recipes/osc.py +69 -130
  18. starbash-0.3.2/src/starbash/recipes/report_registration.py +308 -0
  19. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/siril/import_registration.py +14 -8
  20. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/stages.py +15 -9
  21. starbash-0.3.2/src/starbash/templates/report/Gemfile +3 -0
  22. starbash-0.3.2/src/starbash/templates/report/__init__.py +0 -0
  23. starbash-0.3.2/src/starbash/templates/report/_config.yml +8 -0
  24. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/report/default.html +2 -1
  25. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/report/target.md.jinja +3 -1
  26. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/userconfig.toml +4 -1
  27. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/tool/base.py +19 -14
  28. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/tool/python.py +50 -40
  29. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/tool/starnet.py +25 -4
  30. starbash-0.3.0/src/starbash/doit_types.py +0 -45
  31. starbash-0.3.0/src/starbash/publish/github.py +0 -214
  32. {starbash-0.3.0 → starbash-0.3.2}/LICENSE +0 -0
  33. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/__init__.py +0 -0
  34. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/aliases.py +0 -0
  35. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/analytics.py +0 -0
  36. {starbash-0.3.0/src/starbash/defaults → starbash-0.3.2/src/starbash/assets}/__init__.py +0 -0
  37. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/check_version.py +0 -0
  38. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/commands/__init__.py +0 -0
  39. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/commands/info.py +0 -0
  40. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/commands/process.py +0 -0
  41. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/commands/repo.py +0 -0
  42. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/commands/select.py +0 -0
  43. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/commands/user.py +0 -0
  44. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/database.py +0 -0
  45. {starbash-0.3.0/src/starbash/recipes → starbash-0.3.2/src/starbash/defaults}/__init__.py +0 -0
  46. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/dwarf3.py +0 -0
  47. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/icon.py +0 -0
  48. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/linux.py +0 -0
  49. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/main.py +0 -0
  50. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/os.py +0 -0
  51. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/parameters.py +0 -0
  52. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/paths.py +0 -0
  53. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/processed_target.py +0 -0
  54. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/processing_like.py +0 -0
  55. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/publish/__init__.py +0 -0
  56. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/publish/credentials.py +0 -0
  57. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/recipes/README.md +0 -0
  58. {starbash-0.3.0/src/starbash/templates → starbash-0.3.2/src/starbash/recipes}/__init__.py +0 -0
  59. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/report.py +0 -0
  60. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/rich.py +0 -0
  61. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/safety.py +0 -0
  62. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/score.py +0 -0
  63. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/selection.py +0 -0
  64. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/sim_siril/__init__.py +0 -0
  65. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/sim_siril/connection.py +0 -0
  66. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/sim_siril/enums.py +0 -0
  67. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/sim_siril/utility.py +0 -0
  68. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/stage_utils.py +0 -0
  69. {starbash-0.3.0/src/starbash/templates/report → starbash-0.3.2/src/starbash/templates}/__init__.py +0 -0
  70. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/repo/master.toml +0 -0
  71. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/repo/processed.toml +0 -0
  72. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/report/README.md.jinja +0 -0
  73. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/report/index.md.jinja +0 -0
  74. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/target/master.toml +0 -0
  75. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/target/processed/about.toml +0 -0
  76. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/target/processed/main.toml +0 -0
  77. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/templates/target/processed/sessions.toml +0 -0
  78. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/toml.py +0 -0
  79. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/tool/__init__.py +0 -0
  80. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/tool/context.py +0 -0
  81. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/tool/graxpert.py +0 -0
  82. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/tool/rcastro.py +0 -0
  83. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/tool/siril.py +0 -0
  84. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/ui/main.py +0 -0
  85. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/url.py +0 -0
  86. {starbash-0.3.0 → starbash-0.3.2}/src/starbash/windows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: starbash
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: A tool for automating/standardizing/sharing astrophotography workflows.
5
5
  License-File: LICENSE
6
6
  Author: Kevin Hester
@@ -43,12 +43,14 @@ A tool for automating/standardizing/sharing astrophotography workflows.
43
43
  ## What is starbash?
44
44
  <img src="https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/icon/generated/web/icon-192.png" alt="Starbash: Astrophotography workflows simplified" width="30%" align="right" style="margin-bottom: 20px;">
45
45
 
46
- * Automatic - with sensible defaults that you can change as needed. Just a few keystrokes to preprocess an entire repo of raw images.
47
- * Easy - provides a 'seestar-like' autoprocessing all your sessions (by default). You can optionally do manual postprocessing with all this 'grunt work' preprocessing automated.
46
+ * Automatic - with sensible defaults that you can change as needed. Just a few keystrokes to auto-process an **entire** repo of raw images. It uses sensible customizable defaults - you can optionally tweak parameters and reprocess as needed.
47
+ * Easy - provides a 'seestar-like' autoprocessing all your sessions (by default). You can optionally do manual postprocessing with all this 'grunt work' default processing automated.
48
48
  * Fast<sup>1</sup> - even with large image repositories. Automatic master bias and flat generation. Automatic preprocessing/ stacking/background-elimination with reasonable defaults.
49
- * Multi-session - by default. So your workflows can stack from multiple nights (and still use the correct flats etc...).
49
+ * Multi-session - by default. So your workflows can stack from multiple nights (and still use/auto-generate the correct flats, darks/biases etc...).
50
50
  * Shareable - you can share/use recipes for image preprocessing flows. The eventual goal is a transition away from opaque burried scripts. Recipes can come from multiple repos and you or other orgs can host recipes.
51
+ * Automatic (optional) posting/creation of your generated images and workflows to a free ['github pages'](https://docs.github.com/en/pages) website. [Sample](https://geeksville.github.io/starbash-public/targets/sh2-91/).
51
52
  * Attribution (by default) - a full set of contributions to final image can be tracked: instrument, raw images, processed-by, recipes-by/version etc...
53
+ * Full session/FITS aware history - select targets by name, date-range, instrument etc...
52
54
 
53
55
  (This project is currently 'alpha' and missing recipes for some workflows, but adding new recipes is easy and we're happy to help. Please file a GitHub issue if your images are not auto-processed and we'll work out a fix.)
54
56
 
@@ -58,7 +60,7 @@ A tool for automating/standardizing/sharing astrophotography workflows.
58
60
  ## What is starbash NOT
59
61
 
60
62
  ### Not a new image processing tool (like Siril)
61
- It is important to understand that Starbash is primarily a tool for **repeatable**, **sharable**, **semi-automated** workflows. It is tool agnostic and really just focuses on the relationship between tools, images and sessions. It builds upon the great work of existing tools (currently mainly Siril and Graxpert) so we can grow an ecosystem of recipes.
63
+ It is important to understand that Starbash is primarily a tool for **repeatable**, **sharable**, **semi-automated** workflows. It is tool agnostic and really just focuses on the relationship between tools, images and sessions. It builds upon the great work of existing tools (currently mainly [Siril](https://siril.org/), [GraXpert](https://graxpert.com/), [Blurxterminator](https://www.rc-astro.com/software/bxt/) and [Starnet2](https://starnetastro.com/cli-tools/)) so we can grow an ecosystem of recipes.
62
64
 
63
65
  ### Not a set of scripts
64
66
 
@@ -98,7 +100,12 @@ If you are interested in alpha-testing we ❤️ you. This README should have e
98
100
 
99
101
  ### From the alpha 3 (0.3.0) release (2026/08/31)
100
102
 
101
- * Auto publish your finished images and workflow report to github-sites
103
+ - Added end-to-end GitHub Pages publishing, including a friendlier GitHub setup flow and automatic token refresh.
104
+ - Introduced the new processing layout with shareable per-target configuration and improved recipe/dependency handling.
105
+ - Added Starnet, BlurXTerminator, and NoiseXTerminator support, with live Siril progress and automatic star recomposition.
106
+ - Improved FITS/session reporting with focal-length and FWHM information, plus more reliable OSC stacking.
107
+ - Added safer cleanup of temporary processing files and clearer warnings when external tools are missing.
108
+ - Publishing is now about twice as fast thanks to concurrent uploads.
102
109
 
103
110
  ## Features coming soon
104
111
 
@@ -9,12 +9,14 @@ A tool for automating/standardizing/sharing astrophotography workflows.
9
9
  ## What is starbash?
10
10
  <img src="https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/icon/generated/web/icon-192.png" alt="Starbash: Astrophotography workflows simplified" width="30%" align="right" style="margin-bottom: 20px;">
11
11
 
12
- * Automatic - with sensible defaults that you can change as needed. Just a few keystrokes to preprocess an entire repo of raw images.
13
- * Easy - provides a 'seestar-like' autoprocessing all your sessions (by default). You can optionally do manual postprocessing with all this 'grunt work' preprocessing automated.
12
+ * Automatic - with sensible defaults that you can change as needed. Just a few keystrokes to auto-process an **entire** repo of raw images. It uses sensible customizable defaults - you can optionally tweak parameters and reprocess as needed.
13
+ * Easy - provides a 'seestar-like' autoprocessing all your sessions (by default). You can optionally do manual postprocessing with all this 'grunt work' default processing automated.
14
14
  * Fast<sup>1</sup> - even with large image repositories. Automatic master bias and flat generation. Automatic preprocessing/ stacking/background-elimination with reasonable defaults.
15
- * Multi-session - by default. So your workflows can stack from multiple nights (and still use the correct flats etc...).
15
+ * Multi-session - by default. So your workflows can stack from multiple nights (and still use/auto-generate the correct flats, darks/biases etc...).
16
16
  * Shareable - you can share/use recipes for image preprocessing flows. The eventual goal is a transition away from opaque burried scripts. Recipes can come from multiple repos and you or other orgs can host recipes.
17
+ * Automatic (optional) posting/creation of your generated images and workflows to a free ['github pages'](https://docs.github.com/en/pages) website. [Sample](https://geeksville.github.io/starbash-public/targets/sh2-91/).
17
18
  * Attribution (by default) - a full set of contributions to final image can be tracked: instrument, raw images, processed-by, recipes-by/version etc...
19
+ * Full session/FITS aware history - select targets by name, date-range, instrument etc...
18
20
 
19
21
  (This project is currently 'alpha' and missing recipes for some workflows, but adding new recipes is easy and we're happy to help. Please file a GitHub issue if your images are not auto-processed and we'll work out a fix.)
20
22
 
@@ -24,7 +26,7 @@ A tool for automating/standardizing/sharing astrophotography workflows.
24
26
  ## What is starbash NOT
25
27
 
26
28
  ### Not a new image processing tool (like Siril)
27
- It is important to understand that Starbash is primarily a tool for **repeatable**, **sharable**, **semi-automated** workflows. It is tool agnostic and really just focuses on the relationship between tools, images and sessions. It builds upon the great work of existing tools (currently mainly Siril and Graxpert) so we can grow an ecosystem of recipes.
29
+ It is important to understand that Starbash is primarily a tool for **repeatable**, **sharable**, **semi-automated** workflows. It is tool agnostic and really just focuses on the relationship between tools, images and sessions. It builds upon the great work of existing tools (currently mainly [Siril](https://siril.org/), [GraXpert](https://graxpert.com/), [Blurxterminator](https://www.rc-astro.com/software/bxt/) and [Starnet2](https://starnetastro.com/cli-tools/)) so we can grow an ecosystem of recipes.
28
30
 
29
31
  ### Not a set of scripts
30
32
 
@@ -64,7 +66,12 @@ If you are interested in alpha-testing we ❤️ you. This README should have e
64
66
 
65
67
  ### From the alpha 3 (0.3.0) release (2026/08/31)
66
68
 
67
- * Auto publish your finished images and workflow report to github-sites
69
+ - Added end-to-end GitHub Pages publishing, including a friendlier GitHub setup flow and automatic token refresh.
70
+ - Introduced the new processing layout with shareable per-target configuration and improved recipe/dependency handling.
71
+ - Added Starnet, BlurXTerminator, and NoiseXTerminator support, with live Siril progress and automatic star recomposition.
72
+ - Improved FITS/session reporting with focal-length and FWHM information, plus more reliable OSC stacking.
73
+ - Added safer cleanup of temporary processing files and clearer warnings when external tools are missing.
74
+ - Publishing is now about twice as fast thanks to concurrent uploads.
68
75
 
69
76
  ## Features coming soon
70
77
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "starbash"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "A tool for automating/standardizing/sharing astrophotography workflows."
5
5
  authors = ["Kevin Hester <kevinh@geeksville.com>"]
6
6
  readme = "README.md"
@@ -15,6 +15,7 @@ from rich.progress import track
15
15
  from toml_repo import Repo, RepoManager, get_config_suffix
16
16
 
17
17
  import starbash
18
+ from starbash import doit_types
18
19
  from starbash.aliases import (
19
20
  Aliases,
20
21
  get_aliases,
@@ -185,6 +186,9 @@ class Starbash:
185
186
 
186
187
  # Load app defaults and initialize the repository manager
187
188
  self._init_repos()
189
+ doit_types.configure_max_contexts(
190
+ self.user_repo.get("config.max_contexts", doit_types.DEFAULT_MAX_CONTEXTS)
191
+ )
188
192
  self._init_analytics(cmd) # after init repos so we have user prefs
189
193
 
190
194
  app_version = version("starbash")
@@ -27,10 +27,10 @@ UPLOAD_PATH_BLACKLIST = ("Gemfile",)
27
27
  MAX_BLOB_UPLOADS = 4
28
28
 
29
29
 
30
- def _rewrite() -> Path:
30
+ def _rewrite(github_username: str | None = None) -> Path:
31
31
  site: Path | None = None
32
32
  with Starbash("publish.github.rewrite") as sb:
33
- site = GitHubPublisher(sb).publish()
33
+ site = GitHubPublisher(sb, github_username=github_username).publish()
34
34
  assert site is not None
35
35
  console.print(f"Generated site: {site}")
36
36
  return site
@@ -248,7 +248,18 @@ def _publish_github(dry_run: bool, login: bool) -> None:
248
248
  if credential is None:
249
249
  credential = _authenticate()
250
250
 
251
- site = _rewrite()
251
+ service: GitHubService | None = None
252
+ owner: str | None = None
253
+ if not dry_run:
254
+ assert credential is not None
255
+ service = _credential_service(credential)
256
+ if credential.needs_refresh() and credential.refresh_token:
257
+ service.apply_token_response(
258
+ service.refresh_access_token(CLIENT_ID, credential.refresh_token)
259
+ )
260
+ owner = str(service.user()["login"])
261
+
262
+ site = _rewrite(owner) if owner else _rewrite()
252
263
  files = sorted(
253
264
  path
254
265
  for path in site.rglob("*")
@@ -271,13 +282,10 @@ def _publish_github(dry_run: bool, login: bool) -> None:
271
282
  console.print("No GitHub repository, branch, commit, or Pages configuration was changed.")
272
283
  return
273
284
  assert credential is not None
285
+ assert service is not None
286
+ assert owner is not None
274
287
  with analytics_start_span(name="github", op="upload"):
275
- service = _credential_service(credential)
276
288
  try:
277
- if credential.needs_refresh() and credential.refresh_token:
278
- service.apply_token_response(
279
- service.refresh_access_token(CLIENT_ID, credential.refresh_token)
280
- )
281
289
  with Progress(
282
290
  SpinnerColumn(),
283
291
  TextColumn("[progress.description]{task.description}"),
@@ -288,7 +296,6 @@ def _publish_github(dry_run: bool, login: bool) -> None:
288
296
  ) as progress:
289
297
  operation = progress.add_task("Contacting GitHub", total=8 + len(files))
290
298
 
291
- owner = str(service.user()["login"])
292
299
  pages_url = f"https://{owner}.github.io/starbash-public/"
293
300
  progress.update(operation, description=f"Authenticated as {owner}", advance=1)
294
301
  _require_app_installation(service)
@@ -91,6 +91,14 @@ model.long = "Optolong 2-inch L-Extreme Dual Band 7nm Ha+Oiii Filter"
91
91
  url.info = "https://www.highpointscientific.com/optolong-2-inch-l-extreme-dual-band-7nm-ha-oiii-filter-lxt-200"
92
92
  subtype = "HaOiii"
93
93
 
94
+ [[equipment]]
95
+ type = "filter"
96
+ vendor = "Agena"
97
+ fits.filter = "Tri" # must match the FITS header FILTER field for this filter if you want auto matching.
98
+ model.short = "ant-tri"
99
+ model.long = "Agena Tri-Band RGB Ultra Filter"
100
+ url.info = "https://agenaastro.com/antlia-triband-rgb-ultra-filter-2-mounted.html"
101
+
94
102
  [[equipment]]
95
103
  type = "mount"
96
104
  vendor = "Juwei"
@@ -138,7 +146,9 @@ HaOiii = ["HaOiii", "HaO3", "Duo"]
138
146
  # Astro is the name Dwarf3 uses for their IR cut filter
139
147
  # VIS is the name Dwarf3 uses for their 'clear' filter
140
148
  # IRCUT/LP are the names Seestar uses for their filters
141
- None = ["None", "Astro", "IRCUT", "LP", "VIS"]
149
+ # Tri is a common name for a tri-band light pollution filter (LPF)
150
+
151
+ None = ["None", "Astro", "IRCUT", "LP", "VIS", "Tri"]
142
152
 
143
153
  camera_osc = ["OSC", "ZWO ASI2600MC Duo"]
144
154
  camera_seestar = ["Seestar", "Seestar S50", "Seestar S30", "Seestar S30 Pro"]
@@ -157,7 +167,10 @@ camera_seestar = ["Seestar", "Seestar S50", "Seestar S30", "Seestar S30 Pro"]
157
167
  # http://www.antliafilter.com/pd.jsp?fromColId=2&id=160#_pp=2_671
158
168
  SiiHb = ["SiiHb", "S2Hb"]
159
169
 
170
+ [config]
171
+ # Set this lower if you need to reduce disk usage. If omitted, the default is 2 contexts.
172
+ max_contexts = 2
160
173
 
161
174
  # List the initial search path to find other repos, when the app launches it always first loads this sb.toml file and this file should list all of the
162
175
  # standard default repo locations. When searching repos, repos listed LAST have precedence, so target file can override the root processing defaults,
163
- # then the user prefs, then a live github URL or whatever
176
+ # then the user prefs, then a live github URL or whatever
@@ -22,7 +22,7 @@ from toml_repo import Repo
22
22
  from starbash import InputDef
23
23
  from starbash.database import ImageRow
24
24
  from starbash.doit_types import TaskDict
25
- from starbash.exception import UserHandledError
25
+ from starbash.exception import FilesystemUnavailableError, UserHandledError
26
26
  from starbash.os import symlink_or_copy
27
27
  from starbash.paths import get_user_cache_dir
28
28
  from starbash.tool.base import Tool
@@ -257,7 +257,9 @@ def merge_to(base_name: str, fi: FileInfo) -> None:
257
257
  indexed_ids = []
258
258
  break
259
259
  indexed_ids.append(source_ids[source_index - 1])
260
- if len(indexed_ids) == len(matching_files) and len(set(indexed_ids)) == len(indexed_ids):
260
+ if len(indexed_ids) == len(matching_files) and len(set(indexed_ids)) == len(
261
+ indexed_ids
262
+ ):
261
263
  resolved_source_ids = indexed_ids
262
264
  else:
263
265
  logging.warning(
@@ -269,7 +271,9 @@ def merge_to(base_name: str, fi: FileInfo) -> None:
269
271
  )
270
272
 
271
273
  if resolved_source_ids is not None:
272
- for matching_file, source_id in zip(matching_files, resolved_source_ids, strict=True):
274
+ for matching_file, source_id in zip(
275
+ matching_files, resolved_source_ids, strict=True
276
+ ):
273
277
  source_path = Path(matching_file)
274
278
  collected_files.append(source_path)
275
279
  provenance[source_path.name] = source_id
@@ -288,7 +292,7 @@ def merge_to(base_name: str, fi: FileInfo) -> None:
288
292
  ):
289
293
  dest_name = f"{base_name}_{index:05d}.fits"
290
294
  dest_path = output_dir / dest_name
291
- #logging.debug(f"Linking {source_file} to {dest_path}")
295
+ # logging.debug(f"Linking {source_file} to {dest_path}")
292
296
  symlink_or_copy(str(source_file), str(dest_path))
293
297
 
294
298
  merged_provenance = {
@@ -355,6 +359,17 @@ class ToolAction(BaseAction):
355
359
  self.result = self.tool.run(
356
360
  self.commands, context=context, cwd=self.cwd, log_out=logfile, **self.parameters
357
361
  )
362
+ except OSError as e:
363
+ error = FilesystemUnavailableError(
364
+ f"running {self.tool.name} for task '{self.task.name}'", e
365
+ )
366
+ logging.error("%s (%s)", error, e)
367
+ self.task.meta["exception"] = error
368
+ return TaskFailed(str(error))
369
+ except FilesystemUnavailableError as e:
370
+ logging.error("%s", e)
371
+ self.task.meta["exception"] = e
372
+ return TaskFailed(str(e))
358
373
  except Exception as e:
359
374
  # We pass back any exceptions in task.meta - so that our ConsoleReporter can pick them up (doit normally strips exceptions)
360
375
  self.task.meta["exception"] = e
@@ -422,6 +437,12 @@ class ProcessingResult:
422
437
  # General error from user misconfiguration or tools - not a bug in our code
423
438
  logging.error(f"Skipping run due to: {e}")
424
439
  self.notes = str(e)
440
+ elif isinstance(e, OSError):
441
+ self.notes = (
442
+ "The filesystem became unavailable during processing. "
443
+ "Check that the drive or network mount is connected, then retry."
444
+ )
445
+ logging.error("Filesystem unavailable during processing: %s", e)
425
446
  else:
426
447
  # Unexpected exception - log it and re-raise
427
448
  logging.exception("Unexpected error during processing:")
@@ -562,7 +583,7 @@ class StarbashDoit(TaskLoader2):
562
583
  "verbosity": 2,
563
584
  "dep_file": dep_file,
564
585
  "reporter": MyReporter,
565
- "backend": "dbm", # the json backend is slow and buggy, use dbm instead
586
+ "backend": "dbm", # the json backend is slow and buggy, use dbm instead
566
587
  }
567
588
 
568
589
  def load_tasks(self, cmd: Any, pos_args: Any) -> list[Task]:
@@ -0,0 +1,85 @@
1
+ """Shared types and utilities for doit processing to avoid circular imports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import shutil
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from starbash.paths import get_user_cache_dir
12
+
13
+ type TaskDict = dict[str, Any] # a doit task dictionary
14
+
15
+ DEFAULT_MAX_CONTEXTS = 2
16
+ max_contexts = DEFAULT_MAX_CONTEXTS
17
+
18
+
19
+ def configure_max_contexts(value: Any) -> None:
20
+ """Set the maximum number of processing contexts from user configuration.
21
+
22
+ Invalid values are ignored so a malformed preference cannot prevent
23
+ Starbash from starting.
24
+ """
25
+ global max_contexts
26
+
27
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
28
+ logging.warning(
29
+ "Ignoring invalid max_contexts preference %r; using %d.",
30
+ value,
31
+ DEFAULT_MAX_CONTEXTS,
32
+ )
33
+ max_contexts = DEFAULT_MAX_CONTEXTS
34
+ return
35
+
36
+ max_contexts = value
37
+
38
+
39
+ def get_processing_dir() -> Path:
40
+ """Get the base directory for processing contexts."""
41
+ cache_dir = get_user_cache_dir()
42
+ processing_dir = cache_dir / "processing"
43
+ processing_dir.mkdir(parents=True, exist_ok=True)
44
+ return processing_dir
45
+
46
+
47
+ def cleanup_old_contexts() -> None:
48
+ """Remove oldest context directories if we exceed max_contexts."""
49
+ processing_dir = get_processing_dir()
50
+ logging.debug(f"Removing old processing contexts in: {processing_dir}")
51
+ if not processing_dir.exists():
52
+ return
53
+
54
+ # Safety guard: if we're running under pytest but the processing dir is NOT a test
55
+ # override (no explicit cache override and no STARBASH_CACHE_DIR), refuse to delete
56
+ # anything. A leaked test global (e.g. a small max_contexts from a test config) must
57
+ # never prune the real user's cache.
58
+ from starbash import paths
59
+
60
+ if os.environ.get("PYTEST_CURRENT_TEST") is not None:
61
+ using_override = (
62
+ paths._override_cache_dir is not None or os.getenv("STARBASH_CACHE_DIR")
63
+ )
64
+ if not using_override:
65
+ logging.warning(
66
+ "Refusing to clean processing contexts during tests without a cache override: %s",
67
+ processing_dir,
68
+ )
69
+ return
70
+
71
+ # Get all subdirectories in processing_dir
72
+ contexts = [d for d in processing_dir.iterdir() if d.is_dir()]
73
+
74
+ # If we have more than max_contexts, delete the oldest ones
75
+ if len(contexts) > max_contexts:
76
+ # Sort by modification time (oldest first)
77
+ contexts.sort(key=lambda d: d.stat().st_mtime)
78
+
79
+ # Calculate how many to delete
80
+ num_to_delete = len(contexts) - max_contexts
81
+
82
+ # Delete the oldest directories
83
+ for context_dir in contexts[:num_to_delete]:
84
+ logging.debug(f"Removing old processing context: {context_dir}")
85
+ shutil.rmtree(context_dir, ignore_errors=True)
@@ -25,6 +25,17 @@ class UserHandledError(NonSoftwareError):
25
25
  return False
26
26
 
27
27
 
28
+ class FilesystemUnavailableError(UserHandledError):
29
+ """Raised when a filesystem disappears while processing is in progress."""
30
+
31
+ def __init__(self, operation: str, cause: OSError) -> None:
32
+ super().__init__(
33
+ f"The filesystem became unavailable while {operation}. "
34
+ "Check that the drive or network mount is connected, then retry."
35
+ )
36
+ self.cause = cause
37
+
38
+
28
39
  def raise_missing_repo(kind: str) -> NoReturn:
29
40
  """Raise a UserHandledError indicating that a repository of the given kind is missing."""
30
41
  raise UserHandledError(
@@ -54,6 +65,7 @@ class NoSuitableMastersException(NonFatalException):
54
65
 
55
66
  __all__ = [
56
67
  "UserHandledError",
68
+ "FilesystemUnavailableError",
57
69
  "NonFatalException",
58
70
  "NotEnoughFilesError",
59
71
  "NoSuitableMastersException",
@@ -30,6 +30,10 @@ def _apply_filter(requires: RequireDef, candidates: list[ImageRow]) -> list[Imag
30
30
 
31
31
  kind = get_safe(requires, "kind")
32
32
  value = requires.get("value") # value is optional for some kinds
33
+ # Optional `invert` negates the boolean match so non-matching candidates are
34
+ # kept (e.g. select sessions whose filter is NOT narrowband). Only meaningful
35
+ # for boolean-match kinds (metadata/camera/unprocessed/filename), not min_count.
36
+ invert = bool(requires.get("invert", False))
33
37
 
34
38
  # Stage 1: Filter candidates using kind-specific filter functions
35
39
  def _filter_metadata(metadata: Metadata) -> bool:
@@ -95,7 +99,10 @@ def _apply_filter(requires: RequireDef, candidates: list[ImageRow]) -> list[Imag
95
99
  raise ValueError(f"Unknown requires kind: {kind}")
96
100
 
97
101
  # Apply the filter function to all candidates
98
- filtered_candidates = [img for img in candidates if filter_func(img)]
102
+ if invert:
103
+ filtered_candidates = [img for img in candidates if not filter_func(img)]
104
+ else:
105
+ filtered_candidates = [img for img in candidates if filter_func(img)]
99
106
 
100
107
  # Stage 2: Handle min_count check after filtering
101
108
  if kind == "min_count":
@@ -8,11 +8,34 @@ from astropy.io import fits
8
8
  __all__ = ["read_dimensions", "read_fits_header"]
9
9
 
10
10
 
11
+ def _json_safe(value: Any) -> Any:
12
+ """Convert an astropy header value into a plain JSON-serializable Python value.
13
+
14
+ Commentary cards (COMMENT/HISTORY/blank keyword) come back as
15
+ ``_HeaderCommentaryCards`` (a list-like of strings) and other cards can be
16
+ ``bool``, numpy scalars etc. None of those serialize via ``json.dumps``.
17
+ """
18
+ # Commentary cards: list-like container of strings; join into one string
19
+ if isinstance(value, fits.header._HeaderCommentaryCards):
20
+ return "\n".join(value)
21
+ if isinstance(value, (list, tuple)):
22
+ return [_json_safe(v) for v in value]
23
+ if isinstance(value, bool): # must precede int check (bool is an int subclass)
24
+ return value
25
+ if isinstance(value, (int, float, str)) or value is None:
26
+ return value
27
+ # numpy scalars and anything else: fall back to str()
28
+ return str(value)
29
+
30
+
11
31
  def read_fits_header(file_path: str | Path) -> dict[str, Any]:
12
- """Read and return the primary FITS header from ``file_path``."""
32
+ """Read and return the primary FITS header from ``file_path``.
33
+
34
+ All values are converted to plain JSON-serializable Python types.
35
+ """
13
36
  with fits.open(str(file_path), memmap=False) as hdul:
14
37
  hdu0: Any = hdul[0]
15
- return dict(hdu0.header)
38
+ return {key: _json_safe(value) for key, value in hdu0.header.items()}
16
39
 
17
40
 
18
41
  def read_dimensions(file_path: str | Path) -> tuple[int, int]: