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

@@ -0,0 +1,326 @@
1
+ """Selection commands for filtering sessions and targets."""
2
+
3
+ import os
4
+ import typer
5
+ from pathlib import Path
6
+ from typing_extensions import Annotated
7
+ from datetime import datetime
8
+ from rich.table import Table
9
+
10
+ from starbash.app import Starbash, copy_images_to_dir
11
+ from starbash.database import Database
12
+ from starbash import console
13
+
14
+ app = typer.Typer()
15
+
16
+
17
+ @app.command(name="any")
18
+ def clear():
19
+ """Remove any filters on sessions, etc... (select everything)."""
20
+ with Starbash("selection.clear") as sb:
21
+ sb.selection.clear()
22
+ console.print("[green]Selection cleared - now selecting all sessions[/green]")
23
+
24
+
25
+ @app.command()
26
+ def target(
27
+ target_name: Annotated[
28
+ str,
29
+ typer.Argument(
30
+ help="Target name to add to the selection (e.g., 'M31', 'NGC 7000')"
31
+ ),
32
+ ],
33
+ ):
34
+ """Limit the current selection to only the named target."""
35
+ with Starbash("selection.target") as sb:
36
+ # For now, replace existing targets with this one
37
+ # In the future, we could support adding multiple targets
38
+ sb.selection.targets = []
39
+ sb.selection.add_target(target_name)
40
+ console.print(f"[green]Selection limited to target: {target_name}[/green]")
41
+
42
+
43
+ @app.command()
44
+ def telescope(
45
+ telescope_name: Annotated[
46
+ str,
47
+ typer.Argument(
48
+ help="Telescope name to add to the selection (e.g., 'Vespera', 'EdgeHD 8')"
49
+ ),
50
+ ],
51
+ ):
52
+ """Limit the current selection to only the named telescope."""
53
+ with Starbash("selection.telescope") as sb:
54
+ # For now, replace existing telescopes with this one
55
+ # In the future, we could support adding multiple telescopes
56
+ sb.selection.telescopes = []
57
+ sb.selection.add_telescope(telescope_name)
58
+ console.print(
59
+ f"[green]Selection limited to telescope: {telescope_name}[/green]"
60
+ )
61
+
62
+
63
+ @app.command()
64
+ def date(
65
+ operation: Annotated[
66
+ str,
67
+ typer.Argument(help="Date operation: 'after', 'before', or 'between'"),
68
+ ],
69
+ date_value: Annotated[
70
+ str,
71
+ typer.Argument(
72
+ help="Date in ISO format (YYYY-MM-DD) or two dates separated by space for 'between'"
73
+ ),
74
+ ],
75
+ end_date: Annotated[
76
+ str | None,
77
+ typer.Argument(help="End date for 'between' operation (YYYY-MM-DD)"),
78
+ ] = None,
79
+ ):
80
+ """Limit to sessions in the specified date range.
81
+
82
+ Examples:
83
+ starbash selection date after 2023-10-01
84
+ starbash selection date before 2023-12-31
85
+ starbash selection date between 2023-10-01 2023-12-31
86
+ """
87
+ with Starbash("selection.date") as sb:
88
+ operation = operation.lower()
89
+
90
+ if operation == "after":
91
+ sb.selection.set_date_range(start=date_value, end=None)
92
+ console.print(
93
+ f"[green]Selection limited to sessions after {date_value}[/green]"
94
+ )
95
+ elif operation == "before":
96
+ sb.selection.set_date_range(start=None, end=date_value)
97
+ console.print(
98
+ f"[green]Selection limited to sessions before {date_value}[/green]"
99
+ )
100
+ elif operation == "between":
101
+ if not end_date:
102
+ console.print(
103
+ "[red]Error: 'between' operation requires two dates[/red]"
104
+ )
105
+ raise typer.Exit(1)
106
+ sb.selection.set_date_range(start=date_value, end=end_date)
107
+ console.print(
108
+ f"[green]Selection limited to sessions between {date_value} and {end_date}[/green]"
109
+ )
110
+ else:
111
+ console.print(
112
+ f"[red]Error: Unknown operation '{operation}'. Use 'after', 'before', or 'between'[/red]"
113
+ )
114
+ raise typer.Exit(1)
115
+
116
+
117
+ def format_duration(seconds: int):
118
+ """Format seconds as a human-readable duration string."""
119
+ if seconds < 60:
120
+ return f"{int(seconds)}s"
121
+ elif seconds < 120:
122
+ minutes = int(seconds // 60)
123
+ secs = int(seconds % 60)
124
+ return f"{minutes}m {secs}s" if secs else f"{minutes}m"
125
+ else:
126
+ hours = int(seconds // 3600)
127
+ minutes = int((seconds % 3600) // 60)
128
+ return f"{hours}h {minutes}m" if minutes else f"{hours}h"
129
+
130
+
131
+ @app.command(name="list")
132
+ def list_sessions():
133
+ """List sessions (filtered based on the current selection)"""
134
+
135
+ with Starbash("selection.list") as sb:
136
+ sessions = sb.search_session()
137
+ if sessions and isinstance(sessions, list):
138
+ len_all = sb.db.len_session()
139
+ table = Table(title=f"Sessions ({len(sessions)} selected out of {len_all})")
140
+ sb.analytics.set_data("session.num_selected", len(sessions))
141
+ sb.analytics.set_data("session.num_total", len_all)
142
+
143
+ table.add_column("#", style="cyan", no_wrap=True)
144
+ table.add_column("Date", style="cyan", no_wrap=True)
145
+ table.add_column("# images", style="cyan", no_wrap=True)
146
+ table.add_column("Time", style="cyan", no_wrap=True)
147
+ table.add_column("Type/Filter", style="cyan", no_wrap=True)
148
+ table.add_column("Telescope", style="cyan", no_wrap=True)
149
+ table.add_column(
150
+ "About", style="cyan", no_wrap=True
151
+ ) # type of frames, filter, target
152
+
153
+ total_images = 0
154
+ total_seconds = 0.0
155
+ filters = set()
156
+ image_types = set()
157
+ telescopes = set()
158
+
159
+ for session_index, sess in enumerate(sessions):
160
+ date_iso = sess.get(Database.START_KEY, "N/A")
161
+ # Try to convert ISO UTC datetime to local short date string
162
+ try:
163
+ dt_utc = datetime.fromisoformat(date_iso)
164
+ dt_local = dt_utc.astimezone()
165
+ date = dt_local.strftime("%Y-%m-%d")
166
+ except (ValueError, TypeError):
167
+ date = date_iso
168
+
169
+ object = str(sess.get(Database.OBJECT_KEY, "N/A"))
170
+ filter = sess.get(Database.FILTER_KEY, "N/A")
171
+ filters.add(filter)
172
+ image_type = str(sess.get(Database.IMAGETYP_KEY, "N/A"))
173
+ image_types.add(image_type)
174
+ telescope = str(sess.get(Database.TELESCOP_KEY, "N/A"))
175
+ telescopes.add(telescope)
176
+
177
+ # Format total exposure time as integer seconds
178
+ exptime_raw = str(sess.get(Database.EXPTIME_TOTAL_KEY, "N/A"))
179
+ try:
180
+ exptime_float = float(exptime_raw)
181
+ total_seconds += exptime_float
182
+ total_secs = format_duration(int(exptime_float))
183
+ except (ValueError, TypeError):
184
+ total_secs = exptime_raw
185
+
186
+ # Count images
187
+ try:
188
+ num_images = int(sess.get(Database.NUM_IMAGES_KEY, 0))
189
+ total_images += num_images
190
+ except (ValueError, TypeError):
191
+ num_images = sess.get(Database.NUM_IMAGES_KEY, "N/A")
192
+
193
+ type_str = image_type
194
+ if image_type.upper() == "LIGHT":
195
+ image_type = filter
196
+ elif image_type.upper() == "FLAT":
197
+ image_type = f"{image_type}/{filter}"
198
+ else: # either bias or dark
199
+ object = "" # Don't show meaningless target
200
+
201
+ table.add_row(
202
+ str(session_index + 1),
203
+ date,
204
+ str(num_images),
205
+ total_secs,
206
+ image_type,
207
+ telescope,
208
+ object,
209
+ )
210
+
211
+ # Add totals row
212
+ if sessions:
213
+ table.add_row(
214
+ "",
215
+ "",
216
+ f"[bold]{total_images}[/bold]",
217
+ f"[bold]{format_duration(int(total_seconds))}[/bold]",
218
+ "",
219
+ "",
220
+ "",
221
+ )
222
+
223
+ console.print(table)
224
+
225
+ # FIXME - move these analytics elsewhere so they can be reused when search_session()
226
+ # is used to generate processing lists.
227
+ sb.analytics.set_data("session.total_images", total_images)
228
+ sb.analytics.set_data("session.total_exposure_seconds", int(total_seconds))
229
+ sb.analytics.set_data("session.telescopes", telescopes)
230
+ sb.analytics.set_data("session.filters", filters)
231
+ sb.analytics.set_data("session.image_types", image_types)
232
+
233
+
234
+ @app.command()
235
+ def export(
236
+ session_num: Annotated[
237
+ int,
238
+ typer.Argument(help="Session number to export (from 'select list' output)"),
239
+ ],
240
+ destdir: Annotated[
241
+ str,
242
+ typer.Argument(
243
+ help="Directory path to export to (if it doesn't exist it will be created)"
244
+ ),
245
+ ],
246
+ ):
247
+ """Export the images for the indicated session number.
248
+
249
+ Uses symbolic links when possible, otherwise copies files.
250
+ The session number corresponds to the '#' column in 'select list' output.
251
+ """
252
+ with Starbash("selection.export") as sb:
253
+ # Get the filtered sessions
254
+ sessions = sb.search_session()
255
+
256
+ if not sessions or not isinstance(sessions, list):
257
+ console.print(
258
+ "[red]No sessions found. Check your selection criteria.[/red]"
259
+ )
260
+ raise typer.Exit(1)
261
+
262
+ # Validate session number
263
+ if session_num < 1 or session_num > len(sessions):
264
+ console.print(
265
+ f"[red]Error: Session number {session_num} is out of range. "
266
+ f"Valid range is 1-{len(sessions)}.[/red]"
267
+ )
268
+ console.print(
269
+ "[yellow]Use 'select list' to see available sessions.[/yellow]"
270
+ )
271
+ raise typer.Exit(1)
272
+
273
+ # Get the selected session (convert from 1-based to 0-based index)
274
+ session = sessions[session_num - 1]
275
+
276
+ # Get the session's database row ID
277
+ session_id = session.get("id")
278
+ if session_id is None:
279
+ console.print(
280
+ f"[red]Error: Could not find session ID for session {session_num}.[/red]"
281
+ )
282
+ raise typer.Exit(1)
283
+
284
+ # Determine output directory
285
+ output_dir = Path(destdir)
286
+
287
+ # Create output directory if it doesn't exist
288
+ output_dir.mkdir(parents=True, exist_ok=True)
289
+
290
+ # Get images for this session
291
+ images = sb.get_session_images(session_id)
292
+
293
+ if not images:
294
+ console.print(
295
+ f"[yellow]Warning: No images found for session {session_num}.[/yellow]"
296
+ )
297
+ raise typer.Exit(0)
298
+
299
+ copy_images_to_dir(images, output_dir)
300
+
301
+
302
+ @app.callback(invoke_without_command=True)
303
+ def show_selection(ctx: typer.Context):
304
+ """List information about the current selection.
305
+
306
+ This is the default command when no subcommand is specified.
307
+ """
308
+ if ctx.invoked_subcommand is None:
309
+ with Starbash("selection.show") as sb:
310
+ summary = sb.selection.summary()
311
+
312
+ if summary["status"] == "all":
313
+ console.print(f"[yellow]{summary['message']}[/yellow]")
314
+ else:
315
+ table = Table(title="Current Selection")
316
+ table.add_column("Criteria", style="cyan")
317
+ table.add_column("Value", style="green")
318
+
319
+ for criterion in summary["criteria"]:
320
+ parts = criterion.split(": ", 1)
321
+ if len(parts) == 2:
322
+ table.add_row(parts[0], parts[1])
323
+ else:
324
+ table.add_row(criterion, "")
325
+
326
+ console.print(table)
starbash/commands/user.py CHANGED
@@ -3,6 +3,7 @@ from typing_extensions import Annotated
3
3
 
4
4
  from starbash.app import Starbash
5
5
  from starbash import console
6
+ from rich.panel import Panel
6
7
 
7
8
  app = typer.Typer()
8
9
 
@@ -19,9 +20,9 @@ def analytics(
19
20
  """
20
21
  Enable or disable analytics (crash reports and usage data).
21
22
  """
22
- with Starbash("analytics-enable") as sb:
23
+ with Starbash("analytics.change") as sb:
23
24
  sb.analytics.set_data("analytics.enabled", enable)
24
- sb.user_repo.config["analytics.enabled"] = enable
25
+ sb.user_repo.set("analytics.enabled", enable)
25
26
  sb.user_repo.write_config()
26
27
  status = "enabled" if enable else "disabled"
27
28
  console.print(f"Analytics (crash reports) {status}.")
@@ -39,8 +40,8 @@ def name(
39
40
  """
40
41
  Set your name for attribution in generated images.
41
42
  """
42
- with Starbash("user-name") as sb:
43
- sb.user_repo.config["user.name"] = user_name
43
+ with Starbash("user.name") as sb:
44
+ sb.user_repo.set("user.name", user_name)
44
45
  sb.user_repo.write_config()
45
46
  console.print(f"User name set to: {user_name}")
46
47
 
@@ -57,7 +58,91 @@ def email(
57
58
  """
58
59
  Set your email for attribution in generated images.
59
60
  """
60
- with Starbash("user-email") as sb:
61
- sb.user_repo.config["user.email"] = user_email
61
+ with Starbash("user.email") as sb:
62
+ sb.user_repo.set("user.email", user_email)
62
63
  sb.user_repo.write_config()
63
64
  console.print(f"User email set to: {user_email}")
65
+
66
+
67
+ def do_reinit(sb: Starbash) -> None:
68
+ console.print()
69
+ console.print(
70
+ Panel.fit(
71
+ "[bold cyan]Starbash getting started...[/bold cyan]\n\n"
72
+ "Let's set up your preferences. You can skip any question by pressing Enter.",
73
+ border_style="cyan",
74
+ )
75
+ )
76
+ console.print()
77
+
78
+ # Ask for username
79
+ user_name = typer.prompt(
80
+ "Enter your name (for attribution in generated images)",
81
+ default="",
82
+ show_default=False,
83
+ )
84
+ sb.analytics.set_data("analytics.use_name", user_name != "")
85
+ if user_name:
86
+ sb.user_repo.set("user.name", user_name)
87
+ console.print(f"✅ Name set to: {user_name}")
88
+ else:
89
+ console.print("[dim]Skipped name[/dim]")
90
+
91
+ # Ask for email
92
+ user_email = typer.prompt(
93
+ "Enter your email address (for attribution in generated images)",
94
+ default="",
95
+ show_default=False,
96
+ )
97
+ sb.analytics.set_data("analytics.use_email", user_email != "")
98
+ if user_email:
99
+ sb.user_repo.set("user.email", user_email)
100
+ console.print(f"✅ Email set to: {user_email}")
101
+ else:
102
+ console.print("[dim]Skipped email[/dim]")
103
+
104
+ # Ask about including email in crash reports
105
+ include_in_reports = typer.confirm(
106
+ "Would you like to include your email address with crash reports/analytics? "
107
+ "(This helps us follow up if we need more information about issues.)",
108
+ default=False,
109
+ )
110
+ sb.analytics.set_data("analytics.use_email_report", include_in_reports)
111
+ sb.user_repo.set("analytics.include_user", include_in_reports)
112
+ if include_in_reports:
113
+ console.print("✅ Email will be included with crash reports")
114
+ else:
115
+ console.print("❌ Email will NOT be included with crash reports")
116
+ console.print()
117
+
118
+ # Save all changes
119
+ sb.user_repo.write_config()
120
+
121
+ console.print(
122
+ Panel.fit(
123
+ "[bold green]Configuration complete![/bold green]\n\n"
124
+ "Your preferences have been saved.",
125
+ border_style="green",
126
+ )
127
+ )
128
+
129
+
130
+ @app.command()
131
+ def reinit():
132
+ """
133
+ Configure starbash via a brief guided process.
134
+
135
+ This will ask you for your name, email, and analytics preferences.
136
+ You can skip any question by pressing Enter.
137
+ """
138
+ with Starbash("user.reinit") as sb:
139
+ do_reinit(sb)
140
+
141
+
142
+ @app.callback(invoke_without_command=True)
143
+ def main_callback(ctx: typer.Context):
144
+ """Main callback for the Starbash application."""
145
+ if ctx.invoked_subcommand is None:
146
+ # No command provided, show help
147
+ console.print(ctx.get_help())
148
+ raise typer.Exit()