starbash 0.1.11__py3-none-any.whl → 0.1.15__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.
Files changed (43) hide show
  1. repo/__init__.py +1 -1
  2. repo/manager.py +14 -23
  3. repo/repo.py +52 -10
  4. starbash/__init__.py +10 -3
  5. starbash/aliases.py +49 -4
  6. starbash/analytics.py +3 -2
  7. starbash/app.py +287 -565
  8. starbash/check_version.py +18 -0
  9. starbash/commands/__init__.py +2 -1
  10. starbash/commands/info.py +26 -21
  11. starbash/commands/process.py +76 -24
  12. starbash/commands/repo.py +25 -68
  13. starbash/commands/select.py +140 -148
  14. starbash/commands/user.py +88 -23
  15. starbash/database.py +41 -27
  16. starbash/defaults/starbash.toml +1 -0
  17. starbash/exception.py +21 -0
  18. starbash/main.py +29 -7
  19. starbash/paths.py +23 -9
  20. starbash/processing.py +724 -0
  21. starbash/recipes/README.md +3 -0
  22. starbash/recipes/master_bias/starbash.toml +4 -1
  23. starbash/recipes/master_dark/starbash.toml +0 -1
  24. starbash/recipes/osc.py +190 -0
  25. starbash/recipes/osc_dual_duo/starbash.toml +31 -34
  26. starbash/recipes/osc_simple/starbash.toml +82 -0
  27. starbash/recipes/osc_single_duo/starbash.toml +51 -32
  28. starbash/recipes/seestar/starbash.toml +82 -0
  29. starbash/recipes/starbash.toml +8 -9
  30. starbash/selection.py +29 -38
  31. starbash/templates/repo/master.toml +7 -3
  32. starbash/templates/repo/processed.toml +7 -2
  33. starbash/templates/userconfig.toml +9 -0
  34. starbash/toml.py +13 -13
  35. starbash/tool.py +186 -149
  36. starbash-0.1.15.dist-info/METADATA +216 -0
  37. starbash-0.1.15.dist-info/RECORD +45 -0
  38. starbash/recipes/osc_dual_duo/starbash.py +0 -147
  39. starbash-0.1.11.dist-info/METADATA +0 -147
  40. starbash-0.1.11.dist-info/RECORD +0 -40
  41. {starbash-0.1.11.dist-info → starbash-0.1.15.dist-info}/WHEEL +0 -0
  42. {starbash-0.1.11.dist-info → starbash-0.1.15.dist-info}/entry_points.txt +0 -0
  43. {starbash-0.1.11.dist-info → starbash-0.1.15.dist-info}/licenses/LICENSE +0 -0
starbash/database.py CHANGED
@@ -1,20 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import sqlite3
4
- from pathlib import Path
5
- from typing import Any, Optional, NamedTuple
5
+ from dataclasses import dataclass
6
6
  from datetime import datetime, timedelta
7
- import json
8
- from typing import TypeAlias
7
+ from pathlib import Path
8
+ from typing import Any
9
9
 
10
- from .paths import get_user_data_dir
11
10
  from .aliases import normalize_target_name
11
+ from .paths import get_user_data_dir
12
12
 
13
- SessionRow: TypeAlias = dict[str, Any]
14
- ImageRow: TypeAlias = dict[str, Any]
13
+ type SessionRow = dict[str, Any]
14
+ type ImageRow = dict[str, Any]
15
15
 
16
16
 
17
- class SearchCondition(NamedTuple):
17
+ @dataclass(frozen=True)
18
+ class SearchCondition:
18
19
  """A search condition for database queries.
19
20
 
20
21
  Args:
@@ -83,7 +84,9 @@ class Database:
83
84
  IMAGETYP_KEY = "IMAGETYP"
84
85
  OBJECT_KEY = "OBJECT"
85
86
  TELESCOP_KEY = "TELESCOP"
86
- EXPTIME_KEY = "EXPTIME"
87
+ EXPTIME_KEY = "EXPTIME" # in all image files
88
+ TOTALEXP_KEY = "TOTALEXP" # in stacked ASI files
89
+
87
90
  ID_KEY = "id" # for finding any row by its ID
88
91
  REPO_URL_KEY = "repo_url"
89
92
 
@@ -93,7 +96,7 @@ class Database:
93
96
 
94
97
  def __init__(
95
98
  self,
96
- base_dir: Optional[Path] = None,
99
+ base_dir: Path | None = None,
97
100
  ) -> None:
98
101
  # Resolve base data directory (allow override for tests)
99
102
  if base_dir is None:
@@ -187,7 +190,7 @@ class Database:
187
190
  filter TEXT COLLATE NOCASE,
188
191
  imagetyp TEXT COLLATE NOCASE NOT NULL,
189
192
  object TEXT,
190
- telescop TEXT NOT NULL,
193
+ telescop TEXT COLLATENOCASE NOT NULL,
191
194
  num_images INTEGER NOT NULL,
192
195
  exptime_total REAL NOT NULL,
193
196
  exptime REAL NOT NULL,
@@ -426,26 +429,41 @@ class Database:
426
429
 
427
430
  return results
428
431
 
429
- def search_session(
430
- self, where_tuple: tuple[str, list[Any]] = ("", [])
431
- ) -> list[SessionRow]:
432
+ def search_session(self, conditions: list[SearchCondition] = []) -> list[SessionRow]:
432
433
  """Search for sessions matching the given conditions.
433
434
 
434
435
  Args:
435
- where_tuple
436
+ conditions: List of SearchCondition tuples for filtering sessions.
437
+ Column names should be from the sessions table. If no table prefix
438
+ is given (e.g., "OBJECT"), it will be prefixed with "s." automatically.
436
439
 
437
440
  Returns:
438
- List of matching session records with metadata from the reference image
441
+ List of matching session records with metadata from the reference image and repo_url
439
442
  """
440
- # Build WHERE clause dynamically based on conditions
441
- where_clause, params = where_tuple
443
+ # Build WHERE clause from SearchCondition list
444
+ where_clauses = []
445
+ params = []
446
+
447
+ for condition in conditions:
448
+ # Add table prefix 's.' if not already present to avoid ambiguous column names
449
+ column_name = condition.column_name
450
+ if "." not in column_name:
451
+ # Session table columns that might be ambiguous with images table
452
+ column_name = f"s.{column_name.lower()}"
453
+ where_clauses.append(f"{column_name} {condition.comparison_op} ?")
454
+ params.append(condition.value)
455
+
456
+ # Build the query with JOIN to images and repos tables to get reference image metadata and repo_url
457
+ where_clause = ""
458
+ if where_clauses:
459
+ where_clause = " WHERE " + " AND ".join(where_clauses)
442
460
 
443
- # Build the query with JOIN to images table to get reference image metadata
444
461
  query = f"""
445
462
  SELECT s.id, s.start, s.end, s.filter, s.imagetyp, s.object, s.telescop,
446
- s.num_images, s.exptime_total, s.exptime, s.image_doc_id, i.metadata
463
+ s.num_images, s.exptime_total, s.exptime, s.image_doc_id, i.metadata, r.url as repo_url
447
464
  FROM {self.SESSIONS_TABLE} s
448
465
  LEFT JOIN {self.IMAGES_TABLE} i ON s.image_doc_id = i.id
466
+ LEFT JOIN {self.REPOS_TABLE} r ON i.repo_id = r.id
449
467
  {where_clause}
450
468
  """
451
469
 
@@ -684,9 +702,7 @@ class Database:
684
702
 
685
703
  return dict(row)
686
704
 
687
- def upsert_session(
688
- self, new: SessionRow, existing: SessionRow | None = None
689
- ) -> None:
705
+ def upsert_session(self, new: SessionRow, existing: SessionRow | None = None) -> None:
690
706
  """Insert or update a session record."""
691
707
  cursor = self._db.cursor()
692
708
 
@@ -734,9 +750,7 @@ class Database:
734
750
  new[get_column_name(Database.END_KEY)],
735
751
  new.get(get_column_name(Database.FILTER_KEY)),
736
752
  new[get_column_name(Database.IMAGETYP_KEY)],
737
- normalize_target_name(
738
- new.get(get_column_name(Database.OBJECT_KEY))
739
- ),
753
+ normalize_target_name(new.get(get_column_name(Database.OBJECT_KEY))),
740
754
  new.get(get_column_name(Database.TELESCOP_KEY)),
741
755
  new[get_column_name(Database.NUM_IMAGES_KEY)],
742
756
  new[get_column_name(Database.EXPTIME_TOTAL_KEY)],
@@ -752,7 +766,7 @@ class Database:
752
766
  self._db.close()
753
767
 
754
768
  # Context manager support
755
- def __enter__(self) -> "Database":
769
+ def __enter__(self) -> Database:
756
770
  return self
757
771
 
758
772
  def __exit__(self, exc_type, exc, tb) -> None:
@@ -24,6 +24,7 @@ HaOiii = ["HaOiii", "HaO3"]
24
24
  None = ["None"]
25
25
 
26
26
  camera_osc = ["OSC", "ZWO ASI2600MC Duo"]
27
+ camera_seestar = ["Seestar", "Seestar S50", "Seestar S30", "Seestar S30 Pro"]
27
28
 
28
29
  # Passes SII 672.4nm and H-Beta 486.1nm lines
29
30
  # Capturing of the two main emission wavebands in the deep red and blue at the same time
starbash/exception.py ADDED
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+
3
+
4
+ class UserHandledError(ValueError):
5
+ """An exception that terminates processing of the current file, but we want to help the user fix the problem."""
6
+
7
+ def ask_user_handled(self) -> bool:
8
+ """Prompt the user with a friendly message about the error.
9
+ Returns:
10
+ True if the error was handled, False otherwise.
11
+ """
12
+ from starbash import console # Lazy import to avoid circular dependency
13
+
14
+ console.print(f"Error: {self}")
15
+ return False
16
+
17
+ def __rich__(self) -> Any:
18
+ return self.__str__() # At least this is something readable...
19
+
20
+
21
+ __all__ = ["UserHandledError"]
starbash/main.py CHANGED
@@ -1,13 +1,24 @@
1
1
  import logging
2
+ import warnings
3
+ from typing import Annotated
4
+
2
5
  import typer
3
- from typing_extensions import Annotated
4
6
 
5
- import starbash.url as url
6
7
  import starbash
8
+ import starbash.url as url
7
9
 
8
- from .app import Starbash, get_user_config_path, setup_logging
9
- from .commands import info, process, repo, select, user
10
10
  from . import console
11
+ from .analytics import is_development_environment
12
+ from .app import Starbash
13
+ from .commands import info, process, repo, select, user
14
+ from .paths import get_user_config_path
15
+
16
+ # Suppress deprecation warnings in production mode to provide a cleaner user experience.
17
+ # In development mode (VS Code, devcontainer, or SENTRY_ENVIRONMENT=development),
18
+ # all warnings are shown to help developers identify potential issues.
19
+ # See: is_development_environment() in analytics.py for detection logic.
20
+ if not is_development_environment():
21
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
11
22
 
12
23
  app = typer.Typer(
13
24
  rich_markup_mode="rich",
@@ -17,9 +28,7 @@ app.add_typer(user.app, name="user", help="Manage user settings.")
17
28
  app.add_typer(repo.app, name="repo", help="Manage Starbash repositories.")
18
29
  app.add_typer(select.app, name="select", help="Manage session and target selection.")
19
30
  app.add_typer(info.app, name="info", help="Display system and data information.")
20
- app.add_typer(
21
- process.app, name="process", help="Process images using automated workflows."
22
- )
31
+ app.add_typer(process.app, name="process", help="Process images using automated workflows.")
23
32
 
24
33
 
25
34
  @app.callback(invoke_without_command=True)
@@ -32,11 +41,24 @@ def main_callback(
32
41
  help="Enable debug logging output.",
33
42
  ),
34
43
  ] = False,
44
+ force: bool = typer.Option(
45
+ default=False,
46
+ help="Force reindexing/output file regeneration - even if unchanged.",
47
+ ),
48
+ verbose: bool = typer.Option(
49
+ False,
50
+ "--verbose",
51
+ help="When providing responses, include all entries. Normally long responses are truncated.",
52
+ ),
35
53
  ):
36
54
  """Main callback for the Starbash application."""
37
55
  # Set the log level based on --debug flag
38
56
  if debug:
39
57
  starbash.log_filter_level = logging.DEBUG
58
+ if force:
59
+ starbash.force_regen = True
60
+ if verbose:
61
+ starbash.verbose_output = True
40
62
 
41
63
  if ctx.invoked_subcommand is None:
42
64
  if not get_user_config_path().exists():
starbash/paths.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from pathlib import Path
3
+
3
4
  from platformdirs import PlatformDirs
4
5
 
5
6
  app_name = "starbash"
@@ -7,35 +8,45 @@ app_author = "geeksville"
7
8
  dirs = PlatformDirs(app_name, app_author)
8
9
  config_dir = Path(dirs.user_config_dir)
9
10
  data_dir = Path(dirs.user_data_dir)
11
+ cache_dir = Path(dirs.user_cache_dir)
10
12
  documents_dir = Path(dirs.user_documents_dir) / "starbash"
11
13
 
12
14
  # These can be overridden for testing
13
15
  _override_config_dir: Path | None = None
14
16
  _override_data_dir: Path | None = None
17
+ _override_cache_dir: Path | None = None
15
18
  _override_documents_dir: Path | None = None
16
19
 
17
20
 
18
21
  def set_test_directories(
19
22
  config_dir_override: Path | None = None,
20
23
  data_dir_override: Path | None = None,
24
+ cache_dir_override: Path | None = None,
21
25
  documents_dir_override: Path | None = None,
22
26
  ) -> None:
23
27
  """Set override directories for testing. Used by test fixtures to isolate test data."""
24
- global _override_config_dir, _override_data_dir, _override_documents_dir
28
+ global _override_config_dir, _override_data_dir, _override_cache_dir, _override_documents_dir
25
29
  _override_config_dir = config_dir_override
26
30
  _override_data_dir = data_dir_override
31
+ _override_cache_dir = cache_dir_override
27
32
  _override_documents_dir = documents_dir_override
28
33
 
29
34
 
30
35
  def get_user_config_dir() -> Path:
31
36
  """Get the user config directory. Returns test override if set, otherwise the real user directory."""
32
- dir_to_use = (
33
- _override_config_dir if _override_config_dir is not None else config_dir
34
- )
37
+ dir_to_use = _override_config_dir if _override_config_dir is not None else config_dir
35
38
  os.makedirs(dir_to_use, exist_ok=True)
36
39
  return dir_to_use
37
40
 
38
41
 
42
+ def get_user_config_path() -> Path:
43
+ """Returns the path to the user config file (starbash.toml)."""
44
+ from repo import repo_suffix # Lazy import to avoid circular dependency
45
+
46
+ config_dir = get_user_config_dir()
47
+ return config_dir / repo_suffix
48
+
49
+
39
50
  def get_user_data_dir() -> Path:
40
51
  """Get the user data directory. Returns test override if set, otherwise the real user directory."""
41
52
  dir_to_use = _override_data_dir if _override_data_dir is not None else data_dir
@@ -43,12 +54,15 @@ def get_user_data_dir() -> Path:
43
54
  return dir_to_use
44
55
 
45
56
 
57
+ def get_user_cache_dir() -> Path:
58
+ """Get the user cache directory. Returns test override if set, otherwise the real user directory."""
59
+ dir_to_use = _override_cache_dir if _override_cache_dir is not None else cache_dir
60
+ os.makedirs(dir_to_use, exist_ok=True)
61
+ return dir_to_use
62
+
63
+
46
64
  def get_user_documents_dir() -> Path:
47
65
  """Get the user documents directory. Returns test override if set, otherwise the real user directory."""
48
- dir_to_use = (
49
- _override_documents_dir
50
- if _override_documents_dir is not None
51
- else documents_dir
52
- )
66
+ dir_to_use = _override_documents_dir if _override_documents_dir is not None else documents_dir
53
67
  os.makedirs(dir_to_use, exist_ok=True)
54
68
  return dir_to_use