starbash 0.1.9__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 +145 -0
- starbash/analytics.py +3 -2
- starbash/app.py +512 -473
- starbash/check_version.py +18 -0
- starbash/commands/__init__.py +2 -1
- starbash/commands/info.py +88 -14
- starbash/commands/process.py +76 -24
- starbash/commands/repo.py +41 -68
- starbash/commands/select.py +141 -142
- starbash/commands/user.py +88 -23
- starbash/database.py +219 -112
- starbash/defaults/starbash.toml +24 -3
- starbash/exception.py +21 -0
- starbash/main.py +29 -7
- starbash/paths.py +35 -5
- starbash/processing.py +724 -0
- starbash/recipes/README.md +3 -0
- starbash/recipes/master_bias/starbash.toml +16 -19
- starbash/recipes/master_dark/starbash.toml +33 -0
- starbash/recipes/master_flat/starbash.toml +26 -18
- starbash/recipes/osc.py +190 -0
- starbash/recipes/osc_dual_duo/starbash.toml +54 -44
- 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 +30 -9
- starbash/selection.py +32 -36
- starbash/templates/repo/master.toml +7 -3
- starbash/templates/repo/processed.toml +15 -0
- starbash/templates/userconfig.toml +9 -0
- starbash/toml.py +13 -13
- starbash/tool.py +230 -96
- 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 -151
- starbash-0.1.9.dist-info/METADATA +0 -145
- starbash-0.1.9.dist-info/RECORD +0 -37
- {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/WHEEL +0 -0
- {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/entry_points.txt +0 -0
- {starbash-0.1.9.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
|
-
|
|
8
|
-
import
|
|
6
|
+
import tempfile
|
|
7
|
+
import textwrap
|
|
8
|
+
from typing import Any
|
|
9
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,83 +187,55 @@ def strip_comments(text: str) -> str:
|
|
|
161
187
|
return "\n".join(lines)
|
|
162
188
|
|
|
163
189
|
|
|
164
|
-
def tool_run(cmd: str, cwd: str, commands: str | None = None) -> None:
|
|
190
|
+
def tool_run(cmd: str, cwd: str, commands: str | None = None, timeout: float | None = None) -> None:
|
|
165
191
|
"""Executes an external tool with an optional script of commands in a given working directory."""
|
|
166
192
|
|
|
167
193
|
logger.debug(f"Running {cmd} in {cwd}: stdin={commands}")
|
|
168
|
-
result = subprocess.run(
|
|
169
|
-
cmd, input=commands, shell=True, capture_output=True, text=True, cwd=cwd
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
if result.stderr:
|
|
173
|
-
logger.warning(f"Tool error message:\n{result.stderr.strip()}")
|
|
174
194
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def executable_path(commands: list[str], name: str) -> str:
|
|
187
|
-
"""Find the correct executable path to run for the given tool"""
|
|
188
|
-
for cmd in commands:
|
|
189
|
-
if shutil.which(cmd):
|
|
190
|
-
return cmd
|
|
191
|
-
raise FileNotFoundError(f"{name} not found, you probably need to install it.")
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def siril_run(temp_dir: str, commands: str, input_files: list[str] = []) -> None:
|
|
195
|
-
"""Executes Siril with a script of commands in a given working directory."""
|
|
196
|
-
|
|
197
|
-
# siril_path = "/home/kevinh/packages/Siril-1.4.0~beta3-x86_64.AppImage"
|
|
198
|
-
# Possible siril commands, with preferred option first
|
|
199
|
-
siril_commands = ["org.siril.Siril", "siril-cli", "siril"]
|
|
200
|
-
siril_path = executable_path(siril_commands, "Siril")
|
|
201
|
-
if siril_path == "org.siril.Siril":
|
|
202
|
-
# The executable is inside a flatpak, so run the lighter/faster/no-gui required exe
|
|
203
|
-
# from inside the flatpak
|
|
204
|
-
siril_path = "flatpak run --command=siril-cli org.siril.Siril"
|
|
205
|
-
|
|
206
|
-
# Create symbolic links for all input files in the temp directory
|
|
207
|
-
for f in input_files:
|
|
208
|
-
os.symlink(
|
|
209
|
-
os.path.abspath(str(f)), os.path.join(temp_dir, os.path.basename(str(f)))
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
# We dedent here because the commands are often indented multiline strings
|
|
213
|
-
script_content = textwrap.dedent(
|
|
214
|
-
f"""
|
|
215
|
-
requires 1.4.0-beta3
|
|
216
|
-
{textwrap.dedent(strip_comments(commands))}
|
|
217
|
-
"""
|
|
195
|
+
# Start the process with pipes for streaming
|
|
196
|
+
process = subprocess.Popen(
|
|
197
|
+
cmd,
|
|
198
|
+
stdin=subprocess.PIPE if commands else None,
|
|
199
|
+
stdout=subprocess.PIPE,
|
|
200
|
+
stderr=subprocess.PIPE,
|
|
201
|
+
shell=True,
|
|
202
|
+
text=True,
|
|
203
|
+
cwd=cwd,
|
|
218
204
|
)
|
|
219
205
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
206
|
+
# Wait for process to complete with timeout
|
|
207
|
+
try:
|
|
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")
|
|
223
213
|
|
|
224
|
-
|
|
225
|
-
# It seems like the -d command may also be required when siril is in a flatpak
|
|
226
|
-
cmd = f"{siril_path} -d {temp_dir} -s -"
|
|
214
|
+
returncode = process.returncode
|
|
227
215
|
|
|
228
|
-
|
|
216
|
+
if stderr_lines:
|
|
217
|
+
logger.warning(f"[tool-warnings] {stderr_lines}")
|
|
229
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.")
|
|
230
228
|
|
|
231
|
-
def graxpert_run(cwd: str, arguments: str) -> None:
|
|
232
|
-
"""Executes Graxpert with the specified command line arguments"""
|
|
233
229
|
|
|
234
|
-
|
|
235
|
-
|
|
230
|
+
class MissingToolError(UserHandledError):
|
|
231
|
+
"""Exception raised when a required tool is not found."""
|
|
236
232
|
|
|
237
|
-
|
|
238
|
-
|
|
233
|
+
def __init__(self, *args: object, command: str) -> None:
|
|
234
|
+
super().__init__(*args)
|
|
235
|
+
self.command = command
|
|
239
236
|
|
|
240
|
-
|
|
237
|
+
def __rich__(self) -> Any:
|
|
238
|
+
return str(self) # FIXME do something better here?
|
|
241
239
|
|
|
242
240
|
|
|
243
241
|
class Tool:
|
|
@@ -248,33 +246,89 @@ class Tool:
|
|
|
248
246
|
|
|
249
247
|
# default script file name
|
|
250
248
|
self.default_script_file = None
|
|
249
|
+
self.set_defaults()
|
|
251
250
|
|
|
252
|
-
def
|
|
253
|
-
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
context["temp_dir"] = (
|
|
258
|
-
temp_dir # pass our directory path in for the tool's usage
|
|
251
|
+
def set_defaults(self):
|
|
252
|
+
# default timeout in seconds, if you need to run a tool longer than this, you should change
|
|
253
|
+
# it before calling run()
|
|
254
|
+
self.timeout = (
|
|
255
|
+
5 * 60.0 # 5 minutes - just to make sure we eventually stop all tools
|
|
259
256
|
)
|
|
260
257
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
def run(self, commands: str, context: dict = {}, cwd: str | None = None) -> None:
|
|
259
|
+
"""Run commands inside this tool
|
|
260
|
+
|
|
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
|
|
265
264
|
|
|
266
|
-
|
|
265
|
+
temp_dir = None
|
|
266
|
+
spinner = Spinner(
|
|
267
|
+
"arc", text=f"Tool running: [bold]{self.name}[/bold]...", speed=2.0, style=SPINNER_STYLE
|
|
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:
|
|
267
287
|
"""Run commands inside this tool (with cwd pointing to the specified directory)"""
|
|
268
288
|
raise NotImplementedError()
|
|
269
289
|
|
|
270
290
|
|
|
271
|
-
class
|
|
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"""
|
|
301
|
+
try:
|
|
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
|
+
)
|
|
309
|
+
|
|
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
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class SirilTool(ExternalTool):
|
|
272
323
|
"""Expose Siril as a tool"""
|
|
273
324
|
|
|
274
325
|
def __init__(self) -> None:
|
|
275
|
-
|
|
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/")
|
|
276
329
|
|
|
277
|
-
def
|
|
330
|
+
def _run(self, cwd: str, commands: str, context: dict = {}) -> None:
|
|
331
|
+
"""Executes Siril with a script of commands in a given working directory."""
|
|
278
332
|
|
|
279
333
|
# Iteratively expand the command string to handle nested placeholders.
|
|
280
334
|
# The loop continues until the string no longer changes.
|
|
@@ -282,17 +336,90 @@ class SirilTool(Tool):
|
|
|
282
336
|
|
|
283
337
|
input_files = context.get("input_files", [])
|
|
284
338
|
|
|
285
|
-
|
|
339
|
+
temp_dir = cwd
|
|
286
340
|
|
|
341
|
+
siril_path = self.executable_path
|
|
342
|
+
if siril_path == "org.siril.Siril":
|
|
343
|
+
# The executable is inside a flatpak, so run the lighter/faster/no-gui required exe
|
|
344
|
+
# from inside the flatpak
|
|
345
|
+
siril_path = "flatpak run --command=siril-cli org.siril.Siril"
|
|
287
346
|
|
|
288
|
-
|
|
347
|
+
# Create symbolic links for all input files in the temp directory
|
|
348
|
+
for f in input_files:
|
|
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
|
+
)
|
|
357
|
+
|
|
358
|
+
# We dedent here because the commands are often indented multiline strings
|
|
359
|
+
script_content = textwrap.dedent(
|
|
360
|
+
f"""
|
|
361
|
+
requires 1.4.0-beta3
|
|
362
|
+
{textwrap.dedent(strip_comments(expanded))}
|
|
363
|
+
"""
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
logger.debug(
|
|
367
|
+
f"Running Siril in {temp_dir}, ({len(input_files)} input files) cmds:\n{script_content}"
|
|
368
|
+
)
|
|
369
|
+
logger.info(f"Running Siril ({len(input_files)} input files)")
|
|
370
|
+
|
|
371
|
+
# The `-s -` arguments tell Siril to run in script mode and read commands from stdin.
|
|
372
|
+
# It seems like the -d command may also be required when siril is in a flatpak
|
|
373
|
+
cmd = f"{siril_path} -d {temp_dir} -s -"
|
|
374
|
+
|
|
375
|
+
tool_run(cmd, temp_dir, script_content, timeout=self.timeout)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class GraxpertTool(ExternalTool):
|
|
289
379
|
"""Expose Graxpert as a tool"""
|
|
290
380
|
|
|
291
381
|
def __init__(self) -> None:
|
|
292
|
-
super().__init__("graxpert")
|
|
382
|
+
super().__init__("graxpert", ["graxpert"], "https://graxpert.com/")
|
|
383
|
+
|
|
384
|
+
def _run(self, cwd: str, commands: str, context: dict = {}) -> None:
|
|
385
|
+
"""Executes Graxpert with the specified command line arguments"""
|
|
386
|
+
|
|
387
|
+
# Arguments look similar to: graxpert -cmd background-extraction -output /tmp/testout tests/test_images/real_crummy.fits
|
|
388
|
+
cmd = f"{self.executable_path} {commands}"
|
|
389
|
+
|
|
390
|
+
tool_run(cmd, cwd, timeout=self.timeout)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class PythonScriptError(UserHandledError):
|
|
394
|
+
"""Exception raised when an error occurs during Python script execution."""
|
|
293
395
|
|
|
294
|
-
def
|
|
295
|
-
|
|
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
|
|
296
423
|
|
|
297
424
|
|
|
298
425
|
class PythonTool(Tool):
|
|
@@ -304,7 +431,7 @@ class PythonTool(Tool):
|
|
|
304
431
|
# default script file override
|
|
305
432
|
self.default_script_file = "starbash.py"
|
|
306
433
|
|
|
307
|
-
def
|
|
434
|
+
def _run(self, cwd: str, commands: str, context: dict = {}) -> None:
|
|
308
435
|
original_cwd = os.getcwd()
|
|
309
436
|
try:
|
|
310
437
|
os.chdir(cwd) # cd to where this script expects to run
|
|
@@ -319,14 +446,21 @@ class PythonTool(Tool):
|
|
|
319
446
|
globals = {"context": context}
|
|
320
447
|
exec(byte_code, make_safe_globals(globals), execution_locals)
|
|
321
448
|
except SyntaxError as e:
|
|
322
|
-
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
|
|
323
452
|
except Exception as e:
|
|
324
|
-
raise
|
|
453
|
+
raise PythonScriptError("Error during python script execution") from e
|
|
325
454
|
finally:
|
|
326
455
|
os.chdir(original_cwd)
|
|
327
456
|
|
|
328
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
|
+
|
|
329
465
|
# A dictionary mapping tool names to their respective tool instances.
|
|
330
|
-
tools: dict[str, Tool] = {
|
|
331
|
-
tool.name: tool for tool in [SirilTool(), GraxpertTool(), PythonTool()]
|
|
332
|
-
}
|
|
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)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
repo/__init__.py,sha256=BFeAqakKGMYnEL75Tq0C9h64MMOmgW2ikhSOaBV3xII,229
|
|
2
|
+
repo/manager.py,sha256=E2wuadrZEjTkFGbWbX2VvTEfIE_mHgmIfRJ9HhTud6I,4430
|
|
3
|
+
repo/repo.py,sha256=ku5Sv7zsdyfX2LKGZXysNJNoM5ex0vsRbFRrtHHSje4,12138
|
|
4
|
+
starbash/__init__.py,sha256=WV5wnwLNeaFjM928K51u8l9YQ9G1OFFXJr6WMEjraeQ,1394
|
|
5
|
+
starbash/aliases.py,sha256=yghNRCkH30T0CwasGlwgbHbWIL92NrO8ecX43W4Jeqo,5234
|
|
6
|
+
starbash/analytics.py,sha256=G8Yxkfqfs0IsXr5RJ6uIqXR17gdTEQ-z6eioaxyn2P0,4338
|
|
7
|
+
starbash/app.py,sha256=kxOwWHSvSWeAs2IEKOemwA12uBNinEzT0wua8M3gS68,36158
|
|
8
|
+
starbash/check_version.py,sha256=ZbRsz98hPtYMVJ9A_UJsCrWT74x9xhx4jtkUb5rUofE,540
|
|
9
|
+
starbash/commands/__init__.py,sha256=qXJuMGTIoDMpw_Bmfnqm6cmv9ATWLgWmxHchXU_H-FI,781
|
|
10
|
+
starbash/commands/info.py,sha256=l-cop6rgTxdgyXC3KykxpGZK2tBt9YrGXlfRO9v9zlI,6256
|
|
11
|
+
starbash/commands/process.py,sha256=dDSeJRp-fXLBOw27IC18CUf828aiIpLDPwI61f0P2U8,6885
|
|
12
|
+
starbash/commands/repo.py,sha256=DUDerSx8RPUTVAAbpvuLyJUkLChcaNyWl23KQjzbArg,6700
|
|
13
|
+
starbash/commands/select.py,sha256=yIjc_9wlMWbfmuGCRU_IGIRP535AJNu1mp5oVTn3TwI,14832
|
|
14
|
+
starbash/commands/user.py,sha256=F7mlr9ob6gguGr4g7K1SeRNsi95Ax5hlhEqjF6rb1YM,7021
|
|
15
|
+
starbash/database.py,sha256=Wnfn4CFzOw73o-0KrN-KoXHc1-JkohyOHqYS4aclaYQ,27785
|
|
16
|
+
starbash/defaults/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
starbash/defaults/starbash.toml,sha256=TecXD_9pr-7w_VBLYUBtuyd6F5tpfve-66z2XSoZsdA,3233
|
|
18
|
+
starbash/exception.py,sha256=xj6zugSpKyGjo-akqKf-E92aFvHUDrPKiV7Pw0DftiM,656
|
|
19
|
+
starbash/main.py,sha256=EiF1zu-ITN-csoyuPReSEM4j3IhBG4DZQglUvHHSfVE,2499
|
|
20
|
+
starbash/paths.py,sha256=AlGLqjgrgUYZQu_vkW2Qym5Mtc7fEkpWxMWI8c3790c,2589
|
|
21
|
+
starbash/processing.py,sha256=KiQH3SK8r0vBkYBc6B8ys7TZlH22rFMR_gRO7aCrHUI,31990
|
|
22
|
+
starbash/recipes/README.md,sha256=OMePKQejJb6bTSrtzuE05JcMkfEdBpVZnQtQab_chsA,446
|
|
23
|
+
starbash/recipes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
starbash/recipes/master_bias/starbash.toml,sha256=ru1gyzC0C86t0EAQ2DPir_5BG2DFKRTwWCCvmPt5SX0,2924
|
|
25
|
+
starbash/recipes/master_dark/starbash.toml,sha256=2-X8dChr5Z0K1xoMQr8udELpUprrve_tBHzyKEPJMqM,758
|
|
26
|
+
starbash/recipes/master_flat/starbash.toml,sha256=MDGm7eLakkzWtPSKsnS8X6N2ZRQ_jUEzSSIvLHKOUVw,1728
|
|
27
|
+
starbash/recipes/osc.py,sha256=mUE5vw47Fvmf1wvEzoj5W6bZO9gjGBwfKvddFg7v63Q,7433
|
|
28
|
+
starbash/recipes/osc_dual_duo/starbash.toml,sha256=0ioaYKqImiqLZTBKMY6c8i4PEg6c_Byp_2-UsJNughc,3457
|
|
29
|
+
starbash/recipes/osc_simple/starbash.toml,sha256=qc0lruuenlp2Xoapz3_51B1-onl-XszGy8KmboA4h94,2685
|
|
30
|
+
starbash/recipes/osc_single_duo/starbash.toml,sha256=QjZ7mioSpgZbwHATBKWLbodt_ot7Oym3kjEtQiNdwNo,2879
|
|
31
|
+
starbash/recipes/seestar/starbash.toml,sha256=2x7Pa1VMKh9WK29VMZ-kHxWhK26YE4sTMm29eQJghcA,2664
|
|
32
|
+
starbash/recipes/starbash.toml,sha256=BendeZEtfIwEjvAJYmtI5R_Phhq2nFaXaQ-r9kSS0hQ,1483
|
|
33
|
+
starbash/selection.py,sha256=KxuYQQdou51WOuwUSUNyOQjXKQ4A_AoFi-Lj2XjzNrM,10855
|
|
34
|
+
starbash/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
+
starbash/templates/repo/master.toml,sha256=bWGtKLlhqzAS0knKvCAHKpne-NLSZDNJcX3u3OzjON8,662
|
|
36
|
+
starbash/templates/repo/processed.toml,sha256=wj4BpPKh6D4wMfCFWosFSwznVe4Ys5gL96XhxmCVpow,597
|
|
37
|
+
starbash/templates/userconfig.toml,sha256=qFrKEyyvKpefhDO7eVrezsLO7XReGNI_SY6Q1Aa8ZyE,1787
|
|
38
|
+
starbash/toml.py,sha256=XuNMq_1EyX3rJaW5lRMpKO_kIXdWItSPypJli2IN5Z0,793
|
|
39
|
+
starbash/tool.py,sha256=qJQg_W19av5JwYsd0gYNK7v2ZH8FC_Z_3WZJX3qGBNQ,16664
|
|
40
|
+
starbash/url.py,sha256=lorxQJ27jSfzsKCb0QvpcvLiPZG55Dkd_c1JPFbni4I,402
|
|
41
|
+
starbash-0.1.15.dist-info/METADATA,sha256=HXQJb2uApSn8_mhit5EN8MwI7U6EnmToKfvCXzRMX20,11420
|
|
42
|
+
starbash-0.1.15.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
43
|
+
starbash-0.1.15.dist-info/entry_points.txt,sha256=REQyWs8e5TJsNK7JVVWowKVoytMmKlUwuFHLTmSX4hc,67
|
|
44
|
+
starbash-0.1.15.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
45
|
+
starbash-0.1.15.dist-info/RECORD,,
|