starbash 0.1.8__py3-none-any.whl → 0.1.9__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.
@@ -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)
@@ -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,208 @@
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.toml import toml_from_template
6
11
 
7
12
  app = typer.Typer(invoke_without_command=True)
8
13
 
9
14
 
10
- @app.callback()
11
- def main(
12
- ctx: typer.Context,
15
+ def repo_enumeration(sb: Starbash):
16
+ """return a dict of int (1 based) to Repo instances"""
17
+ verbose = False # assume not verbose for enum picking
18
+ repos = sb.repo_manager.repos if verbose else sb.repo_manager.regular_repos
19
+
20
+ return {i + 1: repo for i, repo in enumerate(repos)}
21
+
22
+
23
+ def complete_repo_by_num(incomplete: str):
24
+ # We need to use stderr_logging to prevent confusing the bash completion parser
25
+ starbash.log_filter_level = (
26
+ logging.ERROR
27
+ ) # avoid showing output while doing completion
28
+ with Starbash("repo.complete.num", stderr_logging=True) as sb:
29
+ for num, repo in repo_enumeration(sb).items():
30
+ if str(num).startswith(incomplete):
31
+ yield (str(num), repo.url)
32
+
33
+
34
+ def complete_repo_by_url(incomplete: str):
35
+ # We need to use stderr_logging to prevent confusing the bash completion parser
36
+ starbash.log_filter_level = (
37
+ logging.ERROR
38
+ ) # avoid showing output while doing completion
39
+ with Starbash("repo.complete.url", stderr_logging=True) as sb:
40
+ repos = sb.repo_manager.regular_repos
41
+
42
+ for repo in repos:
43
+ if repo.url.startswith(incomplete):
44
+ yield (repo.url, f"kind={repo.kind('input')}")
45
+
46
+
47
+ @app.command()
48
+ def list(
13
49
  verbose: bool = typer.Option(
14
50
  False, "--verbose", "-v", help="Show all repos including system repos"
15
51
  ),
52
+ ):
53
+ """
54
+ lists all repositories.
55
+ Use --verbose to show all repos including system/recipe repos.
56
+ """
57
+ with Starbash("repo.list") as sb:
58
+ repos = sb.repo_manager.repos if verbose else sb.repo_manager.regular_repos
59
+ for i, repo in enumerate(repos):
60
+ kind = repo.kind("input")
61
+ # for unknown repos (probably because we haven't written a starbash.toml file to the root yet),
62
+ # we call them "input" because users will be less confused by that
63
+
64
+ if verbose:
65
+ # No numbers for verbose mode (system repos can't be removed)
66
+ console.print(f"{ repo.url } (kind={ kind })")
67
+ else:
68
+ # Show numbers for user repos (can be removed later)
69
+ console.print(f"{ i + 1:2}: { repo.url } (kind={ kind })")
70
+
71
+
72
+ @app.callback()
73
+ def main(
74
+ ctx: typer.Context,
16
75
  ):
17
76
  """
18
77
  Manage repositories.
19
78
 
20
79
  When called without a subcommand, lists all repositories.
21
- Use --verbose to show all repos including system/recipe repos.
22
80
  """
23
81
  # If no subcommand is invoked, run the list behavior
24
82
  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 })")
83
+ # No command provided, show help
84
+ console.print(ctx.get_help())
85
+ raise typer.Exit()
38
86
 
39
87
 
40
88
  @app.command()
41
- def add(path: str):
89
+ def add(
90
+ path: str,
91
+ master: bool = typer.Option(
92
+ False, "--master", help="Mark this new repository for master files."
93
+ ),
94
+ ):
42
95
  """
43
96
  Add a repository. path is either a local path or a remote URL.
44
97
  """
98
+ repo_type = None
99
+ if master:
100
+ repo_type = "master"
45
101
  with Starbash("repo.add") as sb:
46
- repo = sb.user_repo.add_repo_ref(path)
102
+ p = Path(path)
103
+
104
+ repo_toml = p / repo_suffix # the starbash.toml file at the root of the repo
105
+ if repo_toml.exists():
106
+ logging.warning("Using existing repository config file: %s", repo_toml)
107
+ else:
108
+ if repo_type:
109
+ console.print(f"Creating {repo_type} repository: {p}")
110
+ p.mkdir(parents=True, exist_ok=True)
111
+
112
+ toml_from_template(
113
+ f"repo/{repo_type}",
114
+ p / repo_suffix,
115
+ overrides={
116
+ "REPO_TYPE": repo_type,
117
+ "REPO_PATH": str(p),
118
+ "DEFAULT_RELATIVE": "{instrument}/{date}/{imagetyp}/master_{imagetyp}.fits",
119
+ },
120
+ )
121
+ else:
122
+ # No type specified, therefore (for now) assume we are just using this as an input
123
+ # repo (and it must exist)
124
+ if not p.exists():
125
+ console.print(f"[red]Error: Repo path does not exist: {p}[/red]")
126
+ raise typer.Exit(code=1)
127
+
128
+ console.print(f"Adding repository: {p}")
129
+
130
+ repo = sb.user_repo.add_repo_ref(p)
47
131
  if repo:
48
- console.print(f"Added repository: {path}")
49
132
  sb.reindex_repo(repo)
50
133
 
51
- # we don't yet write default config files at roots of repos, but it would be easy to add here
134
+ # we don't yet always write default config files at roots of repos, but it would be easy to add here
52
135
  # r.write_config()
53
136
  sb.user_repo.write_config()
54
137
  # FIXME, we also need to index the newly added repo!!!
55
138
 
56
139
 
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
140
+ def repo_url_to_repo(sb: Starbash, repo_url: str | None) -> Repo | None:
141
+ """Helper to get a Repo instance from a URL or number"""
142
+ if repo_url is None:
143
+ return None
67
144
 
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)
145
+ # try to find by URL
146
+ repo = sb.repo_manager.get_repo_by_url(repo_url)
147
+ if repo is not None:
148
+ return repo
76
149
 
77
- # Get the repo to remove
78
- repo_to_remove = regular_repos[repo_index]
79
- repo_url = repo_to_remove.url
150
+ # Fall back to finding by number
151
+ try:
152
+ # Parse the repo number (1-indexed)
153
+ repo_index = int(repo_url) - 1
80
154
 
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]")
155
+ # Get only the regular (user-visible) repos
156
+ regular_repos = sb.repo_manager.regular_repos
84
157
 
85
- except ValueError:
158
+ if repo_index < 0 or repo_index >= len(regular_repos):
86
159
  console.print(
87
- f"[red]Error: '{reponum}' is not a valid repository number. Please use a number from 'repo list'.[/red]"
160
+ f"[red]Error: '{repo_url}' is not a valid repository number. Please enter a repository number or URL.[/red]"
88
161
  )
89
162
  raise typer.Exit(code=1)
90
163
 
164
+ return regular_repos[repo_index]
165
+ except ValueError:
166
+ console.print(
167
+ f"[red]Error: '{repo_url}' is not valid. Please enter a repository number or URL.[/red]"
168
+ )
169
+ raise typer.Exit(code=1)
170
+
91
171
 
92
172
  @app.command()
93
- def reindex(
173
+ def remove(
94
174
  reponum: Annotated[
175
+ str,
176
+ typer.Argument(
177
+ help="Repository number or URL", autocompletion=complete_repo_by_url
178
+ ),
179
+ ],
180
+ ):
181
+ """
182
+ Remove a repository by number (from list).
183
+ Use 'starbash repo' to see the repository numbers.
184
+ """
185
+ with Starbash("repo.remove") as sb:
186
+ # Get the repo to remove
187
+ repo_to_remove = repo_url_to_repo(sb, reponum)
188
+ if repo_to_remove is None:
189
+ console.print(f"[red]Error: You must specify a repository[/red]")
190
+ raise typer.Exit(code=1)
191
+ repo_url = repo_to_remove.url
192
+
193
+ # Remove the repo reference from user config
194
+ sb.remove_repo_ref(repo_url)
195
+ console.print(f"[green]Removed repository: {repo_url}[/green]")
196
+
197
+
198
+ @app.command()
199
+ def reindex(
200
+ repo_url: Annotated[
95
201
  str | None,
96
- typer.Argument(help="The repository number, if not specified reindex all."),
202
+ typer.Argument(
203
+ help="The repository URL, if not specified reindex all.",
204
+ autocompletion=complete_repo_by_url,
205
+ ),
97
206
  ] = None,
98
207
  force: bool = typer.Option(
99
208
  default=False, help="Reread FITS headers, even if they are already indexed."
@@ -105,35 +214,17 @@ def reindex(
105
214
  Use 'starbash repo' to see the repository numbers.
106
215
  """
107
216
  with Starbash("repo.reindex") as sb:
108
- if reponum is None:
217
+ repo_to_reindex = repo_url_to_repo(sb, repo_url)
218
+
219
+ if repo_to_reindex is None:
109
220
  sb.reindex_repos(force=force)
110
221
  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)
222
+ # Get the repo to reindex
223
+ console.print(f"Reindexing repository: {repo_to_reindex.url}")
224
+ sb.reindex_repo(repo_to_reindex, force=force)
225
+ console.print(
226
+ f"[green]Successfully reindexed repository {repo_to_reindex}[/green]"
227
+ )
137
228
 
138
229
 
139
230
  if __name__ == "__main__":