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.

@@ -20,20 +20,3 @@ def format_duration(seconds: int | float) -> str:
20
20
  hours = int(seconds // 3600)
21
21
  minutes = int((seconds % 3600) // 60)
22
22
  return f"{hours}h {minutes}m" if minutes else f"{hours}h"
23
-
24
-
25
- def to_shortdate(date_iso: str) -> str:
26
- """Convert ISO UTC datetime string to local short date string (YYYY-MM-DD).
27
-
28
- Args:
29
- date_iso: ISO format datetime string (e.g., "2023-10-15T14:30:00Z")
30
-
31
- Returns:
32
- Short date string in YYYY-MM-DD format, or the original string if conversion fails
33
- """
34
- try:
35
- dt_utc = datetime.fromisoformat(date_iso)
36
- dt_local = dt_utc.astimezone()
37
- return dt_local.strftime("%Y-%m-%d")
38
- except (ValueError, TypeError):
39
- return date_iso
starbash/commands/info.py CHANGED
@@ -27,7 +27,7 @@ def dump_column(sb: Starbash, human_name: str, column_name: str) -> None:
27
27
  sessions = sb.search_session()
28
28
 
29
29
  # Also do a complete unfiltered search so we can compare for the users
30
- allsessions = sb.db.search_session()
30
+ allsessions = sb.db.search_session(("", []))
31
31
 
32
32
  column_name = get_column_name(column_name)
33
33
  found = [session[column_name] for session in sessions if session[column_name]]
@@ -38,7 +38,7 @@ def dump_column(sb: Starbash, human_name: str, column_name: str) -> None:
38
38
  all_counts = Counter(allfound)
39
39
 
40
40
  # Sort by telescope name
41
- sorted_telescopes = sorted(found_counts.items())
41
+ sorted_list = sorted(found_counts.items())
42
42
 
43
43
  # Create and display table
44
44
  table = Table(
@@ -49,7 +49,7 @@ def dump_column(sb: Starbash, human_name: str, column_name: str) -> None:
49
49
  "# of sessions", style=TABLE_COLUMN_STYLE, no_wrap=True, justify="right"
50
50
  )
51
51
 
52
- for i, count in sorted_telescopes:
52
+ for i, count in sorted_list:
53
53
  table.add_row(i, str(count))
54
54
 
55
55
  console.print(table)
@@ -76,6 +76,75 @@ def filter():
76
76
  dump_column(sb, "Filter", Database.FILTER_KEY)
77
77
 
78
78
 
79
+ kind_arg = typer.Argument(
80
+ help="Optional image type to filter by (e.g., BIAS, DARK, FLAT, LIGHT)",
81
+ )
82
+
83
+
84
+ @app.command()
85
+ def master(
86
+ kind: Annotated[
87
+ str | None,
88
+ kind_arg,
89
+ ] = None,
90
+ ):
91
+ """List all precalculated master images (darks, biases, flats)."""
92
+ with Starbash("info.master") as sb:
93
+ # Get the master repo
94
+ images = sb.get_master_images(kind)
95
+
96
+ if not images:
97
+ kind_msg = f" of type '{kind}'" if kind else ""
98
+ console.print(f"[yellow]No master images{kind_msg} found.[/yellow]")
99
+ return
100
+
101
+ # Create table to display results
102
+ title = f"Master Images ({len(images)} total)"
103
+ if kind:
104
+ title = f"Master {kind} Images ({len(images)} total)"
105
+ table = Table(title=title)
106
+ table.add_column("Date", style=TABLE_COLUMN_STYLE, no_wrap=True)
107
+ table.add_column("Type", style=TABLE_COLUMN_STYLE, no_wrap=True)
108
+ table.add_column("Filename", style=TABLE_VALUE_STYLE, no_wrap=False)
109
+
110
+ # Sort by date, then by type
111
+ sorted_images = sorted(
112
+ images,
113
+ key=lambda img: (
114
+ img.get(Database.DATE_OBS_KEY) or img.get(Database.DATE_KEY) or "",
115
+ img.get(Database.IMAGETYP_KEY) or "",
116
+ ),
117
+ )
118
+
119
+ for image in sorted_images:
120
+ date = (
121
+ image.get(Database.DATE_OBS_KEY)
122
+ or image.get(Database.DATE_KEY)
123
+ or "Unknown"
124
+ )
125
+ # Extract just the date part (YYYY-MM-DD) if it's a full ISO timestamp
126
+ if "T" in date:
127
+ date = date.split("T")[0]
128
+
129
+ kind = image.get(Database.IMAGETYP_KEY) or "Unknown"
130
+ filename = image.get("path") or "Unknown"
131
+
132
+ table.add_row(date, kind, filename)
133
+
134
+ console.print(table)
135
+
136
+
137
+ @app.command(hidden=True)
138
+ def masters(
139
+ kind: Annotated[
140
+ str | None,
141
+ kind_arg,
142
+ ] = None,
143
+ ):
144
+ """Alias for 'info master' command."""
145
+ master(kind)
146
+
147
+
79
148
  @app.callback(invoke_without_command=True)
80
149
  def main_callback(ctx: typer.Context):
81
150
  """Show user preferences location and other app info.
@@ -0,0 +1,154 @@
1
+ """Processing commands for automated image processing workflows."""
2
+
3
+ import typer
4
+ from pathlib import Path
5
+ from typing_extensions import Annotated
6
+
7
+ from starbash.app import Starbash, copy_images_to_dir
8
+ from starbash import console
9
+ from starbash.commands.select import selection_by_number
10
+ from starbash.database import SessionRow
11
+
12
+ app = typer.Typer()
13
+
14
+
15
+ @app.command()
16
+ def siril(
17
+ session_num: Annotated[
18
+ int,
19
+ typer.Argument(help="Session number to process (from 'select list' output)"),
20
+ ],
21
+ destdir: Annotated[
22
+ str,
23
+ typer.Argument(
24
+ help="Destination directory for Siril directory tree and processing"
25
+ ),
26
+ ],
27
+ run: Annotated[
28
+ bool,
29
+ typer.Option(
30
+ "--run",
31
+ help="Automatically launch Siril GUI after generating directory tree",
32
+ ),
33
+ ] = False,
34
+ ):
35
+ """Generate Siril directory tree and optionally run Siril GUI.
36
+
37
+ Creates a properly structured directory tree for Siril processing with
38
+ biases/, darks/, flats/, and lights/ subdirectories populated with the
39
+ session's images (via symlinks when possible).
40
+
41
+ If --run is specified, launches the Siril GUI with the generated directory
42
+ structure loaded and ready for processing.
43
+ """
44
+ with Starbash("process.siril") as sb:
45
+ console.print(
46
+ f"[yellow]Processing session {session_num} for Siril in {destdir}...[/yellow]"
47
+ )
48
+
49
+ # Determine output directory
50
+ output_dir = Path(destdir)
51
+
52
+ # Get the selected session (convert from 1-based to 0-based index)
53
+ session = selection_by_number(sb, session_num)
54
+
55
+ # Get images for this session
56
+
57
+ def session_to_dir(src_session: SessionRow, subdir_name: str):
58
+ """Copy the images from the specified session to the subdir"""
59
+ img_dir = output_dir / subdir_name
60
+ img_dir.mkdir(parents=True, exist_ok=True)
61
+ images = sb.get_session_images(src_session)
62
+ copy_images_to_dir(images, img_dir)
63
+
64
+ # FIXME - pull this dirname from preferences
65
+ lights = "lights"
66
+ session_to_dir(session, lights)
67
+
68
+ extras = [
69
+ # FIXME search for BIAS/DARK/FLAT etc... using multiple canonical names
70
+ ("BIAS", "biases"),
71
+ ("DARK", "darks"),
72
+ ("FLAT", "flats"),
73
+ ]
74
+ for typ, subdir in extras:
75
+ candidates = sb.guess_sessions(session, typ)
76
+ if not candidates:
77
+ console.print(
78
+ f"[yellow]No candidate sessions found for {typ} calibration frames.[/yellow]"
79
+ )
80
+ else:
81
+ session_to_dir(candidates[0], subdir)
82
+
83
+ # FIXME put an starbash.toml repo file in output_dir (with info about what we picked/why)
84
+ # to allow users to override/reprocess with the same settings.
85
+ # Also FIXME, check for the existence of such a file
86
+
87
+
88
+ @app.command()
89
+ def auto(
90
+ session_num: Annotated[
91
+ int | None,
92
+ typer.Argument(
93
+ help="Session number to process. If not specified, processes all selected sessions."
94
+ ),
95
+ ] = None,
96
+ ):
97
+ """Automatic processing with sensible defaults.
98
+
99
+ If session number is specified, processes only that session.
100
+ Otherwise, all currently selected sessions will be processed automatically
101
+ using the configured recipes and default settings.
102
+
103
+ This command handles:
104
+ - Automatic master frame selection (bias, dark, flat)
105
+ - Calibration of light frames
106
+ - Registration and stacking
107
+ - Basic post-processing
108
+
109
+ The output will be saved according to the configured recipes.
110
+ """
111
+ with Starbash("process.auto") as sb:
112
+ if session_num is not None:
113
+ console.print(f"[yellow]Auto-processing session {session_num}...[/yellow]")
114
+ else:
115
+ console.print("[yellow]Auto-processing all selected sessions...[/yellow]")
116
+
117
+ console.print(
118
+ "[red]Still in development - see https://github.com/geeksville/starbash[/red]"
119
+ )
120
+ sb.run_all_stages()
121
+
122
+
123
+ @app.command()
124
+ def masters():
125
+ """Generate master flats, darks, and biases from selected raw frames.
126
+
127
+ Analyzes the current selection to find all available calibration frames
128
+ (BIAS, DARK, FLAT) and automatically generates master calibration frames
129
+ using stacking recipes.
130
+
131
+ Generated master frames are stored in the configured masters directory
132
+ and will be automatically used for future processing operations.
133
+ """
134
+ with Starbash("process.masters") as sb:
135
+ console.print(
136
+ "[yellow]Generating master frames from current selection...[/yellow]"
137
+ )
138
+ console.print(
139
+ "[red]Still in development - see https://github.com/geeksville/starbash[/red]"
140
+ )
141
+ sb.run_master_stages()
142
+
143
+
144
+ @app.callback(invoke_without_command=True)
145
+ def main_callback(ctx: typer.Context):
146
+ """Process images using automated workflows.
147
+
148
+ These commands handle calibration, registration, stacking, and
149
+ post-processing of astrophotography sessions.
150
+ """
151
+ if ctx.invoked_subcommand is None:
152
+ # No command provided, show help
153
+ console.print(ctx.get_help())
154
+ raise typer.Exit()
starbash/commands/repo.py CHANGED
@@ -1,99 +1,224 @@
1
1
  import typer
2
2
  from typing_extensions import Annotated
3
+ from pathlib import Path
4
+ import logging
3
5
 
6
+ import starbash
7
+ from repo import repo_suffix, Repo
4
8
  from starbash.app import Starbash
5
- from starbash import console
9
+ from starbash import console, log_filter_level
10
+ from starbash.paths import get_user_documents_dir
11
+ from starbash.toml import toml_from_template
6
12
 
7
13
  app = typer.Typer(invoke_without_command=True)
8
14
 
9
15
 
10
- @app.callback()
11
- def main(
12
- ctx: typer.Context,
16
+ def repo_enumeration(sb: Starbash):
17
+ """return a dict of int (1 based) to Repo instances"""
18
+ verbose = False # assume not verbose for enum picking
19
+ repos = sb.repo_manager.repos if verbose else sb.repo_manager.regular_repos
20
+
21
+ return {i + 1: repo for i, repo in enumerate(repos)}
22
+
23
+
24
+ def complete_repo_by_num(incomplete: str):
25
+ # We need to use stderr_logging to prevent confusing the bash completion parser
26
+ starbash.log_filter_level = (
27
+ logging.ERROR
28
+ ) # avoid showing output while doing completion
29
+ with Starbash("repo.complete.num", stderr_logging=True) as sb:
30
+ for num, repo in repo_enumeration(sb).items():
31
+ if str(num).startswith(incomplete):
32
+ yield (str(num), repo.url)
33
+
34
+
35
+ def complete_repo_by_url(incomplete: str):
36
+ # We need to use stderr_logging to prevent confusing the bash completion parser
37
+ starbash.log_filter_level = (
38
+ logging.ERROR
39
+ ) # avoid showing output while doing completion
40
+ with Starbash("repo.complete.url", stderr_logging=True) as sb:
41
+ repos = sb.repo_manager.regular_repos
42
+
43
+ for repo in repos:
44
+ if repo.url.startswith(incomplete):
45
+ yield (repo.url, f"kind={repo.kind('input')}")
46
+
47
+
48
+ @app.command()
49
+ def list(
13
50
  verbose: bool = typer.Option(
14
51
  False, "--verbose", "-v", help="Show all repos including system repos"
15
52
  ),
53
+ ):
54
+ """
55
+ lists all repositories.
56
+ Use --verbose to show all repos including system/recipe repos.
57
+ """
58
+ with Starbash("repo.list") as sb:
59
+ repos = sb.repo_manager.repos if verbose else sb.repo_manager.regular_repos
60
+ for i, repo in enumerate(repos):
61
+ kind = repo.kind("input")
62
+ # for unknown repos (probably because we haven't written a starbash.toml file to the root yet),
63
+ # we call them "input" because users will be less confused by that
64
+
65
+ if verbose:
66
+ # No numbers for verbose mode (system repos can't be removed)
67
+ console.print(f"{ repo.url } (kind={ kind })")
68
+ else:
69
+ # Show numbers for user repos (can be removed later)
70
+ console.print(f"{ i + 1:2}: { repo.url } (kind={ kind })")
71
+
72
+
73
+ @app.callback()
74
+ def main(
75
+ ctx: typer.Context,
16
76
  ):
17
77
  """
18
78
  Manage repositories.
19
79
 
20
80
  When called without a subcommand, lists all repositories.
21
- Use --verbose to show all repos including system/recipe repos.
22
81
  """
23
82
  # If no subcommand is invoked, run the list behavior
24
83
  if ctx.invoked_subcommand is None:
25
- with Starbash("repo.list") as sb:
26
- repos = sb.repo_manager.repos if verbose else sb.repo_manager.regular_repos
27
- for i, repo in enumerate(repos):
28
- kind = repo.kind("input")
29
- # for unknown repos (probably because we haven't written a starbash.toml file to the root yet),
30
- # we call them "input" because users will be less confused by that
31
-
32
- if verbose:
33
- # No numbers for verbose mode (system repos can't be removed)
34
- console.print(f"{ repo.url } (kind={ kind })")
35
- else:
36
- # Show numbers for user repos (can be removed later)
37
- console.print(f"{ i + 1:2}: { repo.url } (kind={ kind })")
84
+ # No command provided, show help
85
+ console.print(ctx.get_help())
86
+ raise typer.Exit()
38
87
 
39
88
 
40
89
  @app.command()
41
- def add(path: str):
90
+ def add(
91
+ path: Annotated[str | None, typer.Argument(help="Path to the respository")] = None,
92
+ master: bool = typer.Option(
93
+ False, "--master", help="Mark this new repository for master files."
94
+ ),
95
+ processed: bool = typer.Option(
96
+ False,
97
+ "--processed",
98
+ help="Mark this new repository for processed output files.",
99
+ ),
100
+ ):
42
101
  """
43
102
  Add a repository. path is either a local path or a remote URL.
44
103
  """
104
+ repo_type = None
105
+ if master:
106
+ repo_type = "master"
107
+ elif processed:
108
+ repo_type = "processed"
109
+
110
+ if path is None:
111
+ if repo_type is not None:
112
+ # if we know the repo type we can auto create a default path
113
+ path = str(get_user_documents_dir() / "repos" / repo_type)
114
+ else:
115
+ console.print("[red]Error: path is required for input repositories[/red]")
116
+ raise typer.Exit(1)
117
+
45
118
  with Starbash("repo.add") as sb:
46
- repo = sb.user_repo.add_repo_ref(path)
119
+ p = Path(path)
120
+
121
+ repo_toml = p / repo_suffix # the starbash.toml file at the root of the repo
122
+ if repo_toml.exists():
123
+ logging.warning("Using existing repository config file: %s", repo_toml)
124
+ else:
125
+ if repo_type:
126
+ console.print(f"Creating {repo_type} repository: {p}")
127
+ p.mkdir(parents=True, exist_ok=True)
128
+
129
+ toml_from_template(
130
+ f"repo/{repo_type}",
131
+ p / repo_suffix,
132
+ overrides={
133
+ "REPO_TYPE": repo_type,
134
+ "REPO_PATH": str(p),
135
+ "DEFAULT_RELATIVE": "{instrument}/{date}/{imagetyp}/master_{session_config}.fit",
136
+ },
137
+ )
138
+ else:
139
+ # No type specified, therefore (for now) assume we are just using this as an input
140
+ # repo (and it must exist)
141
+ if not p.exists():
142
+ console.print(f"[red]Error: Repo path does not exist: {p}[/red]")
143
+ raise typer.Exit(code=1)
144
+
145
+ console.print(f"Adding repository: {p}")
146
+
147
+ repo = sb.user_repo.add_repo_ref(p)
47
148
  if repo:
48
- console.print(f"Added repository: {path}")
49
149
  sb.reindex_repo(repo)
50
150
 
51
- # we don't yet write default config files at roots of repos, but it would be easy to add here
151
+ # we don't yet always write default config files at roots of repos, but it would be easy to add here
52
152
  # r.write_config()
53
153
  sb.user_repo.write_config()
54
- # FIXME, we also need to index the newly added repo!!!
55
154
 
56
155
 
57
- @app.command()
58
- def remove(reponum: str):
59
- """
60
- Remove a repository by number (from list).
61
- Use 'starbash repo' to see the repository numbers.
62
- """
63
- with Starbash("repo.remove") as sb:
64
- try:
65
- # Parse the repo number (1-indexed)
66
- repo_index = int(reponum) - 1
156
+ def repo_url_to_repo(sb: Starbash, repo_url: str | None) -> Repo | None:
157
+ """Helper to get a Repo instance from a URL or number"""
158
+ if repo_url is None:
159
+ return None
67
160
 
68
- # Get only the regular (user-visible) repos
69
- regular_repos = sb.repo_manager.regular_repos
70
-
71
- if repo_index < 0 or repo_index >= len(regular_repos):
72
- console.print(
73
- f"[red]Error: Repository number {reponum} is out of range. Valid range: 1-{len(regular_repos)}[/red]"
74
- )
75
- raise typer.Exit(code=1)
161
+ # try to find by URL
162
+ repo = sb.repo_manager.get_repo_by_url(repo_url)
163
+ if repo is not None:
164
+ return repo
76
165
 
77
- # Get the repo to remove
78
- repo_to_remove = regular_repos[repo_index]
79
- repo_url = repo_to_remove.url
166
+ # Fall back to finding by number
167
+ try:
168
+ # Parse the repo number (1-indexed)
169
+ repo_index = int(repo_url) - 1
80
170
 
81
- # Remove the repo reference from user config
82
- sb.remove_repo_ref(repo_url)
83
- console.print(f"[green]Removed repository: {repo_url}[/green]")
171
+ # Get only the regular (user-visible) repos
172
+ regular_repos = sb.repo_manager.regular_repos
84
173
 
85
- except ValueError:
174
+ if repo_index < 0 or repo_index >= len(regular_repos):
86
175
  console.print(
87
- f"[red]Error: '{reponum}' is not a valid repository number. Please use a number from 'repo list'.[/red]"
176
+ f"[red]Error: '{repo_url}' is not a valid repository number. Please enter a repository number or URL.[/red]"
88
177
  )
89
178
  raise typer.Exit(code=1)
90
179
 
180
+ return regular_repos[repo_index]
181
+ except ValueError:
182
+ console.print(
183
+ f"[red]Error: '{repo_url}' is not valid. Please enter a repository number or URL.[/red]"
184
+ )
185
+ raise typer.Exit(code=1)
186
+
91
187
 
92
188
  @app.command()
93
- def reindex(
189
+ def remove(
94
190
  reponum: Annotated[
191
+ str,
192
+ typer.Argument(
193
+ help="Repository number or URL", autocompletion=complete_repo_by_url
194
+ ),
195
+ ],
196
+ ):
197
+ """
198
+ Remove a repository by number (from list).
199
+ Use 'starbash repo' to see the repository numbers.
200
+ """
201
+ with Starbash("repo.remove") as sb:
202
+ # Get the repo to remove
203
+ repo_to_remove = repo_url_to_repo(sb, reponum)
204
+ if repo_to_remove is None:
205
+ console.print(f"[red]Error: You must specify a repository[/red]")
206
+ raise typer.Exit(code=1)
207
+ repo_url = repo_to_remove.url
208
+
209
+ # Remove the repo reference from user config
210
+ sb.remove_repo_ref(repo_url)
211
+ console.print(f"[green]Removed repository: {repo_url}[/green]")
212
+
213
+
214
+ @app.command()
215
+ def reindex(
216
+ repo_url: Annotated[
95
217
  str | None,
96
- typer.Argument(help="The repository number, if not specified reindex all."),
218
+ typer.Argument(
219
+ help="The repository URL, if not specified reindex all.",
220
+ autocompletion=complete_repo_by_url,
221
+ ),
97
222
  ] = None,
98
223
  force: bool = typer.Option(
99
224
  default=False, help="Reread FITS headers, even if they are already indexed."
@@ -105,35 +230,17 @@ def reindex(
105
230
  Use 'starbash repo' to see the repository numbers.
106
231
  """
107
232
  with Starbash("repo.reindex") as sb:
108
- if reponum is None:
233
+ repo_to_reindex = repo_url_to_repo(sb, repo_url)
234
+
235
+ if repo_to_reindex is None:
109
236
  sb.reindex_repos(force=force)
110
237
  else:
111
- try:
112
- # Parse the repo number (1-indexed)
113
- repo_index = int(reponum) - 1
114
-
115
- # Get only the regular (user-visible) repos
116
- regular_repos = sb.repo_manager.regular_repos
117
-
118
- if repo_index < 0 or repo_index >= len(regular_repos):
119
- console.print(
120
- f"[red]Error: Repository number {reponum} is out of range. Valid range: 1-{len(regular_repos)}[/red]"
121
- )
122
- raise typer.Exit(code=1)
123
-
124
- # Get the repo to reindex
125
- repo_to_reindex = regular_repos[repo_index]
126
- console.print(f"Reindexing repository: {repo_to_reindex.url}")
127
- sb.reindex_repo(repo_to_reindex, force=force)
128
- console.print(
129
- f"[green]Successfully reindexed repository {reponum}[/green]"
130
- )
131
-
132
- except ValueError:
133
- console.print(
134
- f"[red]Error: '{reponum}' is not a valid repository number. Please use a number from 'starbash repo'.[/red]"
135
- )
136
- raise typer.Exit(code=1)
238
+ # Get the repo to reindex
239
+ console.print(f"Reindexing repository: {repo_to_reindex.url}")
240
+ sb.reindex_repo(repo_to_reindex, force=force)
241
+ console.print(
242
+ f"[green]Successfully reindexed repository {repo_to_reindex}[/green]"
243
+ )
137
244
 
138
245
 
139
246
  if __name__ == "__main__":