starbash 0.1.6__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.
@@ -1,5 +1,12 @@
1
1
  """Shared utilities for starbash commands."""
2
2
 
3
+ from datetime import datetime
4
+ from rich.style import Style
5
+
6
+ # Define reusable table styles
7
+ TABLE_COLUMN_STYLE = Style(color="cyan")
8
+ TABLE_VALUE_STYLE = Style(color="green")
9
+
3
10
 
4
11
  def format_duration(seconds: int | float) -> str:
5
12
  """Format seconds as a human-readable duration string."""
starbash/commands/info.py CHANGED
@@ -2,44 +2,78 @@
2
2
 
3
3
  import typer
4
4
  from typing_extensions import Annotated
5
+ from rich.table import Table
6
+ from collections import Counter
5
7
 
6
8
  from starbash.app import Starbash
7
9
  from starbash import console
8
- from starbash.database import Database
10
+ from starbash.database import Database, get_column_name
9
11
  from starbash.paths import get_user_config_dir, get_user_data_dir
10
- from starbash.commands import format_duration
12
+ from starbash.commands import format_duration, TABLE_COLUMN_STYLE, TABLE_VALUE_STYLE
11
13
 
12
14
  app = typer.Typer()
13
15
 
14
16
 
17
+ def plural(name: str) -> str:
18
+ """Return the plural form of a given noun (simple heuristic - FIXME won't work with i18n)."""
19
+ if name.endswith("y"):
20
+ return name[:-1] + "ies"
21
+ else:
22
+ return name + "s"
23
+
24
+
25
+ def dump_column(sb: Starbash, human_name: str, column_name: str) -> None:
26
+ # Get all telescopes from the database
27
+ sessions = sb.search_session()
28
+
29
+ # Also do a complete unfiltered search so we can compare for the users
30
+ allsessions = sb.db.search_session(("", []))
31
+
32
+ column_name = get_column_name(column_name)
33
+ found = [session[column_name] for session in sessions if session[column_name]]
34
+ allfound = [session[column_name] for session in allsessions if session[column_name]]
35
+
36
+ # Count occurrences of each telescope
37
+ found_counts = Counter(found)
38
+ all_counts = Counter(allfound)
39
+
40
+ # Sort by telescope name
41
+ sorted_list = sorted(found_counts.items())
42
+
43
+ # Create and display table
44
+ table = Table(
45
+ title=f"{plural(human_name)} ({len(found_counts)} / {len(all_counts)} selected)"
46
+ )
47
+ table.add_column(human_name, style=TABLE_COLUMN_STYLE, no_wrap=False)
48
+ table.add_column(
49
+ "# of sessions", style=TABLE_COLUMN_STYLE, no_wrap=True, justify="right"
50
+ )
51
+
52
+ for i, count in sorted_list:
53
+ table.add_row(i, str(count))
54
+
55
+ console.print(table)
56
+
57
+
15
58
  @app.command()
16
59
  def target():
17
60
  """List targets (filtered based on the current selection)."""
18
61
  with Starbash("info.target") as sb:
19
- console.print("[yellow]Not yet implemented[/yellow]")
20
- console.print(
21
- "This command will list all unique targets in the current selection."
22
- )
62
+ dump_column(sb, "Target", Database.OBJECT_KEY)
23
63
 
24
64
 
25
65
  @app.command()
26
66
  def telescope():
27
67
  """List telescopes/instruments (filtered based on the current selection)."""
28
68
  with Starbash("info.telescope") as sb:
29
- console.print("[yellow]Not yet implemented[/yellow]")
30
- console.print(
31
- "This command will list all unique telescopes in the current selection."
32
- )
69
+ dump_column(sb, "Telescope", Database.TELESCOP_KEY)
33
70
 
34
71
 
35
72
  @app.command()
36
73
  def filter():
37
- """List all filters found in current selection."""
74
+ """List all filters (filtered based on the current selection)."""
38
75
  with Starbash("info.filter") as sb:
39
- console.print("[yellow]Not yet implemented[/yellow]")
40
- console.print(
41
- "This command will list all unique filters in the current selection."
42
- )
76
+ dump_column(sb, "Filter", Database.FILTER_KEY)
43
77
 
44
78
 
45
79
  @app.callback(invoke_without_command=True)
@@ -50,15 +84,13 @@ def main_callback(ctx: typer.Context):
50
84
  """
51
85
  if ctx.invoked_subcommand is None:
52
86
  with Starbash("info") as sb:
53
- from rich.table import Table
54
-
55
87
  table = Table(title="Starbash Information")
56
- table.add_column("Setting", style="cyan", no_wrap=True)
57
- table.add_column("Value", style="green")
88
+ table.add_column("Setting", style=TABLE_COLUMN_STYLE, no_wrap=True)
89
+ table.add_column("Value", style=TABLE_VALUE_STYLE)
58
90
 
59
91
  # Show config and data directories
60
- table.add_row("Config Directory", str(get_user_config_dir()))
61
- table.add_row("Data Directory", str(get_user_data_dir()))
92
+ # table.add_row("Config Directory", str(get_user_config_dir()))
93
+ # table.add_row("Data Directory", str(get_user_data_dir()))
62
94
 
63
95
  # Show user preferences if set
64
96
  user_name = sb.user_repo.get("user.name")
@@ -69,10 +101,6 @@ def main_callback(ctx: typer.Context):
69
101
  if user_email:
70
102
  table.add_row("User Email", str(user_email))
71
103
 
72
- # Show analytics setting
73
- analytics_enabled = sb.user_repo.get("analytics.enabled", True)
74
- table.add_row("Analytics", "Enabled" if analytics_enabled else "Disabled")
75
-
76
104
  # Show number of repos
77
105
  table.add_row("Total Repositories", str(len(sb.repo_manager.repos)))
78
106
  table.add_row("User Repositories", str(len(sb.repo_manager.regular_repos)))
@@ -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__":