starbash 0.1.11__py3-none-any.whl → 0.1.15__py3-none-any.whl

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 (43) hide show
  1. repo/__init__.py +1 -1
  2. repo/manager.py +14 -23
  3. repo/repo.py +52 -10
  4. starbash/__init__.py +10 -3
  5. starbash/aliases.py +49 -4
  6. starbash/analytics.py +3 -2
  7. starbash/app.py +287 -565
  8. starbash/check_version.py +18 -0
  9. starbash/commands/__init__.py +2 -1
  10. starbash/commands/info.py +26 -21
  11. starbash/commands/process.py +76 -24
  12. starbash/commands/repo.py +25 -68
  13. starbash/commands/select.py +140 -148
  14. starbash/commands/user.py +88 -23
  15. starbash/database.py +41 -27
  16. starbash/defaults/starbash.toml +1 -0
  17. starbash/exception.py +21 -0
  18. starbash/main.py +29 -7
  19. starbash/paths.py +23 -9
  20. starbash/processing.py +724 -0
  21. starbash/recipes/README.md +3 -0
  22. starbash/recipes/master_bias/starbash.toml +4 -1
  23. starbash/recipes/master_dark/starbash.toml +0 -1
  24. starbash/recipes/osc.py +190 -0
  25. starbash/recipes/osc_dual_duo/starbash.toml +31 -34
  26. starbash/recipes/osc_simple/starbash.toml +82 -0
  27. starbash/recipes/osc_single_duo/starbash.toml +51 -32
  28. starbash/recipes/seestar/starbash.toml +82 -0
  29. starbash/recipes/starbash.toml +8 -9
  30. starbash/selection.py +29 -38
  31. starbash/templates/repo/master.toml +7 -3
  32. starbash/templates/repo/processed.toml +7 -2
  33. starbash/templates/userconfig.toml +9 -0
  34. starbash/toml.py +13 -13
  35. starbash/tool.py +186 -149
  36. starbash-0.1.15.dist-info/METADATA +216 -0
  37. starbash-0.1.15.dist-info/RECORD +45 -0
  38. starbash/recipes/osc_dual_duo/starbash.py +0 -147
  39. starbash-0.1.11.dist-info/METADATA +0 -147
  40. starbash-0.1.11.dist-info/RECORD +0 -40
  41. {starbash-0.1.11.dist-info → starbash-0.1.15.dist-info}/WHEEL +0 -0
  42. {starbash-0.1.11.dist-info → starbash-0.1.15.dist-info}/entry_points.txt +0 -0
  43. {starbash-0.1.11.dist-info → starbash-0.1.15.dist-info}/licenses/LICENSE +0 -0
starbash/tool.py CHANGED
@@ -1,17 +1,43 @@
1
+ import logging
1
2
  import os
3
+ import re
2
4
  import shutil
3
- import textwrap
4
- import tempfile
5
5
  import subprocess
6
- import re
7
- import threading
8
- import queue
9
- import logging
6
+ import tempfile
7
+ import textwrap
8
+ from typing import Any
9
+
10
10
  import RestrictedPython
11
+ from rich.live import Live
12
+ from rich.spinner import Spinner
13
+ from rich.traceback import Traceback
14
+
15
+ from starbash.commands import SPINNER_STYLE
16
+ from starbash.exception import UserHandledError
11
17
 
12
18
  logger = logging.getLogger(__name__)
13
19
 
14
20
 
21
+ class ToolError(UserHandledError):
22
+ """Exception raised when a tool fails to execute properly."""
23
+
24
+ def __init__(self, *args: object, command: str, arguments: str | None) -> None:
25
+ super().__init__(*args)
26
+ self.command = command
27
+ self.arguments = arguments
28
+
29
+ def ask_user_handled(self) -> bool:
30
+ from starbash import console # Lazy import to avoid circular dependency
31
+
32
+ console.print(
33
+ f"'{self.command}' failed while running [bold red]{self.arguments}[/bold red]"
34
+ )
35
+ return True
36
+
37
+ def __rich__(self) -> Any:
38
+ return f"Tool: [red]'{self.command}'[/red] failed"
39
+
40
+
15
41
  class _SafeFormatter(dict):
16
42
  """A dictionary for safe string formatting that ignores missing keys during expansion."""
17
43
 
@@ -30,7 +56,7 @@ def expand_context(s: str, context: dict) -> str:
30
56
  expanded = s
31
57
  previous = None
32
58
  max_iterations = 10 # Safety break for infinite recursion
33
- for i in range(max_iterations):
59
+ for _i in range(max_iterations):
34
60
  if expanded == previous:
35
61
  break # Expansion is complete
36
62
  previous = expanded
@@ -161,14 +187,8 @@ def strip_comments(text: str) -> str:
161
187
  return "\n".join(lines)
162
188
 
163
189
 
164
- def tool_run(
165
- cmd: str, cwd: str, commands: str | None = None, timeout: float | None = None
166
- ) -> None:
167
- """Executes an external tool with an optional script of commands in a given working directory.
168
-
169
- Streams stdout and stderr in real-time to the logger, allowing you to see subprocess output
170
- as it happens rather than waiting for completion.
171
- """
190
+ def tool_run(cmd: str, cwd: str, commands: str | None = None, timeout: float | None = None) -> None:
191
+ """Executes an external tool with an optional script of commands in a given working directory."""
172
192
 
173
193
  logger.debug(f"Running {cmd} in {cwd}: stdin={commands}")
174
194
 
@@ -183,111 +203,39 @@ def tool_run(
183
203
  cwd=cwd,
184
204
  )
185
205
 
186
- # Send commands to stdin if provided
187
- if commands and process.stdin:
188
- try:
189
- process.stdin.write(commands)
190
- process.stdin.close()
191
- except BrokenPipeError:
192
- # Process may have terminated early
193
- pass
194
-
195
- # Stream output line by line in real-time
196
- # Use threading for cross-platform compatibility (select doesn't work on Windows with pipes)
197
-
198
- assert process.stdout
199
- assert process.stderr
200
-
201
- output_queue: queue.Queue = queue.Queue()
202
-
203
- def read_stream(stream, log_func, stream_name):
204
- """Read from stream and put lines in queue."""
205
- try:
206
- for line in stream:
207
- line = line.rstrip("\n")
208
- output_queue.put((log_func, stream_name, line))
209
- finally:
210
- output_queue.put((None, stream_name, None)) # Signal EOF
211
-
212
- # Start threads to read stdout and stderr
213
- stdout_thread = threading.Thread(
214
- target=read_stream,
215
- args=(process.stdout, logger.debug, "tool-stdout"),
216
- daemon=True,
217
- )
218
- stderr_thread = threading.Thread(
219
- target=read_stream,
220
- args=(process.stderr, logger.warning, "tool-stderr"),
221
- daemon=True,
222
- )
223
-
224
- stdout_thread.start()
225
- stderr_thread.start()
226
-
227
- # Track which streams have finished
228
- streams_finished = 0
229
-
206
+ # Wait for process to complete with timeout
230
207
  try:
231
- # Process output from queue until both streams are done
232
- while streams_finished < 2:
233
- try:
234
- # Use timeout to periodically check if process has terminated
235
- log_func, stream_name, line = output_queue.get(timeout=0.1)
236
-
237
- if log_func is None:
238
- # EOF signal
239
- streams_finished += 1
240
- else:
241
- # Log the line
242
- log_func(f"[{stream_name}] {line}")
243
-
244
- except queue.Empty:
245
- # No output available, check if process terminated
246
- if process.poll() is not None:
247
- # Process finished, wait a bit more for remaining output
248
- break
249
-
250
- # Wait for threads to finish (they should be done or very close)
251
- stdout_thread.join(timeout=1.0)
252
- stderr_thread.join(timeout=1.0)
253
-
254
- # Drain any remaining items in queue
255
- while not output_queue.empty():
256
- try:
257
- log_func, stream_name, line = output_queue.get_nowait()
258
- if log_func is not None:
259
- log_func(f"[{stream_name}] {line}")
260
- except queue.Empty:
261
- break
262
-
263
- # Wait for process to complete with timeout
264
- try:
265
- process.wait(timeout=timeout)
266
- except subprocess.TimeoutExpired:
267
- process.kill()
268
- process.wait()
269
- raise RuntimeError(f"Tool timed out after {timeout} seconds")
208
+ stdout_lines, stderr_lines = process.communicate(input=commands, timeout=timeout)
209
+ except subprocess.TimeoutExpired:
210
+ process.kill()
211
+ stdout_lines, stderr_lines = process.communicate()
212
+ raise RuntimeError(f"Tool timed out after {timeout} seconds")
213
+
214
+ returncode = process.returncode
215
+
216
+ if stderr_lines:
217
+ logger.warning(f"[tool-warnings] {stderr_lines}")
218
+
219
+ if returncode != 0:
220
+ # log stdout with warn priority because the tool failed
221
+ logger.warning(f"[tool] {stdout_lines}")
222
+ raise ToolError(
223
+ f"{cmd} failed with exit code {returncode}", command=cmd, arguments=commands
224
+ )
225
+ else:
226
+ logger.debug(f"[tool] {stdout_lines}")
227
+ logger.debug("Tool command successful.")
270
228
 
271
- returncode = process.returncode
272
229
 
273
- if returncode != 0:
274
- raise RuntimeError(f"Tool failed with exit code {returncode}")
275
- else:
276
- logger.debug("Tool command successful.")
277
- finally:
278
- # Ensure streams are properly closed
279
- if process.stdout:
280
- process.stdout.close()
281
- if process.stderr:
282
- process.stderr.close()
230
+ class MissingToolError(UserHandledError):
231
+ """Exception raised when a required tool is not found."""
283
232
 
233
+ def __init__(self, *args: object, command: str) -> None:
234
+ super().__init__(*args)
235
+ self.command = command
284
236
 
285
- def executable_path(commands: list[str], name: str) -> str:
286
- """Find the correct executable path to run for the given tool"""
287
- for cmd in commands:
288
- if shutil.which(cmd):
289
- return cmd
290
- raise FileNotFoundError(f"{name} not found, you probably need to install it.")
237
+ def __rich__(self) -> Any:
238
+ return str(self) # FIXME do something better here?
291
239
 
292
240
 
293
241
  class Tool:
@@ -303,34 +251,83 @@ class Tool:
303
251
  def set_defaults(self):
304
252
  # default timeout in seconds, if you need to run a tool longer than this, you should change
305
253
  # it before calling run()
306
- self.timeout = 10.0
254
+ self.timeout = (
255
+ 5 * 60.0 # 5 minutes - just to make sure we eventually stop all tools
256
+ )
257
+
258
+ def run(self, commands: str, context: dict = {}, cwd: str | None = None) -> None:
259
+ """Run commands inside this tool
307
260
 
308
- def run_in_temp_dir(self, commands: str, context: dict = {}) -> None:
309
- """Run commands inside this tool (with cwd pointing to a temp directory)"""
310
- # Create a temporary directory for processing
311
- temp_dir = tempfile.mkdtemp(prefix=self.name)
261
+ If cwd is provided, use that as the working directory otherwise a temp directory is used as cwd.
262
+ """
263
+ from starbash import console # Lazy import to avoid circular dependency
312
264
 
313
- context["temp_dir"] = (
314
- temp_dir # pass our directory path in for the tool's usage
265
+ temp_dir = None
266
+ spinner = Spinner(
267
+ "arc", text=f"Tool running: [bold]{self.name}[/bold]...", speed=2.0, style=SPINNER_STYLE
315
268
  )
269
+ with Live(spinner, console=console, refresh_per_second=5):
270
+ try:
271
+ if not cwd:
272
+ # Create a temporary directory for processing
273
+ cwd = temp_dir = tempfile.mkdtemp(prefix=self.name)
274
+
275
+ context["temp_dir"] = (
276
+ temp_dir # pass our directory path in for the tool's usage
277
+ )
278
+
279
+ self._run(cwd, commands, context=context)
280
+ finally:
281
+ spinner.update(text=f"Tool completed: [bold]{self.name}[/bold].")
282
+ if temp_dir:
283
+ shutil.rmtree(temp_dir)
284
+ context.pop("temp_dir", None)
285
+
286
+ def _run(self, cwd: str, commands: str, context: dict = {}) -> None:
287
+ """Run commands inside this tool (with cwd pointing to the specified directory)"""
288
+ raise NotImplementedError()
289
+
316
290
 
291
+ class ExternalTool(Tool):
292
+ """A tool provided by an external executable"""
293
+
294
+ def __init__(self, name: str, commands: list[str], install_url: str) -> None:
295
+ super().__init__(name)
296
+ self.commands = commands
297
+ self.install_url = install_url
298
+
299
+ def preflight(self) -> None:
300
+ """Check that the tool is available"""
317
301
  try:
318
- self.run(temp_dir, commands, context=context)
319
- finally:
320
- shutil.rmtree(temp_dir)
302
+ _ = self.executable_path # raise if not found
303
+ except MissingToolError:
304
+ logger.warning(
305
+ textwrap.dedent(f"""\
306
+ The {self.name} executable was not found. Some features will be unavailable until you install it.
307
+ Click [link={self.install_url}]here[/link] for installation instructions.""")
308
+ )
321
309
 
322
- def run(self, cwd: str, commands: str, context: dict = {}) -> None:
323
- """Run commands inside this tool (with cwd pointing to the specified directory)"""
324
- raise NotImplementedError()
310
+ @property
311
+ def executable_path(self) -> str:
312
+ """Find the correct executable path to run for the given tool"""
313
+ for cmd in self.commands:
314
+ if shutil.which(cmd):
315
+ return cmd
316
+ raise MissingToolError(
317
+ f"{self.name} not found. Installation instructions [link={self.install_url}]here[/link]",
318
+ command=self.name,
319
+ )
325
320
 
326
321
 
327
- class SirilTool(Tool):
322
+ class SirilTool(ExternalTool):
328
323
  """Expose Siril as a tool"""
329
324
 
330
325
  def __init__(self) -> None:
331
- super().__init__("siril")
326
+ # siril_path = "/home/kevinh/packages/Siril-1.4.0~beta3-x86_64.AppImage"
327
+ # Possible siril commands, with preferred option first
328
+ super().__init__("siril", ["siril-cli", "siril", "org.siril.Siril"], "https://siril.org/")
332
329
 
333
- def run(self, cwd: str, commands: str, context: dict = {}) -> None:
330
+ def _run(self, cwd: str, commands: str, context: dict = {}) -> None:
334
331
  """Executes Siril with a script of commands in a given working directory."""
335
332
 
336
333
  # Iteratively expand the command string to handle nested placeholders.
@@ -341,10 +338,7 @@ class SirilTool(Tool):
341
338
 
342
339
  temp_dir = cwd
343
340
 
344
- # siril_path = "/home/kevinh/packages/Siril-1.4.0~beta3-x86_64.AppImage"
345
- # Possible siril commands, with preferred option first
346
- siril_commands = ["org.siril.Siril", "siril-cli", "siril"]
347
- siril_path = executable_path(siril_commands, "Siril")
341
+ siril_path = self.executable_path
348
342
  if siril_path == "org.siril.Siril":
349
343
  # The executable is inside a flatpak, so run the lighter/faster/no-gui required exe
350
344
  # from inside the flatpak
@@ -352,10 +346,14 @@ class SirilTool(Tool):
352
346
 
353
347
  # Create symbolic links for all input files in the temp directory
354
348
  for f in input_files:
355
- os.symlink(
356
- os.path.abspath(str(f)),
357
- os.path.join(temp_dir, os.path.basename(str(f))),
358
- )
349
+ dest_file = os.path.join(temp_dir, os.path.basename(str(f)))
350
+
351
+ # if a script is re-run we might already have the input file symlinks
352
+ if not os.path.exists(dest_file):
353
+ os.symlink(
354
+ os.path.abspath(str(f)),
355
+ dest_file,
356
+ )
359
357
 
360
358
  # We dedent here because the commands are often indented multiline strings
361
359
  script_content = textwrap.dedent(
@@ -377,21 +375,53 @@ class SirilTool(Tool):
377
375
  tool_run(cmd, temp_dir, script_content, timeout=self.timeout)
378
376
 
379
377
 
380
- class GraxpertTool(Tool):
378
+ class GraxpertTool(ExternalTool):
381
379
  """Expose Graxpert as a tool"""
382
380
 
383
381
  def __init__(self) -> None:
384
- super().__init__("graxpert")
382
+ super().__init__("graxpert", ["graxpert"], "https://graxpert.com/")
385
383
 
386
- def run(self, cwd: str, commands: str, context: dict = {}) -> None:
384
+ def _run(self, cwd: str, commands: str, context: dict = {}) -> None:
387
385
  """Executes Graxpert with the specified command line arguments"""
388
386
 
389
387
  # Arguments look similar to: graxpert -cmd background-extraction -output /tmp/testout tests/test_images/real_crummy.fits
390
- cmd = f"graxpert {commands}"
388
+ cmd = f"{self.executable_path} {commands}"
391
389
 
392
390
  tool_run(cmd, cwd, timeout=self.timeout)
393
391
 
394
392
 
393
+ class PythonScriptError(UserHandledError):
394
+ """Exception raised when an error occurs during Python script execution."""
395
+
396
+ def ask_user_handled(self) -> bool:
397
+ """Prompt the user with a friendly message about the error.
398
+ Returns:
399
+ True if the error was handled, False otherwise.
400
+ """
401
+ from starbash import console # Lazy import to avoid circular dependency
402
+
403
+ console.print(
404
+ """[bold red]Python Script Error[/bold red] please contact the script author and
405
+ give them this information.
406
+
407
+ Processing for the current file will be skipped..."""
408
+ )
409
+
410
+ # Show the traceback with Rich formatting
411
+ if self.__cause__:
412
+ traceback = Traceback.from_exception(
413
+ type(self.__cause__),
414
+ self.__cause__,
415
+ self.__cause__.__traceback__,
416
+ show_locals=True,
417
+ )
418
+ console.print(traceback)
419
+ else:
420
+ console.print(f"[yellow]{str(self)}[/yellow]")
421
+
422
+ return True
423
+
424
+
395
425
  class PythonTool(Tool):
396
426
  """Expose Python as a tool"""
397
427
 
@@ -401,7 +431,7 @@ class PythonTool(Tool):
401
431
  # default script file override
402
432
  self.default_script_file = "starbash.py"
403
433
 
404
- def run(self, cwd: str, commands: str, context: dict = {}) -> None:
434
+ def _run(self, cwd: str, commands: str, context: dict = {}) -> None:
405
435
  original_cwd = os.getcwd()
406
436
  try:
407
437
  os.chdir(cwd) # cd to where this script expects to run
@@ -416,14 +446,21 @@ class PythonTool(Tool):
416
446
  globals = {"context": context}
417
447
  exec(byte_code, make_safe_globals(globals), execution_locals)
418
448
  except SyntaxError as e:
419
- raise # Just rethrow - no need to rewrap
449
+ raise PythonScriptError("Syntax error in python script") from e
450
+ except UserHandledError:
451
+ raise # No need to wrap this - just pass it through for user handling
420
452
  except Exception as e:
421
- raise ValueError(f"Error during python script execution") from e
453
+ raise PythonScriptError("Error during python script execution") from e
422
454
  finally:
423
455
  os.chdir(original_cwd)
424
456
 
425
457
 
458
+ def preflight_tools() -> None:
459
+ """Preflight check all known tools to see if they are available"""
460
+ for tool in tools.values():
461
+ if isinstance(tool, ExternalTool):
462
+ tool.preflight()
463
+
464
+
426
465
  # A dictionary mapping tool names to their respective tool instances.
427
- tools: dict[str, Tool] = {
428
- tool.name: tool for tool in [SirilTool(), GraxpertTool(), PythonTool()]
429
- }
466
+ tools: dict[str, Tool] = {tool.name: tool for tool in [SirilTool(), GraxpertTool(), PythonTool()]}
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: starbash
3
+ Version: 0.1.15
4
+ Summary: A tool for automating/standardizing/sharing astrophotography workflows.
5
+ License-File: LICENSE
6
+ Author: Kevin Hester
7
+ Author-email: kevinh@geeksville.com
8
+ Requires-Python: >=3.12,<3.15
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: astropy (>=7.1.1,<8.0.0)
14
+ Requires-Dist: multidict (>=6.7.0,<7.0.0)
15
+ Requires-Dist: platformdirs (>=4.5.0,<5.0.0)
16
+ Requires-Dist: restrictedpython (>=8.1,<9.0)
17
+ Requires-Dist: rich (>=14.2.0,<15.0.0)
18
+ Requires-Dist: sentry-sdk (>=2.42.1,<3.0.0)
19
+ Requires-Dist: tomlkit (>=0.13.3,<0.14.0)
20
+ Requires-Dist: typer (>=0.20.0,<0.21.0)
21
+ Requires-Dist: update-checker (>=0.18.0,<0.19.0)
22
+ Description-Content-Type: text/markdown
23
+
24
+ # [Starbash](https://github.com/geeksville/starbash)
25
+
26
+ <img src="https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/img/icon.png" alt="Starbash: Astrophotography workflows simplified" width="30%" align="right" style="margin-bottom: 20px;">
27
+
28
+ [![PyPI - Version](https://img.shields.io/pypi/v/starbash)](https://pypi.org/project/starbash/)
29
+ [![GitHub branch check runs](https://img.shields.io/github/check-runs/geeksville/starbash/main)](https://github.com/geeksville/starbash/actions)
30
+ [![codecov](https://codecov.io/github/geeksville/starbash/graph/badge.svg?token=47RE10I7O1)](https://codecov.io/github/geeksville/starbash)
31
+
32
+ A tool for automating/standardizing/sharing astrophotography workflows.
33
+
34
+ * Automatic - with sensible defaults that you can change as needed.
35
+ * Easy - provides a 'seestar-like' starting point for autoprocessing all your sessions (by default).
36
+ * Fast - even with large image repositories. Automatic master bias and flat generation and reasonable defaults.
37
+ * Multi-session - by default. So your workflows can stack from multiple nights (and still use the correct flats etc...).
38
+ * Shareable - you can share/use recipes for image preprocessing flows.
39
+
40
+ (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.)
41
+
42
+ <br clear="right">
43
+
44
+ # Current status
45
+
46
+ This project is still very young - but making good progress 😊!
47
+
48
+ If you are interested in alpha-testing we ❤️ you. This README should have enough instructions to get you going, but if you encounter **any** problems please file a github issue and we'll work together to fix them.
49
+
50
+ ![Sample session movie](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
51
+
52
+ ## Current features
53
+
54
+ * Automatically recognizes and auto-parses the default NINA, Asiair, and Seestar raw file repos (adding support for other layouts is easy).
55
+ * Multi-session support by default (including automatic selection of correct flats, biases, and dark frames).
56
+ * 'Repos' can contain raw files, generated masters, preprocessed files, or recipes.
57
+ * Automatically performs **complete** preprocessing on OSC (broadband, narrowband, or dual Duo filter), Mono (LRGB, SHO) data. i.e., gives you 'seestar-level' auto-preprocessing, so you only need to do the (optional) custom post-processing.
58
+
59
+ ## Features coming soon
60
+
61
+ * Support for mono-camera workflows (this alpha version only supports color cameras).
62
+ * Generates a per-target report/config file which can be customized if the detected defaults or preprocessing are not what you want.
63
+ * 'Recipes' provide repeatable/human-readable/shareable descriptions of all processing steps.
64
+ * Repos can be on the local disk or shared via HTTPS/GitHub/etc. This is particularly useful for recipe repos.
65
+ * Uses Siril and Graxpert for its pre-processing operations (support for Pixinsight-based recipes will probably be coming at some point...).
66
+ * The target report can be used to auto-generate a human-friendly 'postable/shareable' report about that image.
67
+ * Target reports are shareable so that you can request comments from others and others can rerender with different settings.
68
+
69
+ See the [TODO](TODO.md) file for work items and approximate schedule.
70
+
71
+ ## Installing
72
+
73
+ Currently the easiest way to install this command-line based tool is via [pipx](https://pipx.pypa.io/stable/). If you don't already have pipx and you have Python installed, you can auto-install it by running "pip install --user pipx." If you don't have Python installed see the pipx link for pipx installers for any OS.
74
+
75
+ Once pipx is installed just run the following **two** commands (the `sb --install-completion` will make TAB auto-complete automatically complete `sb` options for most platforms). Installing auto-complete is **highly** recommended because it makes entering starbash commands fast by pressing the TAB key:
76
+
77
+ ```
78
+ ➜ pipx install starbash
79
+ installed package starbash 0.1.3, installed using Python 3.12.3
80
+ These apps are now globally available
81
+ - sb
82
+ - starbash
83
+ done! ✨ 🌟 ✨
84
+
85
+ ➜ sb --install-completion
86
+ bash completion installed in /home/.../sb.sh
87
+ Completion will take effect once you restart the terminal
88
+
89
+ ```
90
+
91
+ ## Use
92
+
93
+ ### Initial setup
94
+
95
+ The first time you launch starbash you will be prompted to choose a few options. You will also be told how you can add your existing raw frames and an input repo.
96
+
97
+ ![user setup](doc/img/user-setup.png)
98
+
99
+ If you ever want to rerun this setup just run 'sb user setup'
100
+
101
+ ### Automatic stacking/preprocessing
102
+
103
+ One of the main goals of starbash is to provide 'seestar-like' automatic image preprocessing:
104
+ * automatic stacking (even over multiple sessions) - (via Siril)
105
+ * automatic recipe selection (color, bw, duo filters etc...), but you can customize if starbash picks poorly
106
+ * background removal - (via Graxpert by default) provided as extra (optional) output files
107
+ * star removal - (via Starnet by default) provided as extra (optional) output files
108
+ * no changes to input repos - you can safely ask starbash to auto-process your entire tree of raw images. Processed images go in a special 'processed' output repo.
109
+
110
+ ![auto session](doc/vhs/process-auto.gif)
111
+
112
+ How to use:
113
+
114
+ * Step 1 - Select some sessions. Example commands to use (when running commands the tool will provide feedback on what the current session set contains):
115
+
116
+ ```
117
+ sb select any # selects all sessions in your repo
118
+ sb select # prints information about the current selection
119
+ sb select list # lists all sessions in the current selection
120
+ sb select date after 2025-09-01
121
+ sb select date before 2025-10-01
122
+ sb select date between 2025-07-03 2025-10-01
123
+
124
+ sb select target m31 # select all sessions with m31.
125
+ # Note: tab completion is supported so if you type select target m<tab> you should get a list of all the Messier objects you have in your images.
126
+ # In fact, tab completion works on virtually any starbash option - pressing tab for dates will show you dates you have image sessions for instance...
127
+ ```
128
+
129
+ * Step 2 - Generate 'master' images. This will auto-stack your raw BIAS, DARK, FLAT etc... frames as single frame masters. You only need to perform this step once:
130
+
131
+ ```
132
+ sb process masters
133
+ ```
134
+
135
+ * Step 3 - Do auto-process. This will process all of the sessions you currently have selected. It will group outputs by target name and it will auto-select flat frames on a per-session-date basis. At the end of processing a list of targets and their processing will be printed.
136
+
137
+ ```
138
+ sb process auto
139
+ ```
140
+
141
+ In the output directory we will eventually be putting a 'starbash.toml' file with information about what choices were made during processing (which masters selected, which recipes selected..., selected Siril options, etc...). You can edit that file to pick different choices and if you reprocess that target your choices will be used. (Note: this is not yet implemented in the release version of the tool - but soon.)
142
+
143
+ ### Manual Siril processing
144
+
145
+ FIXME - add getting started instructions (possibly with a screenshare video)
146
+
147
+ ![siril session](doc/vhs/process-siril.gif)
148
+
149
+ ## Supported commands
150
+
151
+ ### Repository Management
152
+ - `sb repo list [--verbose]` - List installed repos (use `-v` for details)
153
+ - `sb repo add [--master] [filepath|URL]` - Add a repository, optionally specifying the type
154
+ - `sb repo remove <REPOURL>` - Remove the indicated repo from the repo list
155
+ - `sb repo reindex [--force] <REPOURL>` - Reindex the specified repo (or all repos if none specified)
156
+
157
+ ### User Preferences
158
+ - `sb user name "Your Name"` - Set name for attribution in generated images
159
+ - `sb user email "foo@example.com"` - Set email for attribution in generated images
160
+ - `sb user analytics <on|off>` - Turn analytics collection on/off
161
+ - `sb user setup` - Configure starbash via a brief guided process
162
+
163
+ ### Selection & Filtering
164
+ - `sb select` - Show information about the current selection
165
+ - `sb select list` - List sessions (filtered based on the current selection)
166
+ - `sb select any` - Remove all filters (select everything)
167
+ - `sb select target <TARGETNAME>` - Limit selection to the named target
168
+ - `sb select telescope <TELESCOPENAME>` - Limit selection to the named telescope
169
+ - `sb select date <after|before|between> <DATE> [DATE]` - Limit to sessions in the specified date range
170
+ - `sb select export SESSIONNUM DESTDIR` - Export the images for the indicated session number into the specified directory (or current directory if not specified). If possible, symbolic links are used; if not, the files are copied.
171
+
172
+ ### Selection information
173
+ - `sb info` - Show user preferences location and other app info
174
+ - `sb info target` - List targets (filtered based on the current selection)
175
+ - `sb info telescope` - List instruments (filtered based on the current selection)
176
+ - `sb info filter` - List all filters found in current selection
177
+ - `sb info master [KIND]` - List all precalculated master images (darks, biases, flats). Optional KIND argument to filter by image type (e.g., BIAS, DARK, FLAT).
178
+
179
+ ## Not yet supported commands
180
+
181
+ ### Export & Processing
182
+ - `sb process siril [--run] SESSIONNUM DESTDIR` - Generate Siril directory tree and optionally run Siril GUI.
183
+ - `sb process auto [SESSIONNUM]` - Automatic processing. If session # is specified, process only that session; otherwise all selected sessions will be processed.
184
+ - `sb process masters` - Generate master flats, darks, and biases from available raw frames in the current selection.
185
+
186
+ ## Supported telescope software
187
+
188
+ FIXME explain FITS and directory paths
189
+
190
+ * N.I.N.A. - tested, seems fairly okay.
191
+ * Asiair - tested, seems fairly okay.
192
+ * Seestar - tested, seems fairly okay.
193
+ * Ekos/Kstars - not tested; please try it and file a GitHub issue if you see any problems.
194
+
195
+ ## Supported tools (now)
196
+
197
+ * Siril
198
+ * Graxpert
199
+ * Python (you can add Python code to recipes if necessary)
200
+
201
+ ## Supported tools (future?)
202
+
203
+ * Pixinsight?
204
+ * Autostakkert?
205
+
206
+ ## Development
207
+
208
+ We try to make this project useful and friendly. If you find problems please file a GitHub issue.
209
+ We accept pull-requests and enjoy discussing possible new development directions via GitHub issues. If you might want to work on this, just describe what your interests are and we can talk about how to get it merged.
210
+
211
+ [Click here](doc/development.md) for the current work in progress developer docs. They will get better before our beta release...
212
+
213
+ ## License
214
+
215
+ Copyright 2025 Kevin Hester, kevinh@geeksville.com.
216
+ Licensed under the [GPL v3](LICENSE)