starbash 0.1.3__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.
- starbash/__init__.py +5 -0
- starbash/analytics.py +21 -6
- starbash/app.py +63 -9
- starbash/commands/repo.py +19 -12
- starbash/commands/select.py +326 -0
- starbash/commands/user.py +91 -6
- starbash/database.py +11 -0
- starbash/defaults/starbash.toml +2 -2
- starbash/main.py +25 -133
- starbash/recipes/README.md +3 -0
- starbash/recipes/__init__.py +0 -0
- starbash/recipes/master_bias/starbash.toml +55 -0
- starbash/recipes/master_flat/starbash.toml +46 -0
- starbash/recipes/osc_dual_duo/starbash.py +151 -0
- starbash/recipes/osc_dual_duo/starbash.toml +88 -0
- starbash/recipes/osc_single_duo/starbash.toml +67 -0
- starbash/recipes/starbash.toml +34 -0
- starbash/repo/manager.py +78 -22
- starbash/templates/userconfig.toml +1 -1
- {starbash-0.1.3.dist-info → starbash-0.1.4.dist-info}/METADATA +33 -23
- starbash-0.1.4.dist-info/RECORD +32 -0
- starbash/commands/selection.py +0 -137
- starbash-0.1.3.dist-info/RECORD +0 -24
- {starbash-0.1.3.dist-info → starbash-0.1.4.dist-info}/WHEEL +0 -0
- {starbash-0.1.3.dist-info → starbash-0.1.4.dist-info}/entry_points.txt +0 -0
- {starbash-0.1.3.dist-info → starbash-0.1.4.dist-info}/licenses/LICENSE +0 -0
starbash/__init__.py
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from .database import Database # re-export for convenience
|
|
2
4
|
from rich.console import Console
|
|
3
5
|
|
|
4
6
|
console = Console()
|
|
5
7
|
|
|
8
|
+
# Global variable for log filter level (can be changed via --debug flag)
|
|
9
|
+
log_filter_level = logging.INFO
|
|
10
|
+
|
|
6
11
|
__all__ = ["Database"]
|
starbash/analytics.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
|
|
4
|
+
import starbash
|
|
3
5
|
from starbash import console
|
|
4
6
|
import starbash.url as url
|
|
5
7
|
|
|
@@ -8,11 +10,12 @@ analytics_allowed = False
|
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def analytics_setup(allowed: bool = False, user_email: str | None = None) -> None:
|
|
11
|
-
import sentry_sdk
|
|
12
|
-
|
|
13
13
|
global analytics_allowed
|
|
14
14
|
analytics_allowed = allowed
|
|
15
15
|
if analytics_allowed:
|
|
16
|
+
import sentry_sdk
|
|
17
|
+
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
18
|
+
|
|
16
19
|
logging.info(
|
|
17
20
|
f"Analytics/crash-reports enabled. To change [link={url.analytics_docs}]click here[/link]",
|
|
18
21
|
extra={"markup": True},
|
|
@@ -22,6 +25,13 @@ def analytics_setup(allowed: bool = False, user_email: str | None = None) -> Non
|
|
|
22
25
|
send_default_pii=True,
|
|
23
26
|
enable_logs=True,
|
|
24
27
|
traces_sample_rate=1.0,
|
|
28
|
+
integrations=[
|
|
29
|
+
LoggingIntegration(
|
|
30
|
+
level=starbash.log_filter_level, # Capture INFO and above as breadcrumbs
|
|
31
|
+
event_level=None, # Don't automatically convert error messages to sentry events
|
|
32
|
+
sentry_logs_level=starbash.log_filter_level, # Capture INFO and above as logs
|
|
33
|
+
),
|
|
34
|
+
],
|
|
25
35
|
)
|
|
26
36
|
|
|
27
37
|
if user_email:
|
|
@@ -41,11 +51,13 @@ def analytics_shutdown() -> None:
|
|
|
41
51
|
sentry_sdk.flush()
|
|
42
52
|
|
|
43
53
|
|
|
54
|
+
def is_running_in_pytest() -> bool:
|
|
55
|
+
"""Detect if code is being run inside pytest."""
|
|
56
|
+
return "PYTEST_CURRENT_TEST" in os.environ
|
|
57
|
+
|
|
58
|
+
|
|
44
59
|
def is_development_environment() -> bool:
|
|
45
60
|
"""Detect if running in a development environment."""
|
|
46
|
-
import os
|
|
47
|
-
import sys
|
|
48
|
-
from pathlib import Path
|
|
49
61
|
|
|
50
62
|
# Check for explicit environment variable
|
|
51
63
|
if os.getenv("STARBASH_ENV") == "development":
|
|
@@ -68,7 +80,10 @@ def analytics_exception(exc: Exception) -> bool:
|
|
|
68
80
|
if analytics_allowed:
|
|
69
81
|
import sentry_sdk
|
|
70
82
|
|
|
71
|
-
|
|
83
|
+
if is_running_in_pytest():
|
|
84
|
+
report_id = "TESTING-ENVIRONMENT"
|
|
85
|
+
else:
|
|
86
|
+
report_id = sentry_sdk.capture_exception(exc)
|
|
72
87
|
|
|
73
88
|
logging.info(
|
|
74
89
|
f"""An unexpected error has occurred and been reported. Thank you for your help.
|
starbash/app.py
CHANGED
|
@@ -10,7 +10,10 @@ from astropy.io import fits
|
|
|
10
10
|
import itertools
|
|
11
11
|
from rich.progress import track
|
|
12
12
|
from rich.logging import RichHandler
|
|
13
|
+
import shutil
|
|
13
14
|
|
|
15
|
+
import starbash
|
|
16
|
+
from starbash import console
|
|
14
17
|
from starbash.database import Database
|
|
15
18
|
from starbash.repo.manager import Repo
|
|
16
19
|
from starbash.tool import Tool
|
|
@@ -32,30 +35,81 @@ def setup_logging():
|
|
|
32
35
|
Configures basic logging.
|
|
33
36
|
"""
|
|
34
37
|
logging.basicConfig(
|
|
35
|
-
level=
|
|
38
|
+
level=starbash.log_filter_level, # use the global log filter level
|
|
36
39
|
format="%(message)s",
|
|
37
40
|
datefmt="[%X]",
|
|
38
41
|
handlers=[RichHandler(rich_tracebacks=True)],
|
|
39
42
|
)
|
|
40
43
|
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
def get_user_config_path() -> Path:
|
|
46
|
+
"""Returns the path to the user config file."""
|
|
47
|
+
config_dir = get_user_config_dir()
|
|
48
|
+
return config_dir / "starbash.toml"
|
|
43
49
|
|
|
44
50
|
|
|
45
51
|
def create_user() -> Path:
|
|
46
52
|
"""Create user directories if they don't exist yet."""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if not (userconfig_path).exists():
|
|
53
|
+
path = get_user_config_path()
|
|
54
|
+
if not path.exists():
|
|
50
55
|
tomlstr = (
|
|
51
56
|
resources.files("starbash")
|
|
52
57
|
.joinpath("templates/userconfig.toml")
|
|
53
58
|
.read_text()
|
|
54
59
|
)
|
|
55
60
|
toml = tomlkit.parse(tomlstr)
|
|
56
|
-
TOMLFile(
|
|
57
|
-
logging.info(f"Created user config file: {
|
|
58
|
-
return
|
|
61
|
+
TOMLFile(path).write(toml)
|
|
62
|
+
logging.info(f"Created user config file: {path}")
|
|
63
|
+
return get_user_config_dir()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def copy_images_to_dir(images: list[dict[str, Any]], output_dir: Path) -> None:
|
|
67
|
+
"""Copy images to the specified output directory (using symbolic links if possible)."""
|
|
68
|
+
|
|
69
|
+
# Export images
|
|
70
|
+
console.print(f"[cyan]Exporting {len(images)} images to {output_dir}...[/cyan]")
|
|
71
|
+
|
|
72
|
+
linked_count = 0
|
|
73
|
+
copied_count = 0
|
|
74
|
+
error_count = 0
|
|
75
|
+
|
|
76
|
+
for image in images:
|
|
77
|
+
# Get the source path from the image metadata
|
|
78
|
+
source_path = Path(image.get("path", ""))
|
|
79
|
+
|
|
80
|
+
if not source_path.exists():
|
|
81
|
+
console.print(f"[red]Warning: Source file not found: {source_path}[/red]")
|
|
82
|
+
error_count += 1
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Determine destination filename
|
|
86
|
+
dest_path = output_dir / source_path.name
|
|
87
|
+
if dest_path.exists():
|
|
88
|
+
console.print(f"[yellow]Skipping existing file: {dest_path}[/yellow]")
|
|
89
|
+
error_count += 1
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
# Try to create a symbolic link first
|
|
93
|
+
try:
|
|
94
|
+
dest_path.symlink_to(source_path.resolve())
|
|
95
|
+
linked_count += 1
|
|
96
|
+
except (OSError, NotImplementedError):
|
|
97
|
+
# If symlink fails, try to copy
|
|
98
|
+
try:
|
|
99
|
+
shutil.copy2(source_path, dest_path)
|
|
100
|
+
copied_count += 1
|
|
101
|
+
except Exception as e:
|
|
102
|
+
console.print(f"[red]Error copying {source_path.name}: {e}[/red]")
|
|
103
|
+
error_count += 1
|
|
104
|
+
|
|
105
|
+
# Print summary
|
|
106
|
+
console.print(f"[green]Export complete![/green]")
|
|
107
|
+
if linked_count > 0:
|
|
108
|
+
console.print(f" Linked: {linked_count} files")
|
|
109
|
+
if copied_count > 0:
|
|
110
|
+
console.print(f" Copied: {copied_count} files")
|
|
111
|
+
if error_count > 0:
|
|
112
|
+
console.print(f" [red]Errors: {error_count} files[/red]")
|
|
59
113
|
|
|
60
114
|
|
|
61
115
|
class Starbash:
|
|
@@ -90,7 +144,7 @@ class Starbash:
|
|
|
90
144
|
self.analytics.__enter__()
|
|
91
145
|
|
|
92
146
|
logging.info(
|
|
93
|
-
f"Repo manager initialized with {len(self.repo_manager.repos)}
|
|
147
|
+
f"Repo manager initialized with {len(self.repo_manager.repos)} repos."
|
|
94
148
|
)
|
|
95
149
|
# self.repo_manager.dump()
|
|
96
150
|
|
starbash/commands/repo.py
CHANGED
|
@@ -22,15 +22,19 @@ def main(
|
|
|
22
22
|
"""
|
|
23
23
|
# If no subcommand is invoked, run the list behavior
|
|
24
24
|
if ctx.invoked_subcommand is None:
|
|
25
|
-
with Starbash("repo
|
|
25
|
+
with Starbash("repo.list") as sb:
|
|
26
26
|
repos = sb.repo_manager.repos if verbose else sb.repo_manager.regular_repos
|
|
27
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
|
+
|
|
28
32
|
if verbose:
|
|
29
33
|
# No numbers for verbose mode (system repos can't be removed)
|
|
30
|
-
console.print(f"{ repo.url } (kind={
|
|
34
|
+
console.print(f"{ repo.url } (kind={ kind })")
|
|
31
35
|
else:
|
|
32
36
|
# Show numbers for user repos (can be removed later)
|
|
33
|
-
console.print(f"{ i + 1:2}: { repo.url } (kind={
|
|
37
|
+
console.print(f"{ i + 1:2}: { repo.url } (kind={ kind })")
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
@app.command()
|
|
@@ -38,13 +42,16 @@ def add(path: str):
|
|
|
38
42
|
"""
|
|
39
43
|
Add a repository. path is either a local path or a remote URL.
|
|
40
44
|
"""
|
|
41
|
-
with Starbash("repo
|
|
42
|
-
sb.user_repo.add_repo_ref(path)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
with Starbash("repo.add") as sb:
|
|
46
|
+
repo = sb.user_repo.add_repo_ref(path)
|
|
47
|
+
if repo:
|
|
48
|
+
console.print(f"Added repository: {path}")
|
|
49
|
+
sb.reindex_repo(repo)
|
|
50
|
+
|
|
51
|
+
# we don't yet write default config files at roots of repos, but it would be easy to add here
|
|
52
|
+
# r.write_config()
|
|
53
|
+
sb.user_repo.write_config()
|
|
54
|
+
# FIXME, we also need to index the newly added repo!!!
|
|
48
55
|
|
|
49
56
|
|
|
50
57
|
@app.command()
|
|
@@ -53,7 +60,7 @@ def remove(reponum: str):
|
|
|
53
60
|
Remove a repository by number (from list).
|
|
54
61
|
Use 'starbash repo' to see the repository numbers.
|
|
55
62
|
"""
|
|
56
|
-
with Starbash("repo
|
|
63
|
+
with Starbash("repo.remove") as sb:
|
|
57
64
|
try:
|
|
58
65
|
# Parse the repo number (1-indexed)
|
|
59
66
|
repo_index = int(reponum) - 1
|
|
@@ -97,7 +104,7 @@ def reindex(
|
|
|
97
104
|
If no number is given, reindex all repositories.
|
|
98
105
|
Use 'starbash repo' to see the repository numbers.
|
|
99
106
|
"""
|
|
100
|
-
with Starbash("repo
|
|
107
|
+
with Starbash("repo.reindex") as sb:
|
|
101
108
|
if reponum is None:
|
|
102
109
|
sb.reindex_repos(force=force)
|
|
103
110
|
else:
|
|
@@ -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)
|