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.
- repo/__init__.py +1 -1
- repo/manager.py +14 -23
- repo/repo.py +52 -10
- starbash/__init__.py +10 -3
- starbash/aliases.py +145 -0
- starbash/analytics.py +3 -2
- starbash/app.py +512 -473
- starbash/check_version.py +18 -0
- starbash/commands/__init__.py +2 -1
- starbash/commands/info.py +88 -14
- starbash/commands/process.py +76 -24
- starbash/commands/repo.py +41 -68
- starbash/commands/select.py +141 -142
- starbash/commands/user.py +88 -23
- starbash/database.py +219 -112
- starbash/defaults/starbash.toml +24 -3
- starbash/exception.py +21 -0
- starbash/main.py +29 -7
- starbash/paths.py +35 -5
- starbash/processing.py +724 -0
- starbash/recipes/README.md +3 -0
- starbash/recipes/master_bias/starbash.toml +16 -19
- starbash/recipes/master_dark/starbash.toml +33 -0
- starbash/recipes/master_flat/starbash.toml +26 -18
- starbash/recipes/osc.py +190 -0
- starbash/recipes/osc_dual_duo/starbash.toml +54 -44
- starbash/recipes/osc_simple/starbash.toml +82 -0
- starbash/recipes/osc_single_duo/starbash.toml +51 -32
- starbash/recipes/seestar/starbash.toml +82 -0
- starbash/recipes/starbash.toml +30 -9
- starbash/selection.py +32 -36
- starbash/templates/repo/master.toml +7 -3
- starbash/templates/repo/processed.toml +15 -0
- starbash/templates/userconfig.toml +9 -0
- starbash/toml.py +13 -13
- starbash/tool.py +230 -96
- starbash-0.1.15.dist-info/METADATA +216 -0
- starbash-0.1.15.dist-info/RECORD +45 -0
- starbash/recipes/osc_dual_duo/starbash.py +0 -151
- starbash-0.1.9.dist-info/METADATA +0 -145
- starbash-0.1.9.dist-info/RECORD +0 -37
- {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/WHEEL +0 -0
- {starbash-0.1.9.dist-info → starbash-0.1.15.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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__ = ["
|
|
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
|
|
8
|
+
from starbash import _is_test_env
|
|
8
9
|
|
|
9
10
|
# Default to no analytics/auto crash reports
|
|
10
11
|
analytics_allowed = False
|