starbash 0.1.8__py3-none-any.whl → 0.1.10__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.
- repo/__init__.py +2 -1
- repo/manager.py +31 -268
- repo/repo.py +294 -0
- starbash/__init__.py +20 -0
- starbash/aliases.py +100 -0
- starbash/analytics.py +4 -0
- starbash/app.py +740 -151
- starbash/commands/__init__.py +0 -17
- starbash/commands/info.py +72 -3
- starbash/commands/process.py +154 -0
- starbash/commands/repo.py +185 -78
- starbash/commands/select.py +135 -44
- starbash/database.py +397 -155
- starbash/defaults/starbash.toml +35 -0
- starbash/main.py +4 -1
- starbash/paths.py +18 -2
- starbash/recipes/master_bias/starbash.toml +32 -19
- starbash/recipes/master_dark/starbash.toml +36 -0
- starbash/recipes/master_flat/starbash.toml +27 -17
- starbash/recipes/osc_dual_duo/starbash.py +1 -5
- starbash/recipes/osc_dual_duo/starbash.toml +8 -4
- starbash/recipes/osc_single_duo/starbash.toml +4 -4
- starbash/recipes/starbash.toml +28 -3
- starbash/selection.py +115 -46
- starbash/templates/repo/master.toml +13 -0
- starbash/templates/repo/processed.toml +10 -0
- starbash/templates/userconfig.toml +1 -1
- starbash/toml.py +29 -0
- starbash/tool.py +199 -67
- {starbash-0.1.8.dist-info → starbash-0.1.10.dist-info}/METADATA +20 -13
- starbash-0.1.10.dist-info/RECORD +40 -0
- starbash-0.1.8.dist-info/RECORD +0 -33
- {starbash-0.1.8.dist-info → starbash-0.1.10.dist-info}/WHEEL +0 -0
- {starbash-0.1.8.dist-info → starbash-0.1.10.dist-info}/entry_points.txt +0 -0
- {starbash-0.1.8.dist-info → starbash-0.1.10.dist-info}/licenses/LICENSE +0 -0
repo/__init__.py
CHANGED
repo/manager.py
CHANGED
|
@@ -12,274 +12,7 @@ import tomlkit
|
|
|
12
12
|
from tomlkit.toml_file import TOMLFile
|
|
13
13
|
from tomlkit.items import AoT
|
|
14
14
|
from multidict import MultiDict
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
repo_suffix = "starbash.toml"
|
|
18
|
-
|
|
19
|
-
REPO_REF = "repo-ref"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Repo:
|
|
23
|
-
"""
|
|
24
|
-
Represents a single starbash repository."""
|
|
25
|
-
|
|
26
|
-
def __init__(self, manager: RepoManager, url: str):
|
|
27
|
-
"""
|
|
28
|
-
Initializes a Repo instance.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
url: The URL to the repository (file or general http/https urls are acceptable).
|
|
32
|
-
"""
|
|
33
|
-
self.manager = manager
|
|
34
|
-
self.url = url
|
|
35
|
-
self.config = self._load_config()
|
|
36
|
-
|
|
37
|
-
def __str__(self) -> str:
|
|
38
|
-
"""Return a concise one-line description of this repo.
|
|
39
|
-
|
|
40
|
-
Example: "Repo(kind=recipe, local=True, url=file:///path/to/repo)"
|
|
41
|
-
"""
|
|
42
|
-
return f"Repo(kind={self.kind}, url={self.url})"
|
|
43
|
-
|
|
44
|
-
__repr__ = __str__
|
|
45
|
-
|
|
46
|
-
def kind(self, unknown_kind: str = "unknown") -> str:
|
|
47
|
-
"""
|
|
48
|
-
Read-only attribute for the repository kind (e.g., "recipe", "data", etc.).
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
The kind of the repository as a string.
|
|
52
|
-
"""
|
|
53
|
-
c = self.get("repo.kind", unknown_kind)
|
|
54
|
-
return str(c)
|
|
55
|
-
|
|
56
|
-
def add_repo_ref(self, dir: str) -> Repo | None:
|
|
57
|
-
"""
|
|
58
|
-
Adds a new repo-ref to this repository's configuration.
|
|
59
|
-
if new returns the newly added Repo object, if already exists returns None"""
|
|
60
|
-
|
|
61
|
-
# if dir is not absolute, we need to resolve it relative to the cwd
|
|
62
|
-
if not Path(dir).is_absolute():
|
|
63
|
-
dir = str((Path.cwd() / dir).resolve())
|
|
64
|
-
|
|
65
|
-
# Add the ref to this repo
|
|
66
|
-
aot = self.config.get(REPO_REF, None)
|
|
67
|
-
if aot is None:
|
|
68
|
-
aot = tomlkit.aot()
|
|
69
|
-
self.config[REPO_REF] = aot # add an empty AoT at the end of the file
|
|
70
|
-
|
|
71
|
-
if type(aot) is not AoT:
|
|
72
|
-
raise ValueError(f"repo-ref in {self.url} is not an array")
|
|
73
|
-
|
|
74
|
-
for t in aot:
|
|
75
|
-
if "dir" in t and t["dir"] == dir:
|
|
76
|
-
logging.warning(f"Repo ref {dir} already exists - ignoring.")
|
|
77
|
-
return None # already exists
|
|
78
|
-
|
|
79
|
-
ref = {"dir": dir}
|
|
80
|
-
aot.append(ref)
|
|
81
|
-
|
|
82
|
-
# Also add the repo to the manager
|
|
83
|
-
return self.add_from_ref(ref)
|
|
84
|
-
|
|
85
|
-
def write_config(self) -> None:
|
|
86
|
-
"""
|
|
87
|
-
Writes the current (possibly modified) configuration back to the repository's config file.
|
|
88
|
-
|
|
89
|
-
Raises:
|
|
90
|
-
ValueError: If the repository is not a local file repository.
|
|
91
|
-
"""
|
|
92
|
-
base_path = self.get_path()
|
|
93
|
-
if base_path is None:
|
|
94
|
-
raise ValueError("Cannot resolve path for non-local repository")
|
|
95
|
-
|
|
96
|
-
config_path = base_path / repo_suffix
|
|
97
|
-
# FIXME, be more careful to write the file atomically (by writing to a temp file and renaming)
|
|
98
|
-
TOMLFile(config_path).write(self.config)
|
|
99
|
-
logging.debug(f"Wrote config to {config_path}")
|
|
100
|
-
|
|
101
|
-
def is_scheme(self, scheme: str = "file") -> bool:
|
|
102
|
-
"""
|
|
103
|
-
Read-only attribute indicating whether the repository URL points to a
|
|
104
|
-
local file system path (file:// scheme).
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
bool: True if the URL is a local file path, False otherwise.
|
|
108
|
-
"""
|
|
109
|
-
return self.url.startswith(f"{scheme}://")
|
|
110
|
-
|
|
111
|
-
def get_path(self) -> Path | None:
|
|
112
|
-
"""
|
|
113
|
-
Resolves the URL to a local file system path if it's a file URI.
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
url: The repository URL.
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
A Path object if the URL is a local file, otherwise None.
|
|
120
|
-
"""
|
|
121
|
-
if self.is_scheme("file"):
|
|
122
|
-
return Path(self.url[len("file://") :])
|
|
123
|
-
|
|
124
|
-
return None
|
|
125
|
-
|
|
126
|
-
def add_from_ref(self, ref: dict) -> Repo:
|
|
127
|
-
"""
|
|
128
|
-
Adds a repository based on a repo-ref dictionary.
|
|
129
|
-
"""
|
|
130
|
-
if "url" in ref:
|
|
131
|
-
url = ref["url"]
|
|
132
|
-
elif "dir" in ref:
|
|
133
|
-
# FIXME don't allow ~ or .. in file paths for security reasons?
|
|
134
|
-
if self.is_scheme("file"):
|
|
135
|
-
path = Path(ref["dir"])
|
|
136
|
-
base_path = self.get_path()
|
|
137
|
-
|
|
138
|
-
if base_path and not path.is_absolute():
|
|
139
|
-
# Resolve relative to the current TOML file's directory
|
|
140
|
-
path = (base_path / path).resolve()
|
|
141
|
-
else:
|
|
142
|
-
# Expand ~ and resolve from CWD
|
|
143
|
-
path = path.expanduser().resolve()
|
|
144
|
-
url = f"file://{path}"
|
|
145
|
-
else:
|
|
146
|
-
# construct an URL relative to this repo's URL
|
|
147
|
-
url = self.url.rstrip("/") + "/" + ref["dir"].lstrip("/")
|
|
148
|
-
else:
|
|
149
|
-
raise ValueError(f"Invalid repo reference: {ref}")
|
|
150
|
-
return self.manager.add_repo(url)
|
|
151
|
-
|
|
152
|
-
def add_by_repo_refs(self) -> None:
|
|
153
|
-
"""Add all repos mentioned by repo-refs in this repo's config."""
|
|
154
|
-
repo_refs = self.config.get(REPO_REF, [])
|
|
155
|
-
|
|
156
|
-
for ref in repo_refs:
|
|
157
|
-
self.add_from_ref(ref)
|
|
158
|
-
|
|
159
|
-
def _read_file(self, filepath: str) -> str:
|
|
160
|
-
"""
|
|
161
|
-
Read a filepath relative to the base of this repo. Return the contents in a string.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
filepath: The path to the file, relative to the repository root.
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
The content of the file as a string.
|
|
168
|
-
"""
|
|
169
|
-
base_path = self.get_path()
|
|
170
|
-
if base_path is None:
|
|
171
|
-
raise ValueError("Cannot read files from non-local repositories")
|
|
172
|
-
target_path = (base_path / filepath).resolve()
|
|
173
|
-
|
|
174
|
-
# Security check to prevent reading files outside the repo directory.
|
|
175
|
-
# FIXME SECURITY - temporarily disabled because I want to let file urls say things like ~/foo.
|
|
176
|
-
# it would false trigger if user homedir path has a symlink in it (such as /home -> /var/home)
|
|
177
|
-
# base_path = PosixPath('/home/kevinh/.config/starbash') │ │
|
|
178
|
-
# filepath = 'starbash.toml' │ │
|
|
179
|
-
# self = <repr-error 'maximum recursion depth exceeded'> │ │
|
|
180
|
-
# target_path = PosixPath('/var/home/kevinh/.config/starbash/starbash.toml')
|
|
181
|
-
#
|
|
182
|
-
# if base_path not in target_path.parents and target_path != base_path:
|
|
183
|
-
# raise PermissionError("Attempted to read file outside of repository")
|
|
184
|
-
|
|
185
|
-
return target_path.read_text()
|
|
186
|
-
|
|
187
|
-
def _read_resource(self, filepath: str) -> str:
|
|
188
|
-
"""
|
|
189
|
-
Read a resource from the installed starbash package using a pkg:// URL.
|
|
190
|
-
|
|
191
|
-
Assumptions (simplified per project constraints):
|
|
192
|
-
- All pkg URLs point somewhere inside the already-imported 'starbash' package.
|
|
193
|
-
- The URL is treated as a path relative to the starbash package root.
|
|
194
|
-
|
|
195
|
-
Examples:
|
|
196
|
-
url: pkg://defaults + filepath: "starbash.toml"
|
|
197
|
-
-> reads starbash/defaults/starbash.toml
|
|
198
|
-
|
|
199
|
-
Args:
|
|
200
|
-
filepath: Path within the base resource directory for this repo.
|
|
201
|
-
|
|
202
|
-
Returns:
|
|
203
|
-
The content of the resource as a string (UTF-8).
|
|
204
|
-
"""
|
|
205
|
-
# Path portion after pkg://, interpreted relative to the 'starbash' package
|
|
206
|
-
subpath = self.url[len("pkg://") :].strip("/")
|
|
207
|
-
|
|
208
|
-
res = resources.files("starbash").joinpath(subpath).joinpath(filepath)
|
|
209
|
-
return res.read_text()
|
|
210
|
-
|
|
211
|
-
def _load_config(self) -> tomlkit.TOMLDocument:
|
|
212
|
-
"""
|
|
213
|
-
Loads the repository's configuration file (e.g., repo.sb.toml).
|
|
214
|
-
|
|
215
|
-
If the config file does not exist, it logs a warning and returns an empty dict.
|
|
216
|
-
|
|
217
|
-
Returns:
|
|
218
|
-
A dictionary containing the parsed configuration.
|
|
219
|
-
"""
|
|
220
|
-
try:
|
|
221
|
-
if self.is_scheme("file"):
|
|
222
|
-
config_content = self._read_file(repo_suffix)
|
|
223
|
-
elif self.is_scheme("pkg"):
|
|
224
|
-
config_content = self._read_resource(repo_suffix)
|
|
225
|
-
else:
|
|
226
|
-
raise ValueError(f"Unsupported URL scheme for repo: {self.url}")
|
|
227
|
-
logging.debug(f"Loading repo config from {repo_suffix}")
|
|
228
|
-
return tomlkit.parse(config_content)
|
|
229
|
-
except FileNotFoundError:
|
|
230
|
-
logging.debug(
|
|
231
|
-
f"No {repo_suffix} found"
|
|
232
|
-
) # we currently make it optional to have the config file at root
|
|
233
|
-
return tomlkit.TOMLDocument() # empty placeholder
|
|
234
|
-
|
|
235
|
-
def get(self, key: str, default: Any | None = None) -> Any | None:
|
|
236
|
-
"""
|
|
237
|
-
Gets a value from this repo's config for a given key.
|
|
238
|
-
The key can be a dot-separated string for nested values.
|
|
239
|
-
|
|
240
|
-
Args:
|
|
241
|
-
key: The dot-separated key to search for (e.g., "repo.kind").
|
|
242
|
-
default: The value to return if the key is not found.
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
The found value or the default.
|
|
246
|
-
"""
|
|
247
|
-
value = self.config
|
|
248
|
-
for k in key.split("."):
|
|
249
|
-
if not isinstance(value, dict):
|
|
250
|
-
return default
|
|
251
|
-
value = value.get(k)
|
|
252
|
-
return value if value is not None else default
|
|
253
|
-
|
|
254
|
-
def set(self, key: str, value: Any) -> None:
|
|
255
|
-
"""
|
|
256
|
-
Sets a value in this repo's config for a given key.
|
|
257
|
-
The key can be a dot-separated string for nested values.
|
|
258
|
-
Creates nested Table structures as needed.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
key: The dot-separated key to set (e.g., "repo.kind").
|
|
262
|
-
value: The value to set.
|
|
263
|
-
|
|
264
|
-
Example:
|
|
265
|
-
repo.set("repo.kind", "preferences")
|
|
266
|
-
repo.set("user.name", "John Doe")
|
|
267
|
-
"""
|
|
268
|
-
keys = key.split(".")
|
|
269
|
-
current: Any = self.config
|
|
270
|
-
|
|
271
|
-
# Navigate/create nested structure for all keys except the last
|
|
272
|
-
for k in keys[:-1]:
|
|
273
|
-
if k not in current:
|
|
274
|
-
# Create a new nested table
|
|
275
|
-
current[k] = tomlkit.table()
|
|
276
|
-
elif not isinstance(current[k], dict):
|
|
277
|
-
# Overwrite non-dict value with a table
|
|
278
|
-
current[k] = tomlkit.table()
|
|
279
|
-
current = current[k]
|
|
280
|
-
|
|
281
|
-
# Set the final value
|
|
282
|
-
current[keys[-1]] = value
|
|
15
|
+
from repo.repo import Repo
|
|
283
16
|
|
|
284
17
|
|
|
285
18
|
class RepoManager:
|
|
@@ -326,6 +59,36 @@ class RepoManager:
|
|
|
326
59
|
|
|
327
60
|
return r
|
|
328
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
|
+
|
|
329
92
|
def get(self, key: str, default=None):
|
|
330
93
|
"""
|
|
331
94
|
Searches for a key across all repositories and returns the first value found.
|
repo/repo.py
ADDED
|
@@ -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
|
starbash/__init__.py
CHANGED
|
@@ -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"]
|