starbash 0.1.0__py3-none-any.whl → 0.1.3__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.
- starbash/analytics.py +121 -0
- starbash/app.py +223 -38
- starbash/commands/repo.py +103 -21
- starbash/commands/selection.py +137 -0
- starbash/commands/user.py +63 -0
- starbash/database.py +476 -25
- starbash/defaults/__init__.py +0 -0
- starbash/{appdefaults.sb.toml → defaults/starbash.toml} +5 -45
- starbash/main.py +142 -13
- starbash/paths.py +38 -0
- starbash/repo/manager.py +129 -59
- starbash/selection.py +251 -0
- starbash/templates/__init__.py +0 -0
- starbash/templates/userconfig.toml +53 -0
- starbash/url.py +9 -0
- starbash-0.1.3.dist-info/METADATA +114 -0
- starbash-0.1.3.dist-info/RECORD +24 -0
- {starbash-0.1.0.dist-info → starbash-0.1.3.dist-info}/WHEEL +1 -1
- starbash-0.1.0.dist-info/METADATA +0 -82
- starbash-0.1.0.dist-info/RECORD +0 -15
- {starbash-0.1.0.dist-info → starbash-0.1.3.dist-info}/entry_points.txt +0 -0
- {starbash-0.1.0.dist-info → starbash-0.1.3.dist-info/licenses}/LICENSE +0 -0
starbash/main.py
CHANGED
|
@@ -1,26 +1,155 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from tomlkit import table
|
|
2
4
|
import typer
|
|
5
|
+
from rich.table import Table
|
|
3
6
|
|
|
4
|
-
from .
|
|
5
|
-
|
|
7
|
+
from starbash.database import Database
|
|
8
|
+
import starbash.url as url
|
|
6
9
|
|
|
7
|
-
app
|
|
10
|
+
from .app import Starbash
|
|
11
|
+
from .commands import repo, user, selection
|
|
12
|
+
from . import console
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
rich_markup_mode="rich",
|
|
16
|
+
help=f"Starbash - Astrophotography workflows simplified.\n\nFor full instructions and support [link={url.project}]click here[/link].",
|
|
17
|
+
)
|
|
18
|
+
app.add_typer(user.app, name="user", help="Manage user settings.")
|
|
8
19
|
app.add_typer(repo.app, name="repo", help="Manage Starbash repositories.")
|
|
20
|
+
app.add_typer(
|
|
21
|
+
selection.app, name="selection", help="Manage session and target selection."
|
|
22
|
+
)
|
|
23
|
+
|
|
9
24
|
|
|
25
|
+
@app.callback(invoke_without_command=True)
|
|
26
|
+
def main_callback(ctx: typer.Context):
|
|
27
|
+
"""Main callback for the Starbash application."""
|
|
28
|
+
if ctx.invoked_subcommand is None:
|
|
29
|
+
# No command provided, show help
|
|
30
|
+
console.print(ctx.get_help())
|
|
31
|
+
raise typer.Exit()
|
|
10
32
|
|
|
11
|
-
@app.command(hidden=True)
|
|
12
|
-
def default_cmd():
|
|
13
|
-
"""Default entry point for the starbash application."""
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
def format_duration(seconds: int):
|
|
35
|
+
"""Format seconds as a human-readable duration string."""
|
|
36
|
+
if seconds < 60:
|
|
37
|
+
return f"{int(seconds)}s"
|
|
38
|
+
elif seconds < 120:
|
|
39
|
+
minutes = int(seconds // 60)
|
|
40
|
+
secs = int(seconds % 60)
|
|
41
|
+
return f"{minutes}m {secs}s" if secs else f"{minutes}m"
|
|
42
|
+
else:
|
|
43
|
+
hours = int(seconds // 3600)
|
|
44
|
+
minutes = int((seconds % 3600) // 60)
|
|
45
|
+
return f"{hours}h {minutes}m" if minutes else f"{hours}h"
|
|
17
46
|
|
|
18
47
|
|
|
19
|
-
@app.
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
48
|
+
@app.command()
|
|
49
|
+
def session():
|
|
50
|
+
"""List sessions (filtered based on the current selection)"""
|
|
51
|
+
|
|
52
|
+
with Starbash("session") as sb:
|
|
53
|
+
sessions = sb.search_session()
|
|
54
|
+
if sessions and isinstance(sessions, list):
|
|
55
|
+
len_all = sb.db.len_session()
|
|
56
|
+
table = Table(title=f"Sessions ({len(sessions)} selected out of {len_all})")
|
|
57
|
+
|
|
58
|
+
table.add_column("Date", style="cyan", no_wrap=True)
|
|
59
|
+
table.add_column("# images", style="cyan", no_wrap=True)
|
|
60
|
+
table.add_column("Time", style="cyan", no_wrap=True)
|
|
61
|
+
table.add_column("Type/Filter", style="cyan", no_wrap=True)
|
|
62
|
+
table.add_column("Telescope", style="cyan", no_wrap=True)
|
|
63
|
+
table.add_column(
|
|
64
|
+
"About", style="cyan", no_wrap=True
|
|
65
|
+
) # type of frames, filter, target
|
|
66
|
+
# table.add_column("Released", justify="right", style="cyan", no_wrap=True)
|
|
67
|
+
|
|
68
|
+
total_images = 0
|
|
69
|
+
total_seconds = 0.0
|
|
70
|
+
|
|
71
|
+
for sess in sessions:
|
|
72
|
+
date_iso = sess.get(Database.START_KEY, "N/A")
|
|
73
|
+
# Try to cnvert ISO UTC datetime to local short date string
|
|
74
|
+
try:
|
|
75
|
+
dt_utc = datetime.fromisoformat(date_iso)
|
|
76
|
+
dt_local = dt_utc.astimezone()
|
|
77
|
+
date = dt_local.strftime("%Y-%m-%d")
|
|
78
|
+
except (ValueError, TypeError):
|
|
79
|
+
date = date_iso
|
|
80
|
+
|
|
81
|
+
object = str(sess.get(Database.OBJECT_KEY, "N/A"))
|
|
82
|
+
filter = sess.get(Database.FILTER_KEY, "N/A")
|
|
83
|
+
image_type = str(sess.get(Database.IMAGETYP_KEY, "N/A"))
|
|
84
|
+
telescop = str(sess.get(Database.TELESCOP_KEY, "N/A"))
|
|
85
|
+
|
|
86
|
+
# Format total exposure time as integer seconds
|
|
87
|
+
exptime_raw = str(sess.get(Database.EXPTIME_TOTAL_KEY, "N/A"))
|
|
88
|
+
try:
|
|
89
|
+
exptime_float = float(exptime_raw)
|
|
90
|
+
total_seconds += exptime_float
|
|
91
|
+
total_secs = format_duration(int(exptime_float))
|
|
92
|
+
except (ValueError, TypeError):
|
|
93
|
+
total_secs = exptime_raw
|
|
94
|
+
|
|
95
|
+
# Count images
|
|
96
|
+
try:
|
|
97
|
+
num_images = int(sess.get(Database.NUM_IMAGES_KEY, 0))
|
|
98
|
+
total_images += num_images
|
|
99
|
+
except (ValueError, TypeError):
|
|
100
|
+
num_images = sess.get(Database.NUM_IMAGES_KEY, "N/A")
|
|
101
|
+
|
|
102
|
+
type_str = image_type
|
|
103
|
+
if image_type.upper() == "LIGHT":
|
|
104
|
+
image_type = filter
|
|
105
|
+
elif image_type.upper() == "FLAT":
|
|
106
|
+
image_type = f"{image_type}/{filter}"
|
|
107
|
+
else: # either bias or dark
|
|
108
|
+
object = "" # Don't show meaningless target
|
|
109
|
+
|
|
110
|
+
table.add_row(
|
|
111
|
+
date,
|
|
112
|
+
str(num_images),
|
|
113
|
+
total_secs,
|
|
114
|
+
image_type,
|
|
115
|
+
telescop,
|
|
116
|
+
object,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Add totals row
|
|
120
|
+
if sessions:
|
|
121
|
+
table.add_row(
|
|
122
|
+
"",
|
|
123
|
+
f"[bold]{total_images}[/bold]",
|
|
124
|
+
f"[bold]{format_duration(int(total_seconds))}[/bold]",
|
|
125
|
+
"",
|
|
126
|
+
"",
|
|
127
|
+
"",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
console.print(table)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# @app.command(hidden=True)
|
|
134
|
+
# def default_cmd():
|
|
135
|
+
# """Default entry point for the starbash application."""
|
|
136
|
+
#
|
|
137
|
+
# with Starbash() as sb:
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# @app.command(hidden=True)
|
|
141
|
+
# def default_cmd():
|
|
142
|
+
# """Default entry point for the starbash application."""
|
|
143
|
+
#
|
|
144
|
+
# with Starbash() as sb:
|
|
145
|
+
# pass
|
|
146
|
+
#
|
|
147
|
+
#
|
|
148
|
+
# @app.callback(invoke_without_command=True)
|
|
149
|
+
# def _default(ctx: typer.Context):
|
|
150
|
+
# # If the user didn’t specify a subcommand, run the default
|
|
151
|
+
# if ctx.invoked_subcommand is None:
|
|
152
|
+
# return default_cmd()
|
|
24
153
|
|
|
25
154
|
|
|
26
155
|
if __name__ == "__main__":
|
starbash/paths.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from platformdirs import PlatformDirs
|
|
4
|
+
|
|
5
|
+
app_name = "starbash"
|
|
6
|
+
app_author = "geeksville"
|
|
7
|
+
dirs = PlatformDirs(app_name, app_author)
|
|
8
|
+
config_dir = Path(dirs.user_config_dir)
|
|
9
|
+
data_dir = Path(dirs.user_data_dir)
|
|
10
|
+
|
|
11
|
+
# These can be overridden for testing
|
|
12
|
+
_override_config_dir: Path | None = None
|
|
13
|
+
_override_data_dir: Path | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_test_directories(
|
|
17
|
+
config_dir_override: Path | None = None, data_dir_override: Path | None = None
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Set override directories for testing. Used by test fixtures to isolate test data."""
|
|
20
|
+
global _override_config_dir, _override_data_dir
|
|
21
|
+
_override_config_dir = config_dir_override
|
|
22
|
+
_override_data_dir = data_dir_override
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_user_config_dir() -> Path:
|
|
26
|
+
"""Get the user config directory. Returns test override if set, otherwise the real user directory."""
|
|
27
|
+
dir_to_use = (
|
|
28
|
+
_override_config_dir if _override_config_dir is not None else config_dir
|
|
29
|
+
)
|
|
30
|
+
os.makedirs(dir_to_use, exist_ok=True)
|
|
31
|
+
return dir_to_use
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_user_data_dir() -> Path:
|
|
35
|
+
"""Get the user data directory. Returns test override if set, otherwise the real user directory."""
|
|
36
|
+
dir_to_use = _override_data_dir if _override_data_dir is not None else data_dir
|
|
37
|
+
os.makedirs(dir_to_use, exist_ok=True)
|
|
38
|
+
return dir_to_use
|
starbash/repo/manager.py
CHANGED
|
@@ -5,20 +5,24 @@ Manages the repository of processing recipes and configurations.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
import logging
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from importlib import resources
|
|
8
9
|
|
|
9
10
|
import tomlkit
|
|
11
|
+
from tomlkit.toml_file import TOMLFile
|
|
10
12
|
from tomlkit.items import AoT
|
|
11
13
|
from multidict import MultiDict
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
repo_suffix = "starbash.toml"
|
|
15
17
|
|
|
18
|
+
REPO_REF = "repo-ref"
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
class Repo:
|
|
18
22
|
"""
|
|
19
23
|
Represents a single starbash repository."""
|
|
20
24
|
|
|
21
|
-
def __init__(self, manager: RepoManager, url: str
|
|
25
|
+
def __init__(self, manager: RepoManager, url: str):
|
|
22
26
|
"""
|
|
23
27
|
Initializes a Repo instance.
|
|
24
28
|
|
|
@@ -27,15 +31,14 @@ class Repo:
|
|
|
27
31
|
"""
|
|
28
32
|
self.manager = manager
|
|
29
33
|
self.url = url
|
|
30
|
-
self.config =
|
|
31
|
-
self.manager.add_all_repos(self.config, self.get_path())
|
|
34
|
+
self.config = self._load_config()
|
|
32
35
|
|
|
33
36
|
def __str__(self) -> str:
|
|
34
37
|
"""Return a concise one-line description of this repo.
|
|
35
38
|
|
|
36
39
|
Example: "Repo(kind=recipe, local=True, url=file:///path/to/repo)"
|
|
37
40
|
"""
|
|
38
|
-
return f"Repo(kind={self.kind},
|
|
41
|
+
return f"Repo(kind={self.kind}, url={self.url})"
|
|
39
42
|
|
|
40
43
|
__repr__ = __str__
|
|
41
44
|
|
|
@@ -49,8 +52,37 @@ class Repo:
|
|
|
49
52
|
"""
|
|
50
53
|
return str(self.get("repo.kind", "unknown"))
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
def add_repo_ref(self, dir: str) -> None:
|
|
56
|
+
aot = self.config.get(REPO_REF, None)
|
|
57
|
+
if aot is None:
|
|
58
|
+
aot = tomlkit.aot()
|
|
59
|
+
else:
|
|
60
|
+
self.config.remove(
|
|
61
|
+
REPO_REF
|
|
62
|
+
) # We want to completely replace it at the end of the file
|
|
63
|
+
|
|
64
|
+
ref = {"dir": dir}
|
|
65
|
+
aot.append(ref)
|
|
66
|
+
self.config[REPO_REF] = aot
|
|
67
|
+
self.add_from_ref(ref)
|
|
68
|
+
|
|
69
|
+
def write_config(self) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Writes the current (possibly modified) configuration back to the repository's config file.
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If the repository is not a local file repository.
|
|
75
|
+
"""
|
|
76
|
+
base_path = self.get_path()
|
|
77
|
+
if base_path is None:
|
|
78
|
+
raise ValueError("Cannot resolve path for non-local repository")
|
|
79
|
+
|
|
80
|
+
config_path = base_path / repo_suffix
|
|
81
|
+
# FIXME, be more careful to write the file atomically (by writing to a temp file and renaming)
|
|
82
|
+
TOMLFile(config_path).write(self.config)
|
|
83
|
+
logging.debug(f"Wrote config to {config_path}")
|
|
84
|
+
|
|
85
|
+
def is_scheme(self, scheme: str = "file") -> bool:
|
|
54
86
|
"""
|
|
55
87
|
Read-only attribute indicating whether the repository URL points to a
|
|
56
88
|
local file system path (file:// scheme).
|
|
@@ -58,7 +90,7 @@ class Repo:
|
|
|
58
90
|
Returns:
|
|
59
91
|
bool: True if the URL is a local file path, False otherwise.
|
|
60
92
|
"""
|
|
61
|
-
return self.url.startswith("
|
|
93
|
+
return self.url.startswith(f"{scheme}://")
|
|
62
94
|
|
|
63
95
|
def get_path(self) -> Path | None:
|
|
64
96
|
"""
|
|
@@ -70,12 +102,39 @@ class Repo:
|
|
|
70
102
|
Returns:
|
|
71
103
|
A Path object if the URL is a local file, otherwise None.
|
|
72
104
|
"""
|
|
73
|
-
if self.
|
|
105
|
+
if self.is_scheme("file"):
|
|
74
106
|
return Path(self.url[len("file://") :])
|
|
75
107
|
|
|
76
108
|
return None
|
|
77
109
|
|
|
78
|
-
def
|
|
110
|
+
def add_from_ref(self, ref: dict) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Adds a repository based on a repo-ref dictionary.
|
|
113
|
+
"""
|
|
114
|
+
if "url" in ref:
|
|
115
|
+
url = ref["url"]
|
|
116
|
+
elif "dir" in ref:
|
|
117
|
+
path = Path(ref["dir"])
|
|
118
|
+
base_path = self.get_path()
|
|
119
|
+
if base_path and not path.is_absolute():
|
|
120
|
+
# Resolve relative to the current TOML file's directory
|
|
121
|
+
path = (base_path / path).resolve()
|
|
122
|
+
else:
|
|
123
|
+
# Expand ~ and resolve from CWD
|
|
124
|
+
path = path.expanduser().resolve()
|
|
125
|
+
url = f"file://{path}"
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError(f"Invalid repo reference: {ref}")
|
|
128
|
+
self.manager.add_repo(url)
|
|
129
|
+
|
|
130
|
+
def add_by_repo_refs(self) -> None:
|
|
131
|
+
"""Add all repos mentioned by repo-refs in this repo's config."""
|
|
132
|
+
repo_refs = self.config.get(REPO_REF, [])
|
|
133
|
+
|
|
134
|
+
for ref in repo_refs:
|
|
135
|
+
self.add_from_ref(ref)
|
|
136
|
+
|
|
137
|
+
def _read_file(self, filepath: str) -> str:
|
|
79
138
|
"""
|
|
80
139
|
Read a filepath relative to the base of this repo. Return the contents in a string.
|
|
81
140
|
|
|
@@ -96,7 +155,31 @@ class Repo:
|
|
|
96
155
|
|
|
97
156
|
return target_path.read_text()
|
|
98
157
|
|
|
99
|
-
def
|
|
158
|
+
def _read_resource(self, filepath: str) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Read a resource from the installed starbash package using a pkg:// URL.
|
|
161
|
+
|
|
162
|
+
Assumptions (simplified per project constraints):
|
|
163
|
+
- All pkg URLs point somewhere inside the already-imported 'starbash' package.
|
|
164
|
+
- The URL is treated as a path relative to the starbash package root.
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
url: pkg://defaults + filepath: "starbash.toml"
|
|
168
|
+
-> reads starbash/defaults/starbash.toml
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
filepath: Path within the base resource directory for this repo.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
The content of the resource as a string (UTF-8).
|
|
175
|
+
"""
|
|
176
|
+
# Path portion after pkg://, interpreted relative to the 'starbash' package
|
|
177
|
+
subpath = self.url[len("pkg://") :].strip("/")
|
|
178
|
+
|
|
179
|
+
res = resources.files("starbash").joinpath(subpath).joinpath(filepath)
|
|
180
|
+
return res.read_text()
|
|
181
|
+
|
|
182
|
+
def _load_config(self) -> tomlkit.TOMLDocument:
|
|
100
183
|
"""
|
|
101
184
|
Loads the repository's configuration file (e.g., repo.sb.toml).
|
|
102
185
|
|
|
@@ -106,12 +189,19 @@ class Repo:
|
|
|
106
189
|
A dictionary containing the parsed configuration.
|
|
107
190
|
"""
|
|
108
191
|
try:
|
|
109
|
-
|
|
192
|
+
if self.is_scheme("file"):
|
|
193
|
+
config_content = self._read_file(repo_suffix)
|
|
194
|
+
elif self.is_scheme("pkg"):
|
|
195
|
+
config_content = self._read_resource(repo_suffix)
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError(f"Unsupported URL scheme for repo: {self.url}")
|
|
110
198
|
logging.debug(f"Loading repo config from {repo_suffix}")
|
|
111
199
|
return tomlkit.parse(config_content)
|
|
112
200
|
except FileNotFoundError:
|
|
113
|
-
logging.
|
|
114
|
-
|
|
201
|
+
logging.debug(
|
|
202
|
+
f"No {repo_suffix} found"
|
|
203
|
+
) # we currently make it optional to have the config file at root
|
|
204
|
+
return tomlkit.TOMLDocument() # empty placeholder
|
|
115
205
|
|
|
116
206
|
def get(self, key: str, default=None):
|
|
117
207
|
"""
|
|
@@ -142,42 +232,36 @@ class RepoManager:
|
|
|
142
232
|
files (like appdefaults.sb.toml).
|
|
143
233
|
"""
|
|
144
234
|
|
|
145
|
-
def __init__(self
|
|
235
|
+
def __init__(self):
|
|
146
236
|
"""
|
|
147
237
|
Initializes the RepoManager by loading the application default repos.
|
|
148
238
|
"""
|
|
149
239
|
self.repos = []
|
|
150
240
|
|
|
151
241
|
# We expose the app default preferences as a special root repo with a private URL
|
|
152
|
-
root_repo = Repo(self, "pkg://starbash-defaults", config=app_defaults)
|
|
153
|
-
self.repos.append(root_repo)
|
|
242
|
+
# root_repo = Repo(self, "pkg://starbash-defaults", config=app_defaults)
|
|
243
|
+
# self.repos.append(root_repo)
|
|
154
244
|
|
|
155
245
|
# Most users will just want to read from merged
|
|
156
|
-
self.merged =
|
|
157
|
-
|
|
158
|
-
def add_all_repos(self, toml: dict, base_path: Path | None = None) -> None:
|
|
159
|
-
# From appdefaults.sb.toml, repo.ref is a list of tables
|
|
160
|
-
repo_refs = toml.get("repo", {}).get("ref", [])
|
|
246
|
+
self.merged = MultiDict()
|
|
161
247
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
path = Path(ref["dir"])
|
|
167
|
-
if base_path and not path.is_absolute():
|
|
168
|
-
# Resolve relative to the current TOML file's directory
|
|
169
|
-
path = (base_path / path).resolve()
|
|
170
|
-
else:
|
|
171
|
-
# Expand ~ and resolve from CWD
|
|
172
|
-
path = path.expanduser().resolve()
|
|
173
|
-
url = f"file://{path}"
|
|
174
|
-
else:
|
|
175
|
-
raise ValueError(f"Invalid repo reference: {ref}")
|
|
176
|
-
self.add_repo(url)
|
|
248
|
+
@property
|
|
249
|
+
def regular_repos(self) -> list[Repo]:
|
|
250
|
+
"We exclude certain repo types (preferences, recipe) from the list of repos users care about."
|
|
251
|
+
return [r for r in self.repos if r.kind not in ("preferences", "recipe")]
|
|
177
252
|
|
|
178
|
-
def add_repo(self, url: str) ->
|
|
253
|
+
def add_repo(self, url: str) -> Repo:
|
|
179
254
|
logging.debug(f"Adding repo: {url}")
|
|
180
|
-
|
|
255
|
+
r = Repo(self, url)
|
|
256
|
+
self.repos.append(r)
|
|
257
|
+
|
|
258
|
+
# FIXME, generate the merged dict lazily
|
|
259
|
+
self._add_merged(r)
|
|
260
|
+
|
|
261
|
+
# if this new repo has sub-repos, add them too
|
|
262
|
+
r.add_by_repo_refs()
|
|
263
|
+
|
|
264
|
+
return r
|
|
181
265
|
|
|
182
266
|
def get(self, key: str, default=None):
|
|
183
267
|
"""
|
|
@@ -214,32 +298,18 @@ class RepoManager:
|
|
|
214
298
|
# For a debug dump, a simple string representation is usually sufficient.
|
|
215
299
|
logging.info(f" %s: %s", key, value)
|
|
216
300
|
|
|
217
|
-
def
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
repositories, all of its values will be present in the returned MultiDict.
|
|
224
|
-
|
|
225
|
-
Returns:
|
|
226
|
-
A MultiDict containing the union of all top-level keys.
|
|
227
|
-
"""
|
|
228
|
-
merged_dict = MultiDict()
|
|
229
|
-
for repo in self.repos:
|
|
230
|
-
for key, value in repo.config.items():
|
|
231
|
-
# if the toml object is an AoT type, monkey patch each element in the array instead
|
|
232
|
-
if isinstance(value, AoT):
|
|
233
|
-
for v in value:
|
|
234
|
-
setattr(v, "source", repo)
|
|
301
|
+
def _add_merged(self, repo: Repo) -> None:
|
|
302
|
+
for key, value in repo.config.items():
|
|
303
|
+
# if the toml object is an AoT type, monkey patch each element in the array instead
|
|
304
|
+
if isinstance(value, AoT):
|
|
305
|
+
for v in value:
|
|
306
|
+
setattr(v, "source", repo)
|
|
235
307
|
else:
|
|
236
308
|
# We monkey patch source into any object that came from a repo, so that users can
|
|
237
309
|
# find the source repo (for attribution, URL relative resolution, whatever...)
|
|
238
310
|
setattr(value, "source", repo)
|
|
239
311
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return merged_dict
|
|
312
|
+
self.merged.add(key, value)
|
|
243
313
|
|
|
244
314
|
def __str__(self):
|
|
245
315
|
lines = [f"RepoManager with {len(self.repos)} repositories:"]
|