starbash 0.1.9__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 (44) 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 +145 -0
  6. starbash/analytics.py +3 -2
  7. starbash/app.py +512 -473
  8. starbash/check_version.py +18 -0
  9. starbash/commands/__init__.py +2 -1
  10. starbash/commands/info.py +88 -14
  11. starbash/commands/process.py +76 -24
  12. starbash/commands/repo.py +41 -68
  13. starbash/commands/select.py +141 -142
  14. starbash/commands/user.py +88 -23
  15. starbash/database.py +219 -112
  16. starbash/defaults/starbash.toml +24 -3
  17. starbash/exception.py +21 -0
  18. starbash/main.py +29 -7
  19. starbash/paths.py +35 -5
  20. starbash/processing.py +724 -0
  21. starbash/recipes/README.md +3 -0
  22. starbash/recipes/master_bias/starbash.toml +16 -19
  23. starbash/recipes/master_dark/starbash.toml +33 -0
  24. starbash/recipes/master_flat/starbash.toml +26 -18
  25. starbash/recipes/osc.py +190 -0
  26. starbash/recipes/osc_dual_duo/starbash.toml +54 -44
  27. starbash/recipes/osc_simple/starbash.toml +82 -0
  28. starbash/recipes/osc_single_duo/starbash.toml +51 -32
  29. starbash/recipes/seestar/starbash.toml +82 -0
  30. starbash/recipes/starbash.toml +30 -9
  31. starbash/selection.py +32 -36
  32. starbash/templates/repo/master.toml +7 -3
  33. starbash/templates/repo/processed.toml +15 -0
  34. starbash/templates/userconfig.toml +9 -0
  35. starbash/toml.py +13 -13
  36. starbash/tool.py +230 -96
  37. starbash-0.1.15.dist-info/METADATA +216 -0
  38. starbash-0.1.15.dist-info/RECORD +45 -0
  39. starbash/recipes/osc_dual_duo/starbash.py +0 -151
  40. starbash-0.1.9.dist-info/METADATA +0 -145
  41. starbash-0.1.9.dist-info/RECORD +0 -37
  42. {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/WHEEL +0 -0
  43. {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/entry_points.txt +0 -0
  44. {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/licenses/LICENSE +0 -0
repo/__init__.py CHANGED
@@ -3,6 +3,6 @@ The repo package handles finding, loading and searching starbash repositories.
3
3
  """
4
4
 
5
5
  from .manager import RepoManager
6
- from .repo import Repo, repo_suffix, REPO_REF
6
+ from .repo import REPO_REF, Repo, repo_suffix
7
7
 
8
8
  __all__ = ["RepoManager", "Repo", "repo_suffix", "REPO_REF"]
repo/manager.py CHANGED
@@ -2,17 +2,19 @@
2
2
  Manages the repository of processing recipes and configurations.
3
3
  """
4
4
 
5
+ # pyright: reportImportCycles=false
6
+ # The circular dependency between manager.py and repo.py is properly handled
7
+ # using TYPE_CHECKING and local imports where needed.
8
+
5
9
  from __future__ import annotations
10
+
6
11
  import logging
7
- from pathlib import Path
8
- from importlib import resources
9
- from typing import Any
12
+ from typing import TYPE_CHECKING
10
13
 
11
- import tomlkit
12
- from tomlkit.toml_file import TOMLFile
13
- from tomlkit.items import AoT
14
14
  from multidict import MultiDict
15
- from repo.repo import Repo
15
+
16
+ if TYPE_CHECKING:
17
+ from repo.repo import Repo
16
18
 
17
19
 
18
20
  class RepoManager:
@@ -28,7 +30,7 @@ class RepoManager:
28
30
  """
29
31
  Initializes the RepoManager by loading the application default repos.
30
32
  """
31
- self.repos = []
33
+ self.repos: list[Repo] = []
32
34
 
33
35
  # We expose the app default preferences as a special root repo with a private URL
34
36
  # root_repo = Repo(self, "pkg://starbash-defaults", config=app_defaults)
@@ -40,13 +42,11 @@ class RepoManager:
40
42
  @property
41
43
  def regular_repos(self) -> list[Repo]:
42
44
  "We exclude certain repo types (preferences, recipe) from the list of repos users care about."
43
- return [
44
- r
45
- for r in self.repos
46
- if r.kind() not in ("preferences") and not r.is_scheme("pkg")
47
- ]
45
+ return [r for r in self.repos if r.kind() not in ("preferences") and not r.is_scheme("pkg")]
48
46
 
49
47
  def add_repo(self, url: str) -> Repo:
48
+ from repo.repo import Repo # Local import to avoid circular dependency
49
+
50
50
  logging.debug(f"Adding repo: {url}")
51
51
  r = Repo(self, url)
52
52
  self.repos.append(r)
@@ -122,19 +122,10 @@ class RepoManager:
122
122
  for key, value in combined_config.items():
123
123
  # tomlkit.items() can return complex types (e.g., ArrayOfTables, Table)
124
124
  # For a debug dump, a simple string representation is usually sufficient.
125
- logging.info(f" %s: %s", key, value)
125
+ logging.info(" %s: %s", key, value)
126
126
 
127
127
  def _add_merged(self, repo: Repo) -> None:
128
128
  for key, value in repo.config.items():
129
- # if the toml object is an AoT type, monkey patch each element in the array instead
130
- if isinstance(value, AoT):
131
- for v in value:
132
- setattr(v, "source", repo)
133
- else:
134
- # We monkey patch source into any object that came from a repo, so that users can
135
- # find the source repo (for attribution, URL relative resolution, whatever...)
136
- setattr(value, "source", repo)
137
-
138
129
  self.merged.add(key, value)
139
130
 
140
131
  def __str__(self):
repo/repo.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from __future__ import annotations
2
+
2
3
  import logging
3
- from pathlib import Path
4
4
  from importlib import resources
5
- from typing import Any, TYPE_CHECKING
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
6
7
 
7
8
  import tomlkit
8
- from tomlkit.toml_file import TOMLFile
9
9
  from tomlkit.items import AoT
10
- from multidict import MultiDict
10
+ from tomlkit.toml_file import TOMLFile
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from repo.manager import RepoManager
@@ -31,6 +31,36 @@ class Repo:
31
31
  self.manager = manager
32
32
  self.url = url
33
33
  self.config = self._load_config()
34
+ self._monkey_patch()
35
+
36
+ def _monkey_patch(self, o: Any | None = None) -> None:
37
+ """Add a 'source' back-ptr to all child items in the config.
38
+
39
+ so that users can find the source repo (for attribution, URL relative resolution, whatever...)
40
+ """
41
+ # base case - start us recursing
42
+ if o is None:
43
+ self._monkey_patch(self.config)
44
+ return
45
+
46
+ # We monkey patch source into any object that came from a repo,
47
+ try:
48
+ o.source = self
49
+
50
+ # Recursively patch dict-like objects
51
+ if isinstance(o, dict):
52
+ for value in o.values():
53
+ self._monkey_patch(value)
54
+ # Recursively patch list-like objects (including AoT)
55
+ elif hasattr(o, "__iter__") and not isinstance(o, str | bytes):
56
+ try:
57
+ for item in o:
58
+ self._monkey_patch(item)
59
+ except TypeError:
60
+ # Not actually iterable, skip
61
+ pass
62
+ except AttributeError:
63
+ pass # simple types like int, str, float, etc. can't have attributes set on them
34
64
 
35
65
  def __str__(self) -> str:
36
66
  """Return a concise one-line description of this repo.
@@ -230,12 +260,7 @@ class Repo:
230
260
  A dictionary containing the parsed configuration.
231
261
  """
232
262
  try:
233
- if self.is_scheme("file"):
234
- config_content = self._read_file(repo_suffix)
235
- elif self.is_scheme("pkg"):
236
- config_content = self._read_resource(repo_suffix)
237
- else:
238
- raise ValueError(f"Unsupported URL scheme for repo: {self.url}")
263
+ config_content = self.read(repo_suffix)
239
264
  logging.debug(f"Loading repo config from {repo_suffix}")
240
265
  return tomlkit.parse(config_content)
241
266
  except FileNotFoundError:
@@ -244,6 +269,23 @@ class Repo:
244
269
  ) # we currently make it optional to have the config file at root
245
270
  return tomlkit.TOMLDocument() # empty placeholder
246
271
 
272
+ def read(self, filepath: str) -> str:
273
+ """
274
+ Read a filepath relative to the base of this repo. Return the contents in a string.
275
+
276
+ Args:
277
+ filepath: The path to the file, relative to the repository root.
278
+
279
+ Returns:
280
+ The content of the file as a string.
281
+ """
282
+ if self.is_scheme("file"):
283
+ return self._read_file(filepath)
284
+ elif self.is_scheme("pkg"):
285
+ return self._read_resource(filepath)
286
+ else:
287
+ raise ValueError(f"Unsupported URL scheme for repo: {self.url}")
288
+
247
289
  def get(self, key: str, default: Any | None = None) -> Any | None:
248
290
  """
249
291
  Gets a value from this repo's config for a given key.
starbash/__init__.py CHANGED
@@ -1,14 +1,15 @@
1
- import datetime
2
1
  import logging
3
2
  import os
4
3
  from datetime import datetime
5
4
 
6
- from .database import Database # re-export for convenience
7
5
  from rich.console import Console
8
6
 
9
7
  # Disable Rich formatting in test environments (pytest or NO_COLOR set)
10
8
  # This prevents ANSI escape codes and line wrapping in test output for more reliable test parsing.
11
9
  _is_test_env = "PYTEST_VERSION" in os.environ
10
+
11
+ # Note: this console instance is probably never used - because the Starbash constructor slams in a new version into
12
+ # this global.
12
13
  console = Console(
13
14
  force_terminal=False if _is_test_env else None,
14
15
  width=999999 if _is_test_env else None, # Disable line wrapping in tests
@@ -17,6 +18,12 @@ console = Console(
17
18
  # Global variable for log filter level (can be changed via --debug flag)
18
19
  log_filter_level = logging.INFO
19
20
 
21
+ # Global variable for forcing some file generation
22
+ force_regen = False
23
+
24
+ # Show extra command output
25
+ verbose_output = False
26
+
20
27
 
21
28
  def to_shortdate(date_iso: str) -> str:
22
29
  """Convert ISO UTC datetime string to local short date string (YYYY-MM-DD).
@@ -35,4 +42,4 @@ def to_shortdate(date_iso: str) -> str:
35
42
  return date_iso
36
43
 
37
44
 
38
- __all__ = ["Database"]
45
+ __all__ = ["console", "to_shortdate", "log_filter_level", "force_regen", "verbose_output"]
starbash/aliases.py ADDED
@@ -0,0 +1,145 @@
1
+ import logging
2
+ import string
3
+ from textwrap import dedent
4
+ from typing import overload
5
+
6
+ from starbash.exception import UserHandledError
7
+
8
+ _translator = str.maketrans("", "", string.punctuation + string.whitespace)
9
+
10
+ __all__ = [
11
+ "Aliases",
12
+ "UnrecognizedAliasError",
13
+ "normalize_target_name",
14
+ "pre_normalize",
15
+ ]
16
+
17
+
18
+ @overload
19
+ def normalize_target_name(name: str) -> str: ...
20
+
21
+
22
+ @overload
23
+ def normalize_target_name(name: None) -> None: ...
24
+
25
+
26
+ def normalize_target_name(name: str | None) -> str | None:
27
+ """Converts a target name to an any filesystem-safe format by removing spaces"""
28
+ if name is None:
29
+ return None
30
+ return name.replace(" ", "").lower()
31
+
32
+
33
+ def pre_normalize(name: str) -> str:
34
+ """Pre-normalize a name by removing all whitespace and punctuation, and converting to lowercase.
35
+
36
+ Args:
37
+ name: The name to pre-normalize.
38
+
39
+ Returns:
40
+ Normalized string with only alphanumeric characters in lowercase.
41
+ """
42
+ # Create translation table that removes all punctuation and whitespace
43
+ return name.lower().translate(_translator)
44
+
45
+
46
+ class UnrecognizedAliasError(UserHandledError):
47
+ """Exception raised when an unrecognized alias is encountered during normalization."""
48
+
49
+ def __init__(self, message: str, alias: str):
50
+ super().__init__(message)
51
+ self.alias = alias
52
+
53
+ def ask_user_handled(self) -> bool:
54
+ from starbash import console # Lazy import to avoid circular dependency
55
+ from starbash.paths import (
56
+ get_user_config_path,
57
+ ) # Moved to paths module to avoid circular dependency
58
+
59
+ console.print(
60
+ dedent(
61
+ f"""{self.__rich__()}
62
+
63
+ For the time being that means editing {get_user_config_path() / "starbash.toml"}
64
+ (FIXME - we'll eventually provide an interactive picker here...)
65
+ """
66
+ )
67
+ )
68
+ return True
69
+
70
+ def __rich__(self) -> str:
71
+ return f"[red]Error:[/red] To process this session you need to add a missing alias for '[red]{self.alias}[/red]'."
72
+
73
+
74
+ class Aliases:
75
+ def __init__(self, alias_dict: dict[str, list[str]]):
76
+ """Initialize the Aliases object with a dictionary mapping keys to their alias lists.
77
+
78
+ The alias_dict structure follows the TOML format:
79
+ - Keys are reference names used in code (e.g., "dark", "flat", "bias", "fits", "SiiOiii", "HaOiii")
80
+ - Values are lists of aliases where the FIRST item is the canonical/preferred name
81
+ - The dictionary key may or may not match the canonical name
82
+
83
+ Example from TOML:
84
+ [aliases]
85
+ dark = ["dark", "darks"] # key "dark" -> canonical "dark"
86
+ flat = ["flat", "flats"] # key "flat" -> canonical "flat"
87
+ SiiOiii = ["SiiOiii", "SII-OIII", "S2-O3"] # key "SiiOiii" -> canonical "SiiOiii"
88
+ """
89
+ self.alias_dict = alias_dict
90
+ self.reverse_dict = {}
91
+
92
+ # Build reverse lookup: any alias variant maps to canonical name
93
+ for _key, aliases in alias_dict.items():
94
+ if not aliases:
95
+ continue
96
+ # The first item in the list is ALWAYS the canonical/preferred form
97
+ canonical = aliases[0]
98
+ for alias in aliases:
99
+ # Map each alias (case-insensitive) to the canonical form (first in list)
100
+ # Also remove spaces, hypens and underscores when matching for normalization
101
+ self.reverse_dict[pre_normalize(alias)] = canonical
102
+
103
+ def get(self, name: str) -> list[str] | None:
104
+ """Get the list of aliases for a given key name.
105
+
106
+ Args:
107
+ name: The key name to look up (as used in code/TOML)
108
+
109
+ Returns:
110
+ List of all aliases for this key, or None if not found.
111
+ The first item in the returned list is the canonical form.
112
+ """
113
+ return self.alias_dict.get(name)
114
+
115
+ def normalize(self, name: str, lenient: bool = False) -> str:
116
+ """Normalize a name to its canonical form using aliases.
117
+
118
+ This performs case-insensitive matching to find the canonical form.
119
+ The canonical form is the first item in the alias list from the TOML.
120
+
121
+ Args:
122
+ name: The name to normalize (e.g., "darks", "FLAT", "HA-OIII")
123
+ lenient: If True, return unconverted names if not found
124
+
125
+ Returns:
126
+ The canonical/preferred form (e.g., "dark", "flat", "HaOiii"), or None if not found
127
+
128
+ Examples:
129
+ normalize("darks") -> "dark"
130
+ normalize("FLAT") -> "flat"
131
+ normalize("HA-OIII") -> "HaOiii"
132
+ """
133
+ result = self.reverse_dict.get(pre_normalize(name))
134
+ if not result:
135
+ if lenient:
136
+ logging.warning(f"Unrecognized alias '{name}' encountered, using unconverted.")
137
+ return name
138
+ raise UnrecognizedAliasError(f"'{name}' not found in aliases.", name)
139
+ return result
140
+
141
+ def equals(self, name1: str, name2: str) -> bool:
142
+ """Check if two names are equivalent based on aliases."""
143
+ norm1 = self.normalize(name1.strip())
144
+ norm2 = self.normalize(name2.strip())
145
+ return norm1 == norm2
starbash/analytics.py CHANGED
@@ -1,10 +1,11 @@
1
1
  import logging
2
2
  import os
3
3
 
4
+ from sentry_sdk.integrations.excepthook import ExcepthookIntegration
5
+
4
6
  import starbash
5
- from starbash import console, _is_test_env
6
7
  import starbash.url as url
7
- from sentry_sdk.integrations.excepthook import ExcepthookIntegration
8
+ from starbash import _is_test_env
8
9
 
9
10
  # Default to no analytics/auto crash reports
10
11
  analytics_allowed = False