starbash 0.1.8__py3-none-any.whl → 0.1.10__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.

Potentially problematic release.


This version of starbash might be problematic. Click here for more details.

@@ -3,6 +3,41 @@
3
3
  [repo]
4
4
  kind = "preferences"
5
5
 
6
+ [aliases]
7
+ # aliases can be used to map non standard (or non english) frame names to standard terms
8
+ # This is also used to map filters based on common misspellings or variations.
9
+ # We assume the first listed option in the list is the 'canonical' name used for printing etc...
10
+
11
+ # frame types
12
+ dark = ["dark", "darks"]
13
+ flat = ["flat", "flats"]
14
+ bias = ["bias", "biases"]
15
+ light = ["light", "lights"]
16
+
17
+ # file suffixes
18
+ fits = ["fits", "fit"]
19
+
20
+ # filter names
21
+ SiiOiii = ["SiiOiii", "S2O3"]
22
+ HaOiii = ["HaOiii", "HaO3"]
23
+
24
+ None = ["None"]
25
+
26
+ # Passes SII 672.4nm and H-Beta 486.1nm lines
27
+ # Capturing of the two main emission wavebands in the deep red and blue at the same time
28
+ #
29
+ # The ALP-T dual band 3.5nm SII&Hb filter is a dual narrowband filter, which lets the deep
30
+ # red Sulfur-II 672.4nm and the blue Hydrogen-Beta 486.1nm lines through and is primarily
31
+ # engineered for color cameras to assist astrophotographers taking deep sky images with
32
+ # superior SNR(Signal to Noise Ratio). With an FWHM halfbandwidth designed at 3.5nm and
33
+ # achieving an optical density (OD) of 4.5 on unwanted wavelengths, it works strongly in
34
+ # blocking light pollution, moonlight, and airglow, leding to enhanced contrast in nebulae
35
+ # images by effectively passing the SII and H-beta emission lines signal only.
36
+ #
37
+ # http://www.antliafilter.com/pd.jsp?fromColId=2&id=160#_pp=2_671
38
+ SiiHb = ["SiiHb", "S2Hb"]
39
+
40
+
6
41
  # FIXME, somewhere here list default patterns which can be used to identify NINA, ASIAIR, SEESTAR
7
42
  # raw repo layouts
8
43
 
starbash/main.py CHANGED
@@ -6,7 +6,7 @@ import starbash.url as url
6
6
  import starbash
7
7
 
8
8
  from .app import Starbash, get_user_config_path, setup_logging
9
- from .commands import info, repo, select, user
9
+ from .commands import info, process, repo, select, user
10
10
  from . import console
11
11
 
12
12
  app = typer.Typer(
@@ -17,6 +17,9 @@ app.add_typer(user.app, name="user", help="Manage user settings.")
17
17
  app.add_typer(repo.app, name="repo", help="Manage Starbash repositories.")
18
18
  app.add_typer(select.app, name="select", help="Manage session and target selection.")
19
19
  app.add_typer(info.app, name="info", help="Display system and data information.")
20
+ app.add_typer(
21
+ process.app, name="process", help="Process images using automated workflows."
22
+ )
20
23
 
21
24
 
22
25
  @app.callback(invoke_without_command=True)
starbash/paths.py CHANGED
@@ -7,19 +7,24 @@ app_author = "geeksville"
7
7
  dirs = PlatformDirs(app_name, app_author)
8
8
  config_dir = Path(dirs.user_config_dir)
9
9
  data_dir = Path(dirs.user_data_dir)
10
+ documents_dir = Path(dirs.user_documents_dir) / "starbash"
10
11
 
11
12
  # These can be overridden for testing
12
13
  _override_config_dir: Path | None = None
13
14
  _override_data_dir: Path | None = None
15
+ _override_documents_dir: Path | None = None
14
16
 
15
17
 
16
18
  def set_test_directories(
17
- config_dir_override: Path | None = None, data_dir_override: Path | None = None
19
+ config_dir_override: Path | None = None,
20
+ data_dir_override: Path | None = None,
21
+ documents_dir_override: Path | None = None,
18
22
  ) -> None:
19
23
  """Set override directories for testing. Used by test fixtures to isolate test data."""
20
- global _override_config_dir, _override_data_dir
24
+ global _override_config_dir, _override_data_dir, _override_documents_dir
21
25
  _override_config_dir = config_dir_override
22
26
  _override_data_dir = data_dir_override
27
+ _override_documents_dir = documents_dir_override
23
28
 
24
29
 
25
30
  def get_user_config_dir() -> Path:
@@ -36,3 +41,14 @@ def get_user_data_dir() -> Path:
36
41
  dir_to_use = _override_data_dir if _override_data_dir is not None else data_dir
37
42
  os.makedirs(dir_to_use, exist_ok=True)
38
43
  return dir_to_use
44
+
45
+
46
+ def get_user_documents_dir() -> Path:
47
+ """Get the user documents directory. Returns test override if set, otherwise the real user directory."""
48
+ dir_to_use = (
49
+ _override_documents_dir
50
+ if _override_documents_dir is not None
51
+ else documents_dir
52
+ )
53
+ os.makedirs(dir_to_use, exist_ok=True)
54
+ return dir_to_use
@@ -10,31 +10,37 @@ author.email = "FIXMESiril?"
10
10
  [[stage]]
11
11
 
12
12
  description = "Generate master bias"
13
- disabled = true # FIXME, debugging later stuff
13
+ # disabled = false # turn on to skip
14
14
 
15
15
  # Restrict processing of this stage to only if detected hardware was found for this session
16
16
  # For any camera
17
- auto.for-camera = []
17
+ # auto.for-camera = []
18
18
 
19
- tool = "siril"
19
+ tool.name = "siril"
20
+ # tool.timeout = 15.0 # allow up to 15 seconds before we timeout and kill tool
20
21
 
21
22
  # or auto?
22
23
  # find the most recent raw fits for the current instrument (as of the time of session start)
23
24
  # input.source = "most-recent" # only look for the most recent set of raws for this particular type
24
25
  input.type = "bias" # look in all raw repos, but look only for bias files
25
26
 
26
- # for early development we have support for simple absolute file paths with globs
27
- input.source = "path"
28
- input.path = "/workspaces/starbash/images/from_astroboy/masters-raw/2025-09-09/BIAS/*.fit*"
29
- input.required = true # Is at least one input file required? If true, we will skip running this stage entirely (with a warning)
27
+ # Look for files in input repos, finding them by using the "relative" tag they contain
28
+ input.source = "repo"
29
+ input.required = 2 # siril needs at least 2 frames to stack
30
+ # old school paths also work (but are not recommended)
31
+ # input.path = ".../from_astroboy/masters-raw/2025-09-09/BIAS/*.fit*"
30
32
 
31
- # make the following also work
32
- #
33
- #os.makedirs(os.path.dirname(output), exist_ok=True)
34
- #os.makedirs(os.path.dirname(process_dir), exist_ok=True)
35
- #frames = glob(f"{masters_raw}/{date}/BIAS/{date}_*.fit*")
36
- #siril_run_in_temp_dir(frames, ...
37
- when = "session-config" # run at the start of each session process
33
+ when = "setup.master.bias" # run when master biases are regenerated
34
+
35
+ # Based on the following definitions in the stage toml file...
36
+ output.dest = "repo" # write to a particular repo
37
+ output.type = "master" # write output to the special masters repo
38
+
39
+ # the following fields will be auto populated in the context before entry:
40
+ # context.output.base_path - the full filepath to write the output file to **excluding the suffix**
41
+ # context.output.full_path - the full filepath to write the output file to (including suffix)
42
+ # (NOT implemented / needed) context.output.root_path - points to the base of the destination repo
43
+ # (NOT implemented / needed) context.output.suffix - the suffix to append to the output file (e.g. .fits or .fit.gz)
38
44
 
39
45
  # The following constants are auto defined before running the tool
40
46
  # context.process_dir (points to the session specific semi-persistent local dir for that sessions written/read data files)
@@ -42,14 +48,21 @@ when = "session-config" # run at the start of each session process
42
48
  # context.temp_dir (points to a temporary directory this tool can use for writing)
43
49
 
44
50
  # Everything in the constants dict will be predefined as named variables for use by the script
45
- context.date = "2025-09-09" # FIXME - later find auto latest date with bias frames
46
- context.output = "{masters}/biases/{date}_stacked.fits" # if the output already exists processing will be skipped
51
+ # context.date = "2025-09-0.9" # FIXME - later find auto latest date with bias frames
52
+
53
+ # output file will be place in the masters repo
54
+ # if the output already exists processing will be skipped
55
+ #
56
+ # with a path like "{instrument}/{date}/{imagetyp}/{sessionconfig}.fits"
57
+ # this path comes from master_repo.relative
58
+ # context.output = "{output.root}/{instrument}/{date}/{imagetyp}/{sessionconfig}.fits"
59
+
47
60
 
48
61
  script = '''
49
62
  # Convert Bias Frames to .fit files
50
- link bias -out={process_dir}
63
+ link frames -out={process_dir}
51
64
  cd {process_dir}
52
65
 
53
- # Stack Bias Frames to bias_stacked.fit
54
- stack bias rej 3 3 -nonorm -out={output}
66
+ # Stack frames
67
+ stack frames rej 3 3 -nonorm -out={output["base_path"]}
55
68
  '''
@@ -0,0 +1,36 @@
1
+
2
+ [repo]
3
+ kind = "recipe"
4
+
5
+
6
+ [recipe]
7
+ author.name = "FIXMESiril?"
8
+ author.email = "FIXMESiril?"
9
+
10
+ [[stage]]
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
+ when = "setup.master.dark" # run when master darks are regenerated
24
+
25
+ # Based on the following definitions in the stage toml file...
26
+ output.dest = "repo" # write to a particular repo
27
+ output.type = "master" # write output to the special masters repo
28
+
29
+ script = '''
30
+ # Convert bias/dark Frames to .fit files
31
+ link frames -out={process_dir}
32
+ cd {process_dir}
33
+
34
+ # Stack frames
35
+ stack frames rej 3 3 -nonorm -out={output["base_path"]}
36
+ '''
@@ -10,26 +10,36 @@ author.email = "FIXMESiril?"
10
10
 
11
11
  [[stage]]
12
12
 
13
+ # See master_bias/starbash.toml for more documentation
14
+
13
15
  description = "Generate master flat"
14
- disabled = true # FIXME, debugging later stuff
16
+ # disabled = false
15
17
 
16
- # For any camera
17
- auto.for-camera = []
18
+ tool.name = "siril"
19
+ # tool.timeout = 15.0 # allow up to 15 seconds before we timeout and kill tool
18
20
 
19
- tool = "siril"
20
- # input.source = "session" # or auto? prefer ones in session otherwise find by in masters
21
- input.type = "flat" # look in _session_ directories, but look only for flat files
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
- # FIXME for early development we have support for simple absolute file paths with globs
24
- input.source = "path"
25
- input.path = "/workspaces/starbash/images/from_astroboy/M 27/2025-09-16/FLAT/*.fit*"
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
29
+
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"]
27
33
 
28
- when = "session-config" # run once per session-config
29
- context.output = "{process_dir}/flat_s{sessionid}_c{sessionconfig}.fits"
34
+ when = "setup.master.flat" # run when master biases are regenerated
30
35
 
31
- # FIXME, bias should have been added to context by two previous stages. But for now hardwire
32
- context.bias = '/workspaces/starbash/images/masters/biases/2025-09-09_stacked.fits'
36
+ # Based on the following definitions in the stage toml file...
37
+ output.dest = "repo" # write to a particular repo
38
+ output.type = "master" # write output to the special masters repo
39
+
40
+ # FIXME for early development we have support for simple absolute file paths with globs
41
+ #input.source = "path"
42
+ #input.path = "/workspaces/starbash/images/from_astroboy/M 27/2025-09-16/FLAT/*.fit*"
33
43
 
34
44
  script = '''
35
45
  # Create a sequence from the raw flat frames
@@ -37,10 +47,10 @@ script = '''
37
47
  cd {process_dir}
38
48
 
39
49
  # Calibrate the flat frames using master bias
40
- calibrate flat -bias={bias}
50
+ calibrate flat -bias={master["bias"]}
41
51
 
42
- # Stack the pre-processed (calibrated) flat frames (writes to flat_stacked.fit)
43
- stack pp_flat rej 3 3 -norm=mul -out=flat_stacked
52
+ # Stack the pre-processed (calibrated) flat frames
53
+ stack pp_flat rej 3 3 -norm=mul -out={output["base_path"]}
44
54
  '''
45
55
 
46
56
  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
@@ -30,8 +30,9 @@ auto.for-filter = ["SiiOiii"]
30
30
  # auto.for-filter = ["HaOiii"]
31
31
  auto.for-camera = ["OSC"]
32
32
 
33
- tool = "siril"
34
- when = "session-light" # run once per session-config
33
+ tool.name = "siril"
34
+
35
+ when = "session.light" # run once per session.config
35
36
  output = "FIXME"
36
37
 
37
38
  # FIXME, bias and flat should have been added to context by two previous stages. But for now hardwire
@@ -74,8 +75,11 @@ description = "Stack OSC dual duo filter data, with separate Ha, Oiii and Sii ch
74
75
  context.target = "M 27" # FIXME
75
76
  context.targets = "/workspaces/starbash/images/processed" # FIXME, do something smarter
76
77
 
77
- tool = "python"
78
- when = "session-stack" # run once after all session/session-config processing was done
78
+ tool.name = "python"
79
+
80
+ when = "session.stack" # run once after all session/session.config processing was done
81
+
82
+ input.masters = ["bias", "flat"]
79
83
 
80
84
  # if not specified starbash.py used
81
85
  # 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-light" # run once per session-config
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-stack" # run once after all session/session-config processing was done
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
 
@@ -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:
@@ -21,14 +23,37 @@ dir = "osc_single_duo"
21
23
 
22
24
  # processing stages, currently all declared here, but possibly in the future they could be added by user/other toml files
23
25
 
26
+ # Not included in standard list - for now we run manually
27
+ #[[stages]]
28
+ #name = "setup-masters" # for flat processing, master generation etc
29
+ #priority = 5
30
+
31
+ #
32
+ # master specific stages
33
+ #
34
+ [[master-stages]]
35
+ name = "setup.master.bias" # generate master bias frames
36
+ priority = 10
37
+
38
+ [[master-stages]]
39
+ name = "setup.master.dark" # generate master bias frames
40
+ priority = 10
41
+
42
+ [[master-stages]]
43
+ name = "setup.master.flat" # generate master flat frames
44
+ priority = 20
45
+
46
+ #
47
+ # session specific processing stages
48
+ #
24
49
  [[stages]]
25
- name = "session-config" # for flat processing, master generation etc
50
+ name = "session.config" # for flat processing, master generation etc
26
51
  priority = 10
27
52
 
28
53
  [[stages]]
29
- name = "session-light" # generate light frames from lights and with reference to flats/bias
54
+ name = "session.light" # generate light frames from lights and with reference to flats/bias
30
55
  priority = 20
31
56
 
32
57
  [[stages]]
33
- name = "session-stack" # stack frames
58
+ name = "session.stack" # stack frames
34
59
  priority = 30
starbash/selection.py CHANGED
@@ -2,11 +2,58 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
5
  import logging
7
- from pathlib import Path
8
- from typing import Any, Optional
9
- from datetime import datetime
6
+ from typing import Any, Optional, TYPE_CHECKING
7
+ from repo import Repo
8
+ from starbash.aliases import normalize_target_name
9
+
10
+
11
+ def where_tuple(conditions: dict[str, Any] | None) -> tuple[str, list[Any]]:
12
+ """Search for sessions matching the given conditions.
13
+
14
+ Args:
15
+ conditions: Dictionary of session key-value pairs to match, or None for all.
16
+ Special keys:
17
+ - 'date_start': Filter sessions starting on or after this date
18
+ - 'date_end': Filter sessions starting on or before this date
19
+
20
+ Returns:
21
+ Tuple of (WHERE clause string, list of parameters)
22
+ """
23
+ if conditions is None:
24
+ conditions = {}
25
+
26
+ # Build WHERE clause dynamically based on conditions
27
+ where_clauses = []
28
+ params = []
29
+
30
+ # Extract date range conditions
31
+ date_start = conditions.get("date_start")
32
+ date_end = conditions.get("date_end")
33
+
34
+ # Add date range filters to WHERE clause
35
+ if date_start:
36
+ where_clauses.append("start >= ?")
37
+ params.append(date_start)
38
+
39
+ if date_end:
40
+ where_clauses.append("start <= ?")
41
+ params.append(date_end)
42
+
43
+ # Add standard conditions to WHERE clause
44
+ for key, value in conditions.items():
45
+ if key not in ("date_start", "date_end") and value is not None:
46
+ column_name = key
47
+ where_clauses.append(f"{column_name} = ?")
48
+ params.append(value)
49
+
50
+ # Build the query
51
+ query = ""
52
+
53
+ if where_clauses:
54
+ query += " WHERE " + " AND ".join(where_clauses)
55
+
56
+ return (query, params)
10
57
 
11
58
 
12
59
  class Selection:
@@ -19,16 +66,17 @@ class Selection:
19
66
  - Image types
20
67
  - Telescope names
21
68
 
22
- The selection state is saved to disk and can be used to build database queries.
69
+ The selection state is saved to the user config repo TOML file and can be
70
+ used to build database queries.
23
71
  """
24
72
 
25
- def __init__(self, state_file: Path):
26
- """Initialize the Selection with a state file path.
73
+ def __init__(self, user_repo: "Repo"):
74
+ """Initialize the Selection with the user config repository.
27
75
 
28
76
  Args:
29
- state_file: Path to the JSON file where selection state is persisted
77
+ user_repo: The Repo object for user preferences where selection state is persisted
30
78
  """
31
- self.state_file = state_file
79
+ self.user_repo = user_repo
32
80
  self.targets: list[str] = []
33
81
  self.date_start: Optional[str] = None
34
82
  self.date_end: Optional[str] = None
@@ -40,39 +88,59 @@ class Selection:
40
88
  self._load()
41
89
 
42
90
  def _load(self) -> None:
43
- """Load selection state from disk."""
44
- if self.state_file.exists():
45
- try:
46
- with open(self.state_file, "r") as f:
47
- data = json.load(f)
48
- self.targets = data.get("targets", [])
49
- self.date_start = data.get("date_start")
50
- self.date_end = data.get("date_end")
51
- self.filters = data.get("filters", [])
52
- self.image_types = data.get("image_types", [])
53
- self.telescopes = data.get("telescopes", [])
54
- logging.debug(f"Loaded selection state from {self.state_file}")
55
- except Exception as e:
56
- logging.warning(f"Failed to load selection state: {e}")
91
+ """Load selection state from user config repo."""
92
+ try:
93
+ # Load with type-safe defaults
94
+ targets = self.user_repo.get("selection.targets", [])
95
+ self.targets = targets if isinstance(targets, list) else []
96
+
97
+ self.date_start = self.user_repo.get("selection.date_start")
98
+ self.date_end = self.user_repo.get("selection.date_end")
99
+
100
+ filters = self.user_repo.get("selection.filters", [])
101
+ self.filters = filters if isinstance(filters, list) else []
102
+
103
+ image_types = self.user_repo.get("selection.image_types", [])
104
+ self.image_types = image_types if isinstance(image_types, list) else []
105
+
106
+ telescopes = self.user_repo.get("selection.telescopes", [])
107
+ self.telescopes = telescopes if isinstance(telescopes, list) else []
108
+
109
+ logging.debug(f"Loaded selection state from {self.user_repo.url}")
110
+ except Exception as e:
111
+ logging.warning(f"Failed to load selection state: {e}")
57
112
 
58
113
  def _save(self) -> None:
59
- """Save selection state to disk."""
114
+ """Save selection state to user config repo."""
60
115
  try:
61
- # Ensure parent directory exists
62
- self.state_file.parent.mkdir(parents=True, exist_ok=True)
63
-
64
- data = {
65
- "targets": self.targets,
66
- "date_start": self.date_start,
67
- "date_end": self.date_end,
68
- "filters": self.filters,
69
- "image_types": self.image_types,
70
- "telescopes": self.telescopes,
71
- }
72
-
73
- with open(self.state_file, "w") as f:
74
- json.dump(data, f, indent=2)
75
- logging.debug(f"Saved selection state to {self.state_file}")
116
+ self.user_repo.set("selection.targets", self.targets)
117
+
118
+ # Handle date fields - set if not None, delete if None (to clear them)
119
+ if self.date_start is not None:
120
+ self.user_repo.set("selection.date_start", self.date_start)
121
+ else:
122
+ # Delete the key if it exists
123
+ if "selection" in self.user_repo.config:
124
+ sel_section = self.user_repo.config["selection"]
125
+ if isinstance(sel_section, dict) and "date_start" in sel_section:
126
+ del sel_section["date_start"] # type: ignore
127
+
128
+ if self.date_end is not None:
129
+ self.user_repo.set("selection.date_end", self.date_end)
130
+ else:
131
+ # Delete the key if it exists
132
+ if "selection" in self.user_repo.config:
133
+ sel_section = self.user_repo.config["selection"]
134
+ if isinstance(sel_section, dict) and "date_end" in sel_section:
135
+ del sel_section["date_end"] # type: ignore
136
+
137
+ self.user_repo.set("selection.filters", self.filters)
138
+ self.user_repo.set("selection.image_types", self.image_types)
139
+ self.user_repo.set("selection.telescopes", self.telescopes)
140
+
141
+ # Write the updated config to disk
142
+ self.user_repo.write_config()
143
+ logging.debug(f"Saved selection state to {self.user_repo.url}")
76
144
  except Exception as e:
77
145
  logging.error(f"Failed to save selection state: {e}")
78
146
 
@@ -174,14 +242,11 @@ class Selection:
174
242
  and not self.telescopes
175
243
  )
176
244
 
177
- def get_query_conditions(self) -> dict[str, Any]:
245
+ def get_query_conditions(self) -> tuple[str, list[Any]]:
178
246
  """Build query conditions based on the current selection.
179
247
 
180
248
  Returns:
181
- Dictionary of query conditions that can be used with Database methods.
182
- Special keys:
183
- - 'date_start': ISO date string for start of range
184
- - 'date_end': ISO date string for end of range
249
+ A tuple of SQL (WHERE clause string, list of parameters)
185
250
  """
186
251
  conditions = {}
187
252
 
@@ -192,7 +257,11 @@ class Selection:
192
257
  if self.targets:
193
258
  # For now, just use the first target
194
259
  # TODO: Support multiple targets in queries
195
- conditions["OBJECT"] = self.targets[0] if len(self.targets) == 1 else None
260
+ conditions["OBJECT"] = (
261
+ normalize_target_name(self.targets[0])
262
+ if len(self.targets) == 1
263
+ else None
264
+ )
196
265
 
197
266
  if self.filters:
198
267
  # For now, just use the first filter
@@ -212,7 +281,7 @@ class Selection:
212
281
  if self.date_end:
213
282
  conditions["date_end"] = self.date_end
214
283
 
215
- return conditions
284
+ return where_tuple(conditions)
216
285
 
217
286
  def summary(self) -> dict[str, Any]:
218
287
  """Get a summary of the current selection state.
@@ -0,0 +1,13 @@
1
+ # This is a master repository for (Starbash)[{PROJECT_URL}].
2
+ #
3
+ # This file marks the root directory of a set of auto matinained astrophotography
4
+ # 'master' files, such as master darks, flats or biases.
5
+ #
6
+ # You generally don't need to edit this file directly - it was auto generated when you ran
7
+ # "sb repo add --master {REPO_PATH}".
8
+ #
9
+
10
+ [repo]
11
+ kind = "master"
12
+
13
+ relative = "{DEFAULT_RELATIVE}"
@@ -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"
@@ -1,4 +1,4 @@
1
- # This is your Starbash user configuration file. It can be used to provide default settings
1
+ # This is your (Starbash)[{PROJECT_URL}] user configuration file. It can be used to provide default settings
2
2
  # to workflows.
3
3
 
4
4
  # (or wherever is appropriate for the user's platform).