starbash 0.1.7__tar.gz → 0.1.9__tar.gz

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.

Files changed (38) hide show
  1. {starbash-0.1.7 → starbash-0.1.9}/PKG-INFO +18 -13
  2. {starbash-0.1.7 → starbash-0.1.9}/README.md +18 -13
  3. {starbash-0.1.7 → starbash-0.1.9}/pyproject.toml +5 -2
  4. starbash-0.1.9/src/repo/__init__.py +8 -0
  5. starbash-0.1.9/src/repo/manager.py +144 -0
  6. starbash-0.1.9/src/repo/repo.py +294 -0
  7. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/__init__.py +20 -0
  8. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/analytics.py +4 -0
  9. starbash-0.1.9/src/starbash/app.py +830 -0
  10. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/commands/__init__.py +0 -17
  11. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/commands/info.py +3 -3
  12. starbash-0.1.9/src/starbash/commands/process.py +154 -0
  13. starbash-0.1.9/src/starbash/commands/repo.py +231 -0
  14. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/commands/select.py +128 -44
  15. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/database.py +237 -88
  16. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/defaults/starbash.toml +17 -0
  17. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/main.py +4 -1
  18. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/recipes/master_bias/starbash.toml +25 -8
  19. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/recipes/starbash.toml +5 -0
  20. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/selection.py +109 -45
  21. starbash-0.1.9/src/starbash/templates/repo/master.toml +13 -0
  22. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/templates/userconfig.toml +1 -1
  23. starbash-0.1.9/src/starbash/toml.py +29 -0
  24. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/tool.py +84 -12
  25. starbash-0.1.7/src/starbash/app.py +0 -464
  26. starbash-0.1.7/src/starbash/commands/repo.py +0 -140
  27. {starbash-0.1.7 → starbash-0.1.9}/LICENSE +0 -0
  28. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/commands/user.py +0 -0
  29. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/defaults/__init__.py +0 -0
  30. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/paths.py +0 -0
  31. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/recipes/README.md +0 -0
  32. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/recipes/__init__.py +0 -0
  33. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/recipes/master_flat/starbash.toml +0 -0
  34. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/recipes/osc_dual_duo/starbash.py +0 -0
  35. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/recipes/osc_dual_duo/starbash.toml +0 -0
  36. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/recipes/osc_single_duo/starbash.toml +0 -0
  37. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/templates/__init__.py +0 -0
  38. {starbash-0.1.7 → starbash-0.1.9}/src/starbash/url.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: starbash
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: A tool for automating/standardizing/sharing astrophotography workflows.
5
5
  License-File: LICENSE
6
6
  Author: Kevin Hester
@@ -20,7 +20,7 @@ Requires-Dist: tomlkit (>=0.13.3,<0.14.0)
20
20
  Requires-Dist: typer (>=0.20.0,<0.21.0)
21
21
  Description-Content-Type: text/markdown
22
22
 
23
- # Starbash
23
+ # [Starbash](https://github.com/geeksville/starbash)
24
24
 
25
25
  <img src="https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/img/icon.png" alt="Starbash: Astrophotography workflows simplified" width="30%" align="right" style="margin-bottom: 20px;">
26
26
 
@@ -67,7 +67,7 @@ See the current [TODO](TODO.md) file for work items. I'll be looking for pre-al
67
67
 
68
68
  Currently the easiest way to install this command-line based tool is to install is via [pipx](https://pipx.pypa.io/stable/). If you don't already have pipx and you have python installed, you can auto install it by running "pip install --user pipx." If you don't have python installed see the pipx link for pipx installers for any OS.
69
69
 
70
- Once pipx is installed just run the following **two** commands (the sb --install-completion will make TAB auto-complete automatically complete sb options (for most platforms)):
70
+ Once pipx is installed just run the following **two** commands (the sb --install-completion will make TAB auto-complete automatically complete sb options (for most platforms)). Installing auto-complete is **highly** recommended because it makes entering starbash commands fast - by pressing the TAB key:
71
71
 
72
72
  ```
73
73
  ➜ pipx install starbash
@@ -88,10 +88,10 @@ FIXME - add getting started instructions (possibly with a screenshare video)
88
88
  ## Supported commands
89
89
 
90
90
  ### Repository Management
91
- - `sb repo [--verbose]` - List installed repos (use `-v` for details)
92
- - `sb repo add <filepath|URL>` - Add a repository
93
- - `sb repo remove <REPONUM>` - Remove the indicated repo from the repo list
94
- - `sb repo reindex [--force] [REPONUM]` - Reindex the specified repo (or all repos if none specified)
91
+ - `sb repo list [--verbose]` - List installed repos (use `-v` for details)
92
+ - `sb repo add [--master] <filepath|URL>` - Add a repository, optionally specifying the type
93
+ - `sb repo remove <REPOURL>` - Remove the indicated repo from the repo list
94
+ - `sb repo reindex [--force] <REPOURL>` - Reindex the specified repo (or all repos if none specified)
95
95
 
96
96
  ### User Preferences
97
97
  - `sb user name "Your Name"` - Set name for attribution in generated images
@@ -108,18 +108,18 @@ FIXME - add getting started instructions (possibly with a screenshare video)
108
108
  - `sb select date <after|before|between> <DATE> [DATE]` - Limit to sessions in the specified date range
109
109
  - `sb select export SESSIONNUM DESTDIR` - Export the images for indicated session number into the specified directory (or current directory if not specified). If possible symbolic links are used, if not the files are copied.
110
110
 
111
- ## Not yet supported commands
112
-
113
- ### Setup & Configuration
111
+ ### Selection information
114
112
  - `sb info` - Show user preferences location and other app info
115
113
  - `sb info target` - List targets (filtered based on the current selection)
116
114
  - `sb info telescope` - List instruments (filtered based on the current selection)
117
115
  - `sb info filter` - List all filters found in current selection
118
116
 
117
+ ## Not yet supported commands
118
+
119
119
  ### Export & Processing
120
- - `sb process siril` - Generate Siril directory tree and run Siril GUI
121
- - `sb process auto` - Automatic processing
122
- - `sb process masters` - Generate master flats, darks, and biases from available raw frames
120
+ - `sb process siril [--run] SESSIONNUM DESTDIR` - Generate Siril directory tree and optionally run Siril GUI
121
+ - `sb process auto [SESSIONNUM]` - Automatic processing. If session # is specified process only that session, otherwise all selected sessions will be processed
122
+ - `sb process masters` - Generate master flats, darks, and biases from available raw frames in the current selection
123
123
 
124
124
  ## Supported tools (now)
125
125
 
@@ -138,3 +138,8 @@ We try to make this project useful and friendly. If you find problems please fi
138
138
  We accept pull-requests and enjoy discussing possible new development directions via github issues. If you might want to work on this, just describe what your interests are and we can talk about how to get it merged.
139
139
 
140
140
  Project members can access crash reports [here](https://geeksville.sentry.io/insights/projects/starbash/?project=4510264204132352).
141
+
142
+ ## License
143
+
144
+ Copyright 2025 Kevin Hester, kevinh@geeksville.com.
145
+ Licensed under the (GPL v3)[LICENSE]
@@ -1,4 +1,4 @@
1
- # Starbash
1
+ # [Starbash](https://github.com/geeksville/starbash)
2
2
 
3
3
  <img src="https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/img/icon.png" alt="Starbash: Astrophotography workflows simplified" width="30%" align="right" style="margin-bottom: 20px;">
4
4
 
@@ -45,7 +45,7 @@ See the current [TODO](TODO.md) file for work items. I'll be looking for pre-al
45
45
 
46
46
  Currently the easiest way to install this command-line based tool is to install is via [pipx](https://pipx.pypa.io/stable/). If you don't already have pipx and you have python installed, you can auto install it by running "pip install --user pipx." If you don't have python installed see the pipx link for pipx installers for any OS.
47
47
 
48
- Once pipx is installed just run the following **two** commands (the sb --install-completion will make TAB auto-complete automatically complete sb options (for most platforms)):
48
+ Once pipx is installed just run the following **two** commands (the sb --install-completion will make TAB auto-complete automatically complete sb options (for most platforms)). Installing auto-complete is **highly** recommended because it makes entering starbash commands fast - by pressing the TAB key:
49
49
 
50
50
  ```
51
51
  ➜ pipx install starbash
@@ -66,10 +66,10 @@ FIXME - add getting started instructions (possibly with a screenshare video)
66
66
  ## Supported commands
67
67
 
68
68
  ### Repository Management
69
- - `sb repo [--verbose]` - List installed repos (use `-v` for details)
70
- - `sb repo add <filepath|URL>` - Add a repository
71
- - `sb repo remove <REPONUM>` - Remove the indicated repo from the repo list
72
- - `sb repo reindex [--force] [REPONUM]` - Reindex the specified repo (or all repos if none specified)
69
+ - `sb repo list [--verbose]` - List installed repos (use `-v` for details)
70
+ - `sb repo add [--master] <filepath|URL>` - Add a repository, optionally specifying the type
71
+ - `sb repo remove <REPOURL>` - Remove the indicated repo from the repo list
72
+ - `sb repo reindex [--force] <REPOURL>` - Reindex the specified repo (or all repos if none specified)
73
73
 
74
74
  ### User Preferences
75
75
  - `sb user name "Your Name"` - Set name for attribution in generated images
@@ -86,18 +86,18 @@ FIXME - add getting started instructions (possibly with a screenshare video)
86
86
  - `sb select date <after|before|between> <DATE> [DATE]` - Limit to sessions in the specified date range
87
87
  - `sb select export SESSIONNUM DESTDIR` - Export the images for indicated session number into the specified directory (or current directory if not specified). If possible symbolic links are used, if not the files are copied.
88
88
 
89
- ## Not yet supported commands
90
-
91
- ### Setup & Configuration
89
+ ### Selection information
92
90
  - `sb info` - Show user preferences location and other app info
93
91
  - `sb info target` - List targets (filtered based on the current selection)
94
92
  - `sb info telescope` - List instruments (filtered based on the current selection)
95
93
  - `sb info filter` - List all filters found in current selection
96
94
 
95
+ ## Not yet supported commands
96
+
97
97
  ### Export & Processing
98
- - `sb process siril` - Generate Siril directory tree and run Siril GUI
99
- - `sb process auto` - Automatic processing
100
- - `sb process masters` - Generate master flats, darks, and biases from available raw frames
98
+ - `sb process siril [--run] SESSIONNUM DESTDIR` - Generate Siril directory tree and optionally run Siril GUI
99
+ - `sb process auto [SESSIONNUM]` - Automatic processing. If session # is specified process only that session, otherwise all selected sessions will be processed
100
+ - `sb process masters` - Generate master flats, darks, and biases from available raw frames in the current selection
101
101
 
102
102
  ## Supported tools (now)
103
103
 
@@ -115,4 +115,9 @@ FIXME - add getting started instructions (possibly with a screenshare video)
115
115
  We try to make this project useful and friendly. If you find problems please file a github issue.
116
116
  We accept pull-requests and enjoy discussing possible new development directions via github issues. If you might want to work on this, just describe what your interests are and we can talk about how to get it merged.
117
117
 
118
- Project members can access crash reports [here](https://geeksville.sentry.io/insights/projects/starbash/?project=4510264204132352).
118
+ Project members can access crash reports [here](https://geeksville.sentry.io/insights/projects/starbash/?project=4510264204132352).
119
+
120
+ ## License
121
+
122
+ Copyright 2025 Kevin Hester, kevinh@geeksville.com.
123
+ Licensed under the (GPL v3)[LICENSE]
@@ -1,10 +1,13 @@
1
1
  [tool.poetry]
2
2
  name = "starbash"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "A tool for automating/standardizing/sharing astrophotography workflows."
5
5
  authors = ["Kevin Hester <kevinh@geeksville.com>"]
6
6
  readme = "README.md"
7
- packages = [{ include = "starbash", from = "src" }]
7
+ packages = [
8
+ { include = "starbash", from = "src" },
9
+ { include = "repo", from = "src" },
10
+ ]
8
11
 
9
12
  [tool.poetry.dependencies]
10
13
  python = ">=3.12,<3.15" # RestrictedPython doesn't yet work on 3.15
@@ -0,0 +1,8 @@
1
+ """
2
+ The repo package handles finding, loading and searching starbash repositories.
3
+ """
4
+
5
+ from .manager import RepoManager
6
+ from .repo import Repo, repo_suffix, REPO_REF
7
+
8
+ __all__ = ["RepoManager", "Repo", "repo_suffix", "REPO_REF"]
@@ -0,0 +1,144 @@
1
+ """
2
+ Manages the repository of processing recipes and configurations.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ import logging
7
+ from pathlib import Path
8
+ from importlib import resources
9
+ from typing import Any
10
+
11
+ import tomlkit
12
+ from tomlkit.toml_file import TOMLFile
13
+ from tomlkit.items import AoT
14
+ from multidict import MultiDict
15
+ from repo.repo import Repo
16
+
17
+
18
+ class RepoManager:
19
+ """
20
+ Manages the collection of starbash repositories.
21
+
22
+ This class is responsible for finding, loading, and providing an API
23
+ for searching through known repositories defined in TOML configuration
24
+ files (like appdefaults.sb.toml).
25
+ """
26
+
27
+ def __init__(self):
28
+ """
29
+ Initializes the RepoManager by loading the application default repos.
30
+ """
31
+ self.repos = []
32
+
33
+ # We expose the app default preferences as a special root repo with a private URL
34
+ # root_repo = Repo(self, "pkg://starbash-defaults", config=app_defaults)
35
+ # self.repos.append(root_repo)
36
+
37
+ # Most users will just want to read from merged
38
+ self.merged = MultiDict()
39
+
40
+ @property
41
+ def regular_repos(self) -> list[Repo]:
42
+ "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
+ ]
48
+
49
+ def add_repo(self, url: str) -> Repo:
50
+ logging.debug(f"Adding repo: {url}")
51
+ r = Repo(self, url)
52
+ self.repos.append(r)
53
+
54
+ # FIXME, generate the merged dict lazily
55
+ self._add_merged(r)
56
+
57
+ # if this new repo has sub-repos, add them too
58
+ r.add_by_repo_refs()
59
+
60
+ return r
61
+
62
+ def get_repo_by_url(self, url: str) -> Repo | None:
63
+ """
64
+ Retrieves a repository by its URL.
65
+
66
+ Args:
67
+ url: The URL of the repository to retrieve.
68
+
69
+ Returns:
70
+ The Repo instance with the matching URL, or None if not found.
71
+ """
72
+ for repo in self.repos:
73
+ if repo.url == url:
74
+ return repo
75
+ return None
76
+
77
+ def get_repo_by_kind(self, kind: str) -> Repo | None:
78
+ """
79
+ Retrieves the first repository matching the specified kind.
80
+
81
+ Args:
82
+ kind: The kind of repository to search for (e.g., "recipe", "preferences").
83
+
84
+ Returns:
85
+ The first Repo instance matching the kind, or None if not found.
86
+ """
87
+ for repo in self.repos:
88
+ if repo.kind() == kind:
89
+ return repo
90
+ return None
91
+
92
+ def get(self, key: str, default=None):
93
+ """
94
+ Searches for a key across all repositories and returns the first value found.
95
+ The search is performed in reverse order of repository loading, so the
96
+ most recently added repositories have precedence.
97
+
98
+ Args:
99
+ key: The dot-separated key to search for (e.g., "repo.kind").
100
+ default: The value to return if the key is not found in any repo.
101
+
102
+ Returns:
103
+ The found value or the default.
104
+ """
105
+ # Iterate in reverse to give precedence to later-loaded repos
106
+ for repo in reversed(self.repos):
107
+ value = repo.get(key)
108
+ if value is not None:
109
+ return value
110
+
111
+ return default
112
+
113
+ def dump(self):
114
+ """
115
+ Prints a detailed, multi-line description of the combined top-level keys
116
+ and values from all repositories, using a MultiDict for aggregation.
117
+ This is useful for debugging and inspecting the consolidated configuration.
118
+ """
119
+
120
+ combined_config = self.merged
121
+ logging.info("RepoManager Dump")
122
+ for key, value in combined_config.items():
123
+ # tomlkit.items() can return complex types (e.g., ArrayOfTables, Table)
124
+ # For a debug dump, a simple string representation is usually sufficient.
125
+ logging.info(f" %s: %s", key, value)
126
+
127
+ def _add_merged(self, repo: Repo) -> None:
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
+ self.merged.add(key, value)
139
+
140
+ def __str__(self):
141
+ lines = [f"RepoManager with {len(self.repos)} repositories:"]
142
+ for i, repo in enumerate(self.repos):
143
+ lines.append(f" [{i}] {repo.url}")
144
+ return "\n".join(lines)
@@ -0,0 +1,294 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ from pathlib import Path
4
+ from importlib import resources
5
+ from typing import Any, TYPE_CHECKING
6
+
7
+ import tomlkit
8
+ from tomlkit.toml_file import TOMLFile
9
+ from tomlkit.items import AoT
10
+ from multidict import MultiDict
11
+
12
+ if TYPE_CHECKING:
13
+ from repo.manager import RepoManager
14
+
15
+ repo_suffix = "starbash.toml"
16
+
17
+ REPO_REF = "repo-ref"
18
+
19
+
20
+ class Repo:
21
+ """
22
+ Represents a single starbash repository."""
23
+
24
+ def __init__(self, manager: RepoManager, url: str):
25
+ """
26
+ Initializes a Repo instance.
27
+
28
+ Args:
29
+ url: The URL to the repository (file or general http/https urls are acceptable).
30
+ """
31
+ self.manager = manager
32
+ self.url = url
33
+ self.config = self._load_config()
34
+
35
+ def __str__(self) -> str:
36
+ """Return a concise one-line description of this repo.
37
+
38
+ Example: "Repo(kind=recipe, local=True, url=file:///path/to/repo)"
39
+ """
40
+ return f"Repo(kind={self.kind()}, url={self.url})"
41
+
42
+ __repr__ = __str__
43
+
44
+ def kind(self, unknown_kind: str = "unknown") -> str:
45
+ """
46
+ Read-only attribute for the repository kind (e.g., "recipe", "data", etc.).
47
+
48
+ Returns:
49
+ The kind of the repository as a string.
50
+ """
51
+ c = self.get("repo.kind", unknown_kind)
52
+ return str(c)
53
+
54
+ def add_repo_ref(self, dir: Path) -> Repo | None:
55
+ """
56
+ Adds a new repo-ref to this repository's configuration.
57
+ if new returns the newly added Repo object, if already exists returns None"""
58
+
59
+ # if dir is not absolute, we need to resolve it relative to the cwd
60
+ if not dir.is_absolute():
61
+ dir = (Path.cwd() / dir).resolve()
62
+
63
+ # Add the ref to this repo
64
+ aot = self.config.get(REPO_REF, None)
65
+ if aot is None:
66
+ aot = tomlkit.aot()
67
+ self.config[REPO_REF] = aot # add an empty AoT at the end of the file
68
+
69
+ if type(aot) is not AoT:
70
+ raise ValueError(f"repo-ref in {self.url} is not an array")
71
+
72
+ for t in aot:
73
+ if "dir" in t and t["dir"] == str(dir):
74
+ logging.warning(f"Repo ref {dir} already exists - ignoring.")
75
+ return None # already exists
76
+
77
+ ref = {"dir": str(dir)}
78
+ aot.append(ref)
79
+
80
+ # Also add the repo to the manager
81
+ return self.add_from_ref(ref)
82
+
83
+ def write_config(self) -> None:
84
+ """
85
+ Writes the current (possibly modified) configuration back to the repository's config file.
86
+
87
+ Raises:
88
+ ValueError: If the repository is not a local file repository.
89
+ """
90
+ base_path = self.get_path()
91
+ if base_path is None:
92
+ raise ValueError("Cannot resolve path for non-local repository")
93
+
94
+ config_path = base_path / repo_suffix
95
+ # FIXME, be more careful to write the file atomically (by writing to a temp file and renaming)
96
+ TOMLFile(config_path).write(self.config)
97
+ logging.debug(f"Wrote config to {config_path}")
98
+
99
+ def is_scheme(self, scheme: str = "file") -> bool:
100
+ """
101
+ Read-only attribute indicating whether the repository URL points to a
102
+ local file system path (file:// scheme).
103
+
104
+ Returns:
105
+ bool: True if the URL is a local file path, False otherwise.
106
+ """
107
+ return self.url.startswith(f"{scheme}://")
108
+
109
+ def get_path(self) -> Path | None:
110
+ """
111
+ Resolves the URL to a local file system path if it's a file URI.
112
+
113
+ Args:
114
+ url: The repository URL.
115
+
116
+ Returns:
117
+ A Path object if the URL is a local file, otherwise None.
118
+ """
119
+ if self.is_scheme("file"):
120
+ return Path(self.url[len("file://") :])
121
+
122
+ return None
123
+
124
+ def add_from_ref(self, ref: dict) -> Repo:
125
+ """
126
+ Adds a repository based on a repo-ref dictionary.
127
+ """
128
+ if "url" in ref:
129
+ url = ref["url"]
130
+ elif "dir" in ref:
131
+ # FIXME don't allow ~ or .. in file paths for security reasons?
132
+ if self.is_scheme("file"):
133
+ path = Path(ref["dir"])
134
+ base_path = self.get_path()
135
+
136
+ if base_path and not path.is_absolute():
137
+ # Resolve relative to the current TOML file's directory
138
+ path = (base_path / path).resolve()
139
+ else:
140
+ # Expand ~ and resolve from CWD
141
+ path = path.expanduser().resolve()
142
+ url = f"file://{path}"
143
+ else:
144
+ # construct an URL relative to this repo's URL
145
+ url = self.url.rstrip("/") + "/" + ref["dir"].lstrip("/")
146
+ else:
147
+ raise ValueError(f"Invalid repo reference: {ref}")
148
+ return self.manager.add_repo(url)
149
+
150
+ def add_by_repo_refs(self) -> None:
151
+ """Add all repos mentioned by repo-refs in this repo's config."""
152
+ repo_refs = self.config.get(REPO_REF, [])
153
+
154
+ for ref in repo_refs:
155
+ self.add_from_ref(ref)
156
+
157
+ def resolve_path(self, filepath: str) -> Path:
158
+ """
159
+ Resolve a filepath relative to the base of this repo.
160
+
161
+ Args:
162
+ filepath: The path to the file, relative to the repository root.
163
+
164
+ Returns:
165
+ The resolved Path object.
166
+ """
167
+ base_path = self.get_path()
168
+ if base_path is None:
169
+ raise ValueError("Cannot resolve filepaths for non-local repositories")
170
+ target_path = (base_path / filepath).resolve()
171
+
172
+ # Security check to prevent accessing files outside the repo directory.
173
+ # FIXME SECURITY - temporarily disabled because I want to let file urls say things like ~/foo.
174
+ # it would false trigger if user homedir path has a symlink in it (such as /home -> /var/home)
175
+ # base_path = PosixPath('/home/kevinh/.config/starbash') │ │
176
+ # filepath = 'starbash.toml' │ │
177
+ # self = <repr-error 'maximum recursion depth exceeded'> │ │
178
+ # target_path = PosixPath('/var/home/kevinh/.config/starbash/starbash.toml')
179
+ #
180
+ # if base_path not in target_path.parents and target_path != base_path:
181
+ # raise PermissionError("Attempted to access file outside of repository")
182
+
183
+ return target_path
184
+
185
+ def _read_file(self, filepath: str) -> str:
186
+ """
187
+ Read a filepath relative to the base of this repo. Return the contents in a string.
188
+
189
+ Args:
190
+ filepath: The path to the file, relative to the repository root.
191
+
192
+ Returns:
193
+ The content of the file as a string.
194
+ """
195
+ target_path = self.resolve_path(filepath)
196
+
197
+ return target_path.read_text()
198
+
199
+ def _read_resource(self, filepath: str) -> str:
200
+ """
201
+ Read a resource from the installed starbash package using a pkg:// URL.
202
+
203
+ Assumptions (simplified per project constraints):
204
+ - All pkg URLs point somewhere inside the already-imported 'starbash' package.
205
+ - The URL is treated as a path relative to the starbash package root.
206
+
207
+ Examples:
208
+ url: pkg://defaults + filepath: "starbash.toml"
209
+ -> reads starbash/defaults/starbash.toml
210
+
211
+ Args:
212
+ filepath: Path within the base resource directory for this repo.
213
+
214
+ Returns:
215
+ The content of the resource as a string (UTF-8).
216
+ """
217
+ # Path portion after pkg://, interpreted relative to the 'starbash' package
218
+ subpath = self.url[len("pkg://") :].strip("/")
219
+
220
+ res = resources.files("starbash").joinpath(subpath).joinpath(filepath)
221
+ return res.read_text()
222
+
223
+ def _load_config(self) -> tomlkit.TOMLDocument:
224
+ """
225
+ Loads the repository's configuration file (e.g., repo.sb.toml).
226
+
227
+ If the config file does not exist, it logs a warning and returns an empty dict.
228
+
229
+ Returns:
230
+ A dictionary containing the parsed configuration.
231
+ """
232
+ 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}")
239
+ logging.debug(f"Loading repo config from {repo_suffix}")
240
+ return tomlkit.parse(config_content)
241
+ except FileNotFoundError:
242
+ logging.debug(
243
+ f"No {repo_suffix} found"
244
+ ) # we currently make it optional to have the config file at root
245
+ return tomlkit.TOMLDocument() # empty placeholder
246
+
247
+ def get(self, key: str, default: Any | None = None) -> Any | None:
248
+ """
249
+ Gets a value from this repo's config for a given key.
250
+ The key can be a dot-separated string for nested values.
251
+
252
+ Args:
253
+ key: The dot-separated key to search for (e.g., "repo.kind").
254
+ default: The value to return if the key is not found.
255
+
256
+ Returns:
257
+ The found value or the default.
258
+ """
259
+ value = self.config
260
+ for k in key.split("."):
261
+ if not isinstance(value, dict):
262
+ return default
263
+ value = value.get(k)
264
+ return value if value is not None else default
265
+
266
+ def set(self, key: str, value: Any) -> None:
267
+ """
268
+ Sets a value in this repo's config for a given key.
269
+ The key can be a dot-separated string for nested values.
270
+ Creates nested Table structures as needed.
271
+
272
+ Args:
273
+ key: The dot-separated key to set (e.g., "repo.kind").
274
+ value: The value to set.
275
+
276
+ Example:
277
+ repo.set("repo.kind", "preferences")
278
+ repo.set("user.name", "John Doe")
279
+ """
280
+ keys = key.split(".")
281
+ current: Any = self.config
282
+
283
+ # Navigate/create nested structure for all keys except the last
284
+ for k in keys[:-1]:
285
+ if k not in current:
286
+ # Create a new nested table
287
+ current[k] = tomlkit.table()
288
+ elif not isinstance(current[k], dict):
289
+ # Overwrite non-dict value with a table
290
+ current[k] = tomlkit.table()
291
+ current = current[k]
292
+
293
+ # Set the final value
294
+ current[keys[-1]] = value
@@ -1,5 +1,7 @@
1
+ import datetime
1
2
  import logging
2
3
  import os
4
+ from datetime import datetime
3
5
 
4
6
  from .database import Database # re-export for convenience
5
7
  from rich.console import Console
@@ -15,4 +17,22 @@ console = Console(
15
17
  # Global variable for log filter level (can be changed via --debug flag)
16
18
  log_filter_level = logging.INFO
17
19
 
20
+
21
+ def to_shortdate(date_iso: str) -> str:
22
+ """Convert ISO UTC datetime string to local short date string (YYYY-MM-DD).
23
+
24
+ Args:
25
+ date_iso: ISO format datetime string (e.g., "2023-10-15T14:30:00Z")
26
+
27
+ Returns:
28
+ Short date string in YYYY-MM-DD format
29
+ """
30
+ try:
31
+ dt_utc = datetime.fromisoformat(date_iso)
32
+ dt_local = dt_utc.astimezone()
33
+ return dt_local.strftime("%Y-%m-%d")
34
+ except (ValueError, TypeError):
35
+ return date_iso
36
+
37
+
18
38
  __all__ = ["Database"]
@@ -4,6 +4,7 @@ import os
4
4
  import starbash
5
5
  from starbash import console, _is_test_env
6
6
  import starbash.url as url
7
+ from sentry_sdk.integrations.excepthook import ExcepthookIntegration
7
8
 
8
9
  # Default to no analytics/auto crash reports
9
10
  analytics_allowed = False
@@ -25,6 +26,9 @@ def analytics_setup(allowed: bool = False, user_email: str | None = None) -> Non
25
26
  send_default_pii=True,
26
27
  enable_logs=True,
27
28
  traces_sample_rate=1.0,
29
+ disabled_integrations=[
30
+ ExcepthookIntegration()
31
+ ], # This line removes the aggressive unhandled exception catcher
28
32
  integrations=[
29
33
  LoggingIntegration(
30
34
  level=starbash.log_filter_level, # Capture INFO and above as breadcrumbs