starbash 0.1.9__py3-none-any.whl → 0.1.11__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.
- starbash/aliases.py +100 -0
- starbash/app.py +509 -192
- starbash/commands/info.py +69 -0
- starbash/commands/repo.py +19 -3
- starbash/commands/select.py +8 -1
- starbash/database.py +187 -94
- starbash/defaults/starbash.toml +23 -3
- starbash/paths.py +18 -2
- starbash/recipes/master_bias/starbash.toml +12 -18
- starbash/recipes/master_dark/starbash.toml +34 -0
- starbash/recipes/master_flat/starbash.toml +26 -18
- starbash/recipes/osc_dual_duo/starbash.py +1 -5
- starbash/recipes/osc_dual_duo/starbash.toml +41 -28
- starbash/recipes/osc_single_duo/starbash.toml +4 -4
- starbash/recipes/starbash.toml +26 -4
- starbash/selection.py +6 -1
- starbash/templates/repo/processed.toml +10 -0
- starbash/tool.py +161 -64
- {starbash-0.1.9.dist-info → starbash-0.1.11.dist-info}/METADATA +5 -3
- starbash-0.1.11.dist-info/RECORD +40 -0
- starbash-0.1.9.dist-info/RECORD +0 -37
- {starbash-0.1.9.dist-info → starbash-0.1.11.dist-info}/WHEEL +0 -0
- {starbash-0.1.9.dist-info → starbash-0.1.11.dist-info}/entry_points.txt +0 -0
- {starbash-0.1.9.dist-info → starbash-0.1.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
[repo]
|
|
3
|
+
kind = "recipe"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
[recipe]
|
|
7
|
+
author.name = "FIXMESiril?"
|
|
8
|
+
author.email = "FIXMESiril?"
|
|
9
|
+
|
|
10
|
+
[recipe.stage.master-dark]
|
|
11
|
+
|
|
12
|
+
description = "Generate master dark"
|
|
13
|
+
|
|
14
|
+
tool.name = "siril"
|
|
15
|
+
tool.timeout = 30
|
|
16
|
+
|
|
17
|
+
input.type = "dark"
|
|
18
|
+
|
|
19
|
+
# Look for files in input repos, finding them by using the "relative" tag they contain
|
|
20
|
+
input.source = "repo"
|
|
21
|
+
input.required = 2 # siril needs at least 2 frames to stack
|
|
22
|
+
|
|
23
|
+
# Based on the following definitions in the stage toml file...
|
|
24
|
+
output.dest = "repo" # write to a particular repo
|
|
25
|
+
output.type = "master" # write output to the special masters repo
|
|
26
|
+
|
|
27
|
+
script = '''
|
|
28
|
+
# Convert bias/dark Frames to .fit files
|
|
29
|
+
link frames -out={process_dir}
|
|
30
|
+
cd {process_dir}
|
|
31
|
+
|
|
32
|
+
# Stack frames
|
|
33
|
+
stack frames rej 3 3 -nonorm -out={output["base_path"]}
|
|
34
|
+
'''
|
|
@@ -8,28 +8,36 @@ author.name = "FIXMESiril?"
|
|
|
8
8
|
author.email = "FIXMESiril?"
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
[
|
|
11
|
+
[recipe.stage.master-flat]
|
|
12
|
+
|
|
13
|
+
# See master_bias/starbash.toml for more documentation
|
|
12
14
|
|
|
13
15
|
description = "Generate master flat"
|
|
14
|
-
disabled =
|
|
16
|
+
# disabled = false
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
tool.name = "siril"
|
|
19
|
+
# tool.timeout = 15.0 # allow up to 15 seconds before we timeout and kill tool
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
input.
|
|
21
|
+
# or auto?
|
|
22
|
+
# find the most recent raw fits for the current instrument (as of the time of session start)
|
|
23
|
+
# input.source = "most-recent" # only look for the most recent set of raws for this particular type
|
|
24
|
+
input.type = "flat" # look in all raw repos, but look only for flat files
|
|
22
25
|
|
|
23
|
-
#
|
|
24
|
-
input.source = "
|
|
25
|
-
input.
|
|
26
|
-
input.required = true # Is at least one input file required? If true, we will skip running this stage entirely (with a warning)
|
|
26
|
+
# Look for files in input repos, finding them by using the "relative" tag they contain
|
|
27
|
+
input.source = "repo"
|
|
28
|
+
input.required = 2 # siril needs at least 2 frames to stack
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
context.
|
|
30
|
+
# We require a master bias frame for this recipe. By the time our recipe is invoked
|
|
31
|
+
# context.master.bias will have been set to a full path to a master bias frame
|
|
32
|
+
input.masters = ["bias"]
|
|
30
33
|
|
|
31
|
-
#
|
|
32
|
-
|
|
34
|
+
# Based on the following definitions in the stage toml file...
|
|
35
|
+
output.dest = "repo" # write to a particular repo
|
|
36
|
+
output.type = "master" # write output to the special masters repo
|
|
37
|
+
|
|
38
|
+
# FIXME for early development we have support for simple absolute file paths with globs
|
|
39
|
+
#input.source = "path"
|
|
40
|
+
#input.path = "/workspaces/starbash/images/from_astroboy/M 27/2025-09-16/FLAT/*.fit*"
|
|
33
41
|
|
|
34
42
|
script = '''
|
|
35
43
|
# Create a sequence from the raw flat frames
|
|
@@ -37,10 +45,10 @@ script = '''
|
|
|
37
45
|
cd {process_dir}
|
|
38
46
|
|
|
39
47
|
# Calibrate the flat frames using master bias
|
|
40
|
-
calibrate flat -bias={bias}
|
|
48
|
+
calibrate flat -bias={master["bias"]}
|
|
41
49
|
|
|
42
|
-
# Stack the pre-processed (calibrated) flat frames
|
|
43
|
-
stack pp_flat rej 3 3 -norm=mul -out=
|
|
50
|
+
# Stack the pre-processed (calibrated) flat frames
|
|
51
|
+
stack pp_flat rej 3 3 -norm=mul -out={output["base_path"]}
|
|
44
52
|
'''
|
|
45
53
|
|
|
46
54
|
temporaries = ["flat", "pp_flat"]
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import os
|
|
5
5
|
from glob import glob
|
|
6
6
|
from starbash.tool import tools
|
|
7
|
+
from starbash.aliases import normalize_target_name
|
|
7
8
|
|
|
8
9
|
siril = tools["siril"]
|
|
9
10
|
|
|
@@ -18,11 +19,6 @@ def perhaps_delete_temps(temps: list[str]) -> None:
|
|
|
18
19
|
os.remove(path)
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def normalize_target_name(name: str) -> str:
|
|
22
|
-
"""Converts a target name to an any filesystem-safe format by removing spaces"""
|
|
23
|
-
return name.replace(" ", "").upper()
|
|
24
|
-
|
|
25
|
-
|
|
26
22
|
def make_stacked(sessionconfig: str, variant: str, output_file: str):
|
|
27
23
|
"""
|
|
28
24
|
Registers and stacks all pre-processed light frames for a given filter configuration
|
|
@@ -2,21 +2,10 @@
|
|
|
2
2
|
[repo]
|
|
3
3
|
kind = "recipe"
|
|
4
4
|
|
|
5
|
-
# all sb.toml files can optionally contain a version section. if version of the running starbash app is out of bounds a warning message will be printed
|
|
6
|
-
# to the user and the file will be ignored for future processing.
|
|
7
|
-
[recipe.require.version]
|
|
8
|
-
min="0.1.0"
|
|
9
|
-
max="4.5.8"
|
|
10
|
-
|
|
11
5
|
[recipe]
|
|
12
6
|
author.name = "Kevin Hester"
|
|
13
7
|
author.email = "kevinh@geeksville.com"
|
|
14
8
|
|
|
15
|
-
[[stage]]
|
|
16
|
-
|
|
17
|
-
description = "Extract OSC dual duo filter Ha, Oiii and Sii channels"
|
|
18
|
-
disabled = true # FIXME, debugging later stuff
|
|
19
|
-
|
|
20
9
|
# FIXME-somehow-specify-what-filternames are used to auto detect this recipe can be used?
|
|
21
10
|
# figure out how to support dual duo vs single duo. Perhaps: the FIRST recipe that matches an auto rule
|
|
22
11
|
# is used for any auto-defected defaults. If an auto match is found it will be saved in the generated starter
|
|
@@ -25,26 +14,39 @@ disabled = true # FIXME, debugging later stuff
|
|
|
25
14
|
# non OSC people use names like LRGB or SHO
|
|
26
15
|
|
|
27
16
|
# for dual duo if we see Sii assume they also have HaOiii
|
|
28
|
-
auto.
|
|
17
|
+
auto.require.filter = ["SiiOiii"]
|
|
29
18
|
# for single duo look for this
|
|
30
|
-
# auto.
|
|
31
|
-
auto.
|
|
19
|
+
# auto.require.filter = ["HaOiii"]
|
|
20
|
+
auto.require.camera = ["OSC"]
|
|
21
|
+
|
|
22
|
+
# all sb.toml files can optionally contain a version section. if version of the running starbash app is out of bounds a warning message will be printed
|
|
23
|
+
# to the user and the file will be ignored for future processing.
|
|
24
|
+
[recipe.require.version]
|
|
25
|
+
min="0.1.0"
|
|
26
|
+
max="4.5.8"
|
|
27
|
+
|
|
28
|
+
[recipe.stage.light]
|
|
29
|
+
|
|
30
|
+
description = "Extract OSC dual duo filter Ha, Oiii and Sii channels"
|
|
31
|
+
# disabled = true # FIXME, debugging later stuff
|
|
32
32
|
|
|
33
|
-
tool = "siril"
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
tool.name = "siril"
|
|
34
|
+
|
|
35
|
+
when = "session.light" # run once per session.config
|
|
36
|
+
|
|
37
|
+
input.masters = ["bias", "flat"]
|
|
36
38
|
|
|
37
39
|
# FIXME, bias and flat should have been added to context by two previous stages. But for now hardwire
|
|
38
40
|
# Note: they should not have filename extensions (see strip_extension in the old process.py)
|
|
39
|
-
context.bias = '/workspaces/starbash/images/masters/biases/2025-09-09_stacked.fits'
|
|
41
|
+
# context.bias = '/workspaces/starbash/images/masters/biases/2025-09-09_stacked.fits'
|
|
40
42
|
|
|
41
|
-
context.sessionid = "2025-09-16" # FIXME, generate this by looping over all sessions (from outside this file)
|
|
42
|
-
context.sessionconfig = "SiiOiii" # FIXME generate this by looping over all session configs
|
|
43
|
-
context.light_base = "light_s{sessionid}_c{sessionconfig}"
|
|
43
|
+
# context.sessionid = "2025-09-16" # FIXME, generate this by looping over all sessions (from outside this file)
|
|
44
|
+
# context.sessionconfig = "SiiOiii" # FIXME generate this by looping over all session configs
|
|
45
|
+
# context.light_base = "light_s{sessionid}_c{sessionconfig}"
|
|
44
46
|
|
|
45
47
|
# FIXME until auto light finding is in
|
|
46
|
-
input.source = "path"
|
|
47
|
-
input.path = "/workspaces/starbash/images/from_astroboy/M 27/2025-09-16/LIGHT/*.fit*"
|
|
48
|
+
# input.source = "path"
|
|
49
|
+
# input.path = "/workspaces/starbash/images/from_astroboy/M 27/2025-09-16/LIGHT/*.fit*"
|
|
48
50
|
|
|
49
51
|
script = '''
|
|
50
52
|
# Create a sequence from the raw light frames, seq file goes to process_dir
|
|
@@ -63,7 +65,12 @@ script = '''
|
|
|
63
65
|
|
|
64
66
|
temporaries = ["FIXME"]
|
|
65
67
|
|
|
66
|
-
[
|
|
68
|
+
[recipe.stage.stack]
|
|
69
|
+
|
|
70
|
+
disabled = false # not yet ready to test
|
|
71
|
+
|
|
72
|
+
# FIXME this stage should only be considered if the previous stage in this same array
|
|
73
|
+
# was run. It must be run inside the same tempdir (so that files from previous stage are available)
|
|
67
74
|
|
|
68
75
|
# FIXME, eventually we could make it optional to even have a starbash.toml. If we find an
|
|
69
76
|
# starbash.py we could introspect it for a starbash_config dict. And look inside that for description
|
|
@@ -71,11 +78,17 @@ temporaries = ["FIXME"]
|
|
|
71
78
|
|
|
72
79
|
description = "Stack OSC dual duo filter data, with separate Ha, Oiii and Sii channels"
|
|
73
80
|
|
|
74
|
-
context.target = "M 27" # FIXME
|
|
75
|
-
context.targets = "/workspaces/starbash/images/processed" # FIXME, do something smarter
|
|
81
|
+
# context.target = "M 27" # FIXME
|
|
82
|
+
# context.targets = "/workspaces/starbash/images/processed" # FIXME, do something smarter
|
|
83
|
+
|
|
84
|
+
tool.name = "python"
|
|
85
|
+
|
|
86
|
+
when = "final.stack" # run once after all session/session.config processing was done
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
# Based on the following definitions in the stage toml file...
|
|
89
|
+
# FIXME, we should inherit this - most recipes shouldn't have to declare it
|
|
90
|
+
output.dest = "repo" # write to a particular repo
|
|
91
|
+
output.type = "processed" # write output to the special masters repo
|
|
79
92
|
|
|
80
93
|
# if not specified starbash.py used
|
|
81
94
|
# script-file = "script.py"
|
|
@@ -23,8 +23,8 @@ disabled = true # FIXME, we don't yet have auto selection based on filter types
|
|
|
23
23
|
auto.for-filter = ["HaOiii"]
|
|
24
24
|
auto.for-camera = ["OSC"]
|
|
25
25
|
|
|
26
|
-
tool = "siril"
|
|
27
|
-
when = "session
|
|
26
|
+
tool.name = "siril"
|
|
27
|
+
when = "session.light" # run once per session.config
|
|
28
28
|
output = "FIXME"
|
|
29
29
|
|
|
30
30
|
script = '''
|
|
@@ -45,8 +45,8 @@ temporaries = ["FIXME"]
|
|
|
45
45
|
|
|
46
46
|
disabled = true # FIXME, we don't yet have auto selection based on filter types
|
|
47
47
|
|
|
48
|
-
tool = "python"
|
|
49
|
-
when = "session
|
|
48
|
+
tool.name = "python"
|
|
49
|
+
when = "session.stack" # run once after all session/session.config processing was done
|
|
50
50
|
|
|
51
51
|
script-file = "script.py"
|
|
52
52
|
|
starbash/recipes/starbash.toml
CHANGED
|
@@ -5,6 +5,8 @@ kind = "repo"
|
|
|
5
5
|
[[repo-ref]]
|
|
6
6
|
dir = "master_bias"
|
|
7
7
|
[[repo-ref]]
|
|
8
|
+
dir = "master_dark"
|
|
9
|
+
[[repo-ref]]
|
|
8
10
|
dir = "master_flat"
|
|
9
11
|
|
|
10
12
|
# Note: For automated recipe finding, it is important to list more demanding recipes first. For instance:
|
|
@@ -26,14 +28,34 @@ dir = "osc_single_duo"
|
|
|
26
28
|
#name = "setup-masters" # for flat processing, master generation etc
|
|
27
29
|
#priority = 5
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
#
|
|
32
|
+
# master specific stages
|
|
33
|
+
#
|
|
34
|
+
[[master-stages]]
|
|
35
|
+
name = "master-bias" # generate master bias frames
|
|
36
|
+
priority = 10
|
|
37
|
+
input = "bias" # only used for frames of this type
|
|
38
|
+
|
|
39
|
+
[[master-stages]]
|
|
40
|
+
name = "master-dark" # generate master dark frames
|
|
31
41
|
priority = 10
|
|
42
|
+
input = "dark"
|
|
43
|
+
|
|
44
|
+
[[master-stages]]
|
|
45
|
+
name = "master-flat" # generate master flat frames
|
|
46
|
+
priority = 20
|
|
47
|
+
input = "flat"
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
# session specific processing stages, not currently used, for now I just do this list from code
|
|
51
|
+
#
|
|
32
52
|
|
|
33
53
|
[[stages]]
|
|
34
|
-
name = "
|
|
54
|
+
name = "light" # generate light frames from lights and with reference to flats/bias
|
|
35
55
|
priority = 20
|
|
56
|
+
input = "light" # only used for frames of this type
|
|
36
57
|
|
|
37
58
|
[[stages]]
|
|
38
|
-
name = "
|
|
59
|
+
name = "stack" # stack frames
|
|
39
60
|
priority = 30
|
|
61
|
+
input = "light" # only used for frames of this type
|
starbash/selection.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
from typing import Any, Optional, TYPE_CHECKING
|
|
7
7
|
from repo import Repo
|
|
8
|
+
from starbash.aliases import normalize_target_name
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def where_tuple(conditions: dict[str, Any] | None) -> tuple[str, list[Any]]:
|
|
@@ -256,7 +257,11 @@ class Selection:
|
|
|
256
257
|
if self.targets:
|
|
257
258
|
# For now, just use the first target
|
|
258
259
|
# TODO: Support multiple targets in queries
|
|
259
|
-
conditions["OBJECT"] =
|
|
260
|
+
conditions["OBJECT"] = (
|
|
261
|
+
normalize_target_name(self.targets[0])
|
|
262
|
+
if len(self.targets) == 1
|
|
263
|
+
else None
|
|
264
|
+
)
|
|
260
265
|
|
|
261
266
|
if self.filters:
|
|
262
267
|
# For now, just use the first filter
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# This is a processed repository for (Starbash)[{PROJECT_URL}].
|
|
2
|
+
#
|
|
3
|
+
# This file marks the root directory of a set of generated/processed starbash output files.
|
|
4
|
+
#
|
|
5
|
+
# You generally don't need to edit this file directly - it was auto generated when you ran
|
|
6
|
+
# "sb repo add --processed {REPO_PATH}".
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
[repo]
|
|
10
|
+
kind = "processed"
|
starbash/tool.py
CHANGED
|
@@ -4,9 +4,9 @@ import textwrap
|
|
|
4
4
|
import tempfile
|
|
5
5
|
import subprocess
|
|
6
6
|
import re
|
|
7
|
-
|
|
7
|
+
import threading
|
|
8
|
+
import queue
|
|
8
9
|
import logging
|
|
9
|
-
|
|
10
10
|
import RestrictedPython
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
@@ -161,83 +161,133 @@ def strip_comments(text: str) -> str:
|
|
|
161
161
|
return "\n".join(lines)
|
|
162
162
|
|
|
163
163
|
|
|
164
|
-
def tool_run(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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()}")
|
|
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.
|
|
174
168
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
raise RuntimeError(f"Tool failed with exit code {result.returncode}")
|
|
179
|
-
else:
|
|
180
|
-
logger.debug("Tool command successful.")
|
|
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
|
+
"""
|
|
181
172
|
|
|
182
|
-
|
|
183
|
-
logger.debug(f"Tool output:\n{result.stdout.strip()}")
|
|
173
|
+
logger.debug(f"Running {cmd} in {cwd}: stdin={commands}")
|
|
184
174
|
|
|
175
|
+
# Start the process with pipes for streaming
|
|
176
|
+
process = subprocess.Popen(
|
|
177
|
+
cmd,
|
|
178
|
+
stdin=subprocess.PIPE if commands else None,
|
|
179
|
+
stdout=subprocess.PIPE,
|
|
180
|
+
stderr=subprocess.PIPE,
|
|
181
|
+
shell=True,
|
|
182
|
+
text=True,
|
|
183
|
+
cwd=cwd,
|
|
184
|
+
)
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
192
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)
|
|
193
197
|
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
assert process.stdout
|
|
199
|
+
assert process.stderr
|
|
196
200
|
|
|
197
|
-
|
|
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"
|
|
201
|
+
output_queue: queue.Queue = queue.Queue()
|
|
205
202
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
211
|
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
"""
|
|
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,
|
|
218
217
|
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
stderr_thread = threading.Thread(
|
|
219
|
+
target=read_stream,
|
|
220
|
+
args=(process.stderr, logger.warning, "tool-stderr"),
|
|
221
|
+
daemon=True,
|
|
222
222
|
)
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
cmd = f"{siril_path} -d {temp_dir} -s -"
|
|
224
|
+
stdout_thread.start()
|
|
225
|
+
stderr_thread.start()
|
|
227
226
|
|
|
228
|
-
|
|
227
|
+
# Track which streams have finished
|
|
228
|
+
streams_finished = 0
|
|
229
229
|
|
|
230
|
+
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
|
|
230
262
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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")
|
|
270
|
+
|
|
271
|
+
returncode = process.returncode
|
|
272
|
+
|
|
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()
|
|
236
283
|
|
|
237
|
-
# Arguments look similar to: graxpert -cmd background-extraction -output /tmp/testout tests/test_images/real_crummy.fits
|
|
238
|
-
cmd = f"{graxpert_path} {arguments}"
|
|
239
284
|
|
|
240
|
-
|
|
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.")
|
|
241
291
|
|
|
242
292
|
|
|
243
293
|
class Tool:
|
|
@@ -248,6 +298,12 @@ class Tool:
|
|
|
248
298
|
|
|
249
299
|
# default script file name
|
|
250
300
|
self.default_script_file = None
|
|
301
|
+
self.set_defaults()
|
|
302
|
+
|
|
303
|
+
def set_defaults(self):
|
|
304
|
+
# default timeout in seconds, if you need to run a tool longer than this, you should change
|
|
305
|
+
# it before calling run()
|
|
306
|
+
self.timeout = 10.0
|
|
251
307
|
|
|
252
308
|
def run_in_temp_dir(self, commands: str, context: dict = {}) -> None:
|
|
253
309
|
"""Run commands inside this tool (with cwd pointing to a temp directory)"""
|
|
@@ -275,6 +331,7 @@ class SirilTool(Tool):
|
|
|
275
331
|
super().__init__("siril")
|
|
276
332
|
|
|
277
333
|
def run(self, cwd: str, commands: str, context: dict = {}) -> None:
|
|
334
|
+
"""Executes Siril with a script of commands in a given working directory."""
|
|
278
335
|
|
|
279
336
|
# Iteratively expand the command string to handle nested placeholders.
|
|
280
337
|
# The loop continues until the string no longer changes.
|
|
@@ -282,7 +339,42 @@ class SirilTool(Tool):
|
|
|
282
339
|
|
|
283
340
|
input_files = context.get("input_files", [])
|
|
284
341
|
|
|
285
|
-
|
|
342
|
+
temp_dir = cwd
|
|
343
|
+
|
|
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")
|
|
348
|
+
if siril_path == "org.siril.Siril":
|
|
349
|
+
# The executable is inside a flatpak, so run the lighter/faster/no-gui required exe
|
|
350
|
+
# from inside the flatpak
|
|
351
|
+
siril_path = "flatpak run --command=siril-cli org.siril.Siril"
|
|
352
|
+
|
|
353
|
+
# Create symbolic links for all input files in the temp directory
|
|
354
|
+
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
|
+
)
|
|
359
|
+
|
|
360
|
+
# We dedent here because the commands are often indented multiline strings
|
|
361
|
+
script_content = textwrap.dedent(
|
|
362
|
+
f"""
|
|
363
|
+
requires 1.4.0-beta3
|
|
364
|
+
{textwrap.dedent(strip_comments(expanded))}
|
|
365
|
+
"""
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
logger.debug(
|
|
369
|
+
f"Running Siril in {temp_dir}, ({len(input_files)} input files) cmds:\n{script_content}"
|
|
370
|
+
)
|
|
371
|
+
logger.info(f"Running Siril ({len(input_files)} input files)")
|
|
372
|
+
|
|
373
|
+
# The `-s -` arguments tell Siril to run in script mode and read commands from stdin.
|
|
374
|
+
# It seems like the -d command may also be required when siril is in a flatpak
|
|
375
|
+
cmd = f"{siril_path} -d {temp_dir} -s -"
|
|
376
|
+
|
|
377
|
+
tool_run(cmd, temp_dir, script_content, timeout=self.timeout)
|
|
286
378
|
|
|
287
379
|
|
|
288
380
|
class GraxpertTool(Tool):
|
|
@@ -292,7 +384,12 @@ class GraxpertTool(Tool):
|
|
|
292
384
|
super().__init__("graxpert")
|
|
293
385
|
|
|
294
386
|
def run(self, cwd: str, commands: str, context: dict = {}) -> None:
|
|
295
|
-
|
|
387
|
+
"""Executes Graxpert with the specified command line arguments"""
|
|
388
|
+
|
|
389
|
+
# Arguments look similar to: graxpert -cmd background-extraction -output /tmp/testout tests/test_images/real_crummy.fits
|
|
390
|
+
cmd = f"graxpert {commands}"
|
|
391
|
+
|
|
392
|
+
tool_run(cmd, cwd, timeout=self.timeout)
|
|
296
393
|
|
|
297
394
|
|
|
298
395
|
class PythonTool(Tool):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: starbash
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: A tool for automating/standardizing/sharing astrophotography workflows.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Kevin Hester
|
|
@@ -33,6 +33,7 @@ A tool for automating/standardizing/sharing astrophotography workflows.
|
|
|
33
33
|
* Automatic - with sensible defaults, that you can change as needed.
|
|
34
34
|
* Easy - provides a 'seestar like' starting-point for autoprocessing all your sessions (by default).
|
|
35
35
|
* Fast - even with large image repositories. Automatic master bias and flat generation and reasonable defaults
|
|
36
|
+
* Multi-session - by default. So your workflows can stack from multiple nights (and still use the correct flats etc...)
|
|
36
37
|
* Sharable - you can share/use recipes for image preprocessing flows.
|
|
37
38
|
|
|
38
39
|
(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.)
|
|
@@ -89,7 +90,7 @@ FIXME - add getting started instructions (possibly with a screenshare video)
|
|
|
89
90
|
|
|
90
91
|
### Repository Management
|
|
91
92
|
- `sb repo list [--verbose]` - List installed repos (use `-v` for details)
|
|
92
|
-
- `sb repo add [--master]
|
|
93
|
+
- `sb repo add [--master] [filepath|URL]` - Add a repository, optionally specifying the type
|
|
93
94
|
- `sb repo remove <REPOURL>` - Remove the indicated repo from the repo list
|
|
94
95
|
- `sb repo reindex [--force] <REPOURL>` - Reindex the specified repo (or all repos if none specified)
|
|
95
96
|
|
|
@@ -113,6 +114,7 @@ FIXME - add getting started instructions (possibly with a screenshare video)
|
|
|
113
114
|
- `sb info target` - List targets (filtered based on the current selection)
|
|
114
115
|
- `sb info telescope` - List instruments (filtered based on the current selection)
|
|
115
116
|
- `sb info filter` - List all filters found in current selection
|
|
117
|
+
- `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)
|
|
116
118
|
|
|
117
119
|
## Not yet supported commands
|
|
118
120
|
|
|
@@ -142,4 +144,4 @@ Project members can access crash reports [here](https://geeksville.sentry.io/ins
|
|
|
142
144
|
## License
|
|
143
145
|
|
|
144
146
|
Copyright 2025 Kevin Hester, kevinh@geeksville.com.
|
|
145
|
-
Licensed under the
|
|
147
|
+
Licensed under the [GPL v3](LICENSE)
|