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.
- repo/__init__.py +1 -1
- repo/manager.py +14 -23
- repo/repo.py +52 -10
- starbash/__init__.py +10 -3
- starbash/aliases.py +49 -4
- starbash/analytics.py +3 -2
- starbash/app.py +287 -565
- starbash/check_version.py +18 -0
- starbash/commands/__init__.py +2 -1
- starbash/commands/info.py +26 -21
- starbash/commands/process.py +76 -24
- starbash/commands/repo.py +25 -68
- starbash/commands/select.py +140 -148
- starbash/commands/user.py +88 -23
- starbash/database.py +41 -27
- starbash/defaults/starbash.toml +1 -0
- starbash/exception.py +21 -0
- starbash/main.py +29 -7
- starbash/paths.py +23 -9
- starbash/processing.py +724 -0
- starbash/recipes/README.md +3 -0
- starbash/recipes/master_bias/starbash.toml +4 -1
- starbash/recipes/master_dark/starbash.toml +0 -1
- starbash/recipes/osc.py +190 -0
- starbash/recipes/osc_dual_duo/starbash.toml +31 -34
- starbash/recipes/osc_simple/starbash.toml +82 -0
- starbash/recipes/osc_single_duo/starbash.toml +51 -32
- starbash/recipes/seestar/starbash.toml +82 -0
- starbash/recipes/starbash.toml +8 -9
- starbash/selection.py +29 -38
- starbash/templates/repo/master.toml +7 -3
- starbash/templates/repo/processed.toml +7 -2
- starbash/templates/userconfig.toml +9 -0
- starbash/toml.py +13 -13
- starbash/tool.py +186 -149
- starbash-0.1.15.dist-info/METADATA +216 -0
- starbash-0.1.15.dist-info/RECORD +45 -0
- starbash/recipes/osc_dual_duo/starbash.py +0 -147
- starbash-0.1.11.dist-info/METADATA +0 -147
- starbash-0.1.11.dist-info/RECORD +0 -40
- {starbash-0.1.11.dist-info → starbash-0.1.15.dist-info}/WHEEL +0 -0
- {starbash-0.1.11.dist-info → starbash-0.1.15.dist-info}/entry_points.txt +0 -0
- {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
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
|
286
|
-
|
|
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 =
|
|
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
|
-
|
|
309
|
-
"""
|
|
310
|
-
#
|
|
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
|
-
|
|
314
|
-
|
|
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.
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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(
|
|
322
|
+
class SirilTool(ExternalTool):
|
|
328
323
|
"""Expose Siril as a tool"""
|
|
329
324
|
|
|
330
325
|
def __init__(self) -> None:
|
|
331
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
356
|
-
|
|
357
|
-
|
|
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(
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
+
[](https://pypi.org/project/starbash/)
|
|
29
|
+
[](https://github.com/geeksville/starbash/actions)
|
|
30
|
+
[](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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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)
|