rhiza 0.9.0__py3-none-any.whl → 0.10.0__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.
- rhiza/__main__.py +1 -1
- rhiza/bundle_resolver.py +78 -0
- rhiza/cli.py +10 -10
- rhiza/commands/__init__.py +5 -3
- rhiza/commands/init.py +37 -10
- rhiza/commands/materialize.py +110 -26
- rhiza/commands/summarise.py +1 -1
- rhiza/commands/validate.py +94 -9
- rhiza/commands/welcome.py +1 -1
- rhiza/models.py +200 -7
- {rhiza-0.9.0.dist-info → rhiza-0.10.0.dist-info}/METADATA +2 -2
- rhiza-0.10.0.dist-info/RECORD +22 -0
- rhiza-0.9.0.dist-info/RECORD +0 -21
- {rhiza-0.9.0.dist-info → rhiza-0.10.0.dist-info}/WHEEL +0 -0
- {rhiza-0.9.0.dist-info → rhiza-0.10.0.dist-info}/entry_points.txt +0 -0
- {rhiza-0.9.0.dist-info → rhiza-0.10.0.dist-info}/licenses/LICENSE +0 -0
rhiza/__main__.py
CHANGED
|
@@ -11,7 +11,7 @@ import typer
|
|
|
11
11
|
from rhiza.cli import app
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def load_plugins(app: typer.Typer):
|
|
14
|
+
def load_plugins(app: typer.Typer) -> None:
|
|
15
15
|
"""Load plugins from entry points."""
|
|
16
16
|
# 'rhiza.plugins' matches the group we defined in rhiza-tools
|
|
17
17
|
plugin_entries = entry_points(group="rhiza.plugins")
|
rhiza/bundle_resolver.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Bundle resolution logic for template configuration.
|
|
2
|
+
|
|
3
|
+
This module provides functions to load and resolve bundle configurations
|
|
4
|
+
from the template repository's bundles.yml file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rhiza.models import RhizaBundles, RhizaTemplate
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_bundles_from_clone(tmp_dir: Path) -> RhizaBundles | None:
|
|
13
|
+
"""Load .rhiza/bundles.yml from cloned template repo.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
tmp_dir: Path to the cloned template repository.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
RhizaBundles if bundles.yml exists, None otherwise.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
yaml.YAMLError: If bundles.yml is malformed.
|
|
23
|
+
ValueError: If bundles.yml is invalid.
|
|
24
|
+
"""
|
|
25
|
+
bundles_file = tmp_dir / ".rhiza" / "bundles.yml"
|
|
26
|
+
if not bundles_file.exists():
|
|
27
|
+
return None
|
|
28
|
+
return RhizaBundles.from_yaml(bundles_file)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_include_paths(
|
|
32
|
+
template: RhizaTemplate,
|
|
33
|
+
bundles_config: RhizaBundles | None,
|
|
34
|
+
) -> list[str]:
|
|
35
|
+
"""Resolve template configuration to file paths.
|
|
36
|
+
|
|
37
|
+
Supports:
|
|
38
|
+
- Template-based mode (templates field)
|
|
39
|
+
- Path-based mode (include field)
|
|
40
|
+
- Hybrid mode (both templates and include)
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
template: The template configuration.
|
|
44
|
+
bundles_config: The loaded bundles configuration, or None if not available.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of file paths to materialize.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If configuration is invalid or bundles.yml is missing.
|
|
51
|
+
"""
|
|
52
|
+
paths = []
|
|
53
|
+
|
|
54
|
+
# Resolve templates to paths if specified
|
|
55
|
+
if template.templates:
|
|
56
|
+
if not bundles_config:
|
|
57
|
+
msg = "Template uses templates but bundles.yml not found in template repository"
|
|
58
|
+
raise ValueError(msg)
|
|
59
|
+
paths.extend(bundles_config.resolve_to_paths(template.templates))
|
|
60
|
+
|
|
61
|
+
# Add include paths if specified
|
|
62
|
+
if template.include:
|
|
63
|
+
paths.extend(template.include)
|
|
64
|
+
|
|
65
|
+
# At least one must be specified
|
|
66
|
+
if not paths:
|
|
67
|
+
msg = "Template configuration must specify either 'templates' or 'include'"
|
|
68
|
+
raise ValueError(msg)
|
|
69
|
+
|
|
70
|
+
# Deduplicate while preserving order
|
|
71
|
+
seen = set()
|
|
72
|
+
deduplicated = []
|
|
73
|
+
for path in paths:
|
|
74
|
+
if path not in seen:
|
|
75
|
+
deduplicated.append(path)
|
|
76
|
+
seen.add(path)
|
|
77
|
+
|
|
78
|
+
return deduplicated
|
rhiza/cli.py
CHANGED
|
@@ -23,14 +23,14 @@ app = typer.Typer(
|
|
|
23
23
|
"""
|
|
24
24
|
Rhiza - Manage reusable configuration templates for Python projects
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
https://jebel-quant.github.io/rhiza-cli/
|
|
27
27
|
"""
|
|
28
28
|
),
|
|
29
29
|
add_completion=True,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def version_callback(value: bool):
|
|
33
|
+
def version_callback(value: bool) -> None:
|
|
34
34
|
"""Print version information and exit.
|
|
35
35
|
|
|
36
36
|
Args:
|
|
@@ -54,7 +54,7 @@ def main(
|
|
|
54
54
|
callback=version_callback,
|
|
55
55
|
is_eager=True,
|
|
56
56
|
),
|
|
57
|
-
):
|
|
57
|
+
) -> None:
|
|
58
58
|
"""Rhiza CLI main callback.
|
|
59
59
|
|
|
60
60
|
This callback is executed before any command. It handles global options
|
|
@@ -107,7 +107,7 @@ def init(
|
|
|
107
107
|
"--template-branch",
|
|
108
108
|
help="Custom template branch. Defaults to 'main'.",
|
|
109
109
|
),
|
|
110
|
-
):
|
|
110
|
+
) -> None:
|
|
111
111
|
r"""Initialize or validate .rhiza/template.yml.
|
|
112
112
|
|
|
113
113
|
Creates a default `.rhiza/template.yml` configuration file if one
|
|
@@ -157,7 +157,7 @@ def materialize(
|
|
|
157
157
|
help="Create and checkout a new branch in the target repository for changes",
|
|
158
158
|
),
|
|
159
159
|
force: bool = typer.Option(False, "--force", "-y", help="Overwrite existing files"),
|
|
160
|
-
):
|
|
160
|
+
) -> None:
|
|
161
161
|
r"""Inject Rhiza configuration templates into a target repository.
|
|
162
162
|
|
|
163
163
|
Materializes configuration files from the template repository specified
|
|
@@ -190,7 +190,7 @@ def validate(
|
|
|
190
190
|
help="Target git repository (defaults to current directory)",
|
|
191
191
|
),
|
|
192
192
|
] = Path("."),
|
|
193
|
-
):
|
|
193
|
+
) -> None:
|
|
194
194
|
r"""Validate Rhiza template configuration.
|
|
195
195
|
|
|
196
196
|
Validates the .rhiza/template.yml file to ensure it is syntactically
|
|
@@ -227,7 +227,7 @@ def migrate(
|
|
|
227
227
|
help="Target git repository (defaults to current directory)",
|
|
228
228
|
),
|
|
229
229
|
] = Path("."),
|
|
230
|
-
):
|
|
230
|
+
) -> None:
|
|
231
231
|
r"""Migrate project to the new .rhiza folder structure.
|
|
232
232
|
|
|
233
233
|
This command helps transition projects to use the new `.rhiza/` folder
|
|
@@ -253,7 +253,7 @@ def migrate(
|
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
@app.command()
|
|
256
|
-
def welcome():
|
|
256
|
+
def welcome() -> None:
|
|
257
257
|
r"""Display a friendly welcome message and explain what Rhiza is.
|
|
258
258
|
|
|
259
259
|
Shows a welcome message, explains Rhiza's purpose, key features,
|
|
@@ -282,7 +282,7 @@ def uninstall(
|
|
|
282
282
|
"-y",
|
|
283
283
|
help="Skip confirmation prompt and proceed with deletion",
|
|
284
284
|
),
|
|
285
|
-
):
|
|
285
|
+
) -> None:
|
|
286
286
|
r"""Remove all Rhiza-managed files from the repository.
|
|
287
287
|
|
|
288
288
|
Reads the `.rhiza.history` file and removes all files that were
|
|
@@ -327,7 +327,7 @@ def summarise(
|
|
|
327
327
|
help="Output file path (defaults to stdout)",
|
|
328
328
|
),
|
|
329
329
|
] = None,
|
|
330
|
-
):
|
|
330
|
+
) -> None:
|
|
331
331
|
r"""Generate a summary of staged changes for PR descriptions.
|
|
332
332
|
|
|
333
333
|
Analyzes staged git changes and generates a structured PR description
|
rhiza/commands/__init__.py
CHANGED
|
@@ -50,6 +50,8 @@ For more detailed usage examples and workflows, see the USAGE.md guide
|
|
|
50
50
|
or try rhiza <command> --help
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
from .init import init
|
|
54
|
-
from .materialize import materialize
|
|
55
|
-
from .validate import validate
|
|
53
|
+
from .init import init
|
|
54
|
+
from .materialize import materialize
|
|
55
|
+
from .validate import validate
|
|
56
|
+
|
|
57
|
+
__all__ = ["init", "materialize", "validate"]
|
rhiza/commands/init.py
CHANGED
|
@@ -81,11 +81,27 @@ def _prompt_git_host() -> str:
|
|
|
81
81
|
git_host = "github"
|
|
82
82
|
logger.debug("Non-interactive mode detected, defaulting to github")
|
|
83
83
|
|
|
84
|
-
return git_host
|
|
84
|
+
return str(git_host)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_default_templates_for_host(git_host: str) -> list[str]:
|
|
88
|
+
"""Get default templates based on git hosting platform.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
git_host: Git hosting platform.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of template names.
|
|
95
|
+
"""
|
|
96
|
+
common = ["core", "tests", "docs"]
|
|
97
|
+
if git_host == "gitlab":
|
|
98
|
+
return [*common, "gitlab"]
|
|
99
|
+
else:
|
|
100
|
+
return [*common, "github"]
|
|
85
101
|
|
|
86
102
|
|
|
87
103
|
def _get_include_paths_for_host(git_host: str) -> list[str]:
|
|
88
|
-
"""Get include paths based on git hosting platform.
|
|
104
|
+
"""Get include paths based on git hosting platform (legacy, path-based).
|
|
89
105
|
|
|
90
106
|
Args:
|
|
91
107
|
git_host: Git hosting platform.
|
|
@@ -129,6 +145,7 @@ def _create_template_file(
|
|
|
129
145
|
git_host: str,
|
|
130
146
|
template_repository: str | None = None,
|
|
131
147
|
template_branch: str | None = None,
|
|
148
|
+
use_templates: bool = True,
|
|
132
149
|
) -> None:
|
|
133
150
|
"""Create default template.yml file.
|
|
134
151
|
|
|
@@ -137,6 +154,7 @@ def _create_template_file(
|
|
|
137
154
|
git_host: Git hosting platform.
|
|
138
155
|
template_repository: Custom template repository (format: owner/repo).
|
|
139
156
|
template_branch: Custom template branch.
|
|
157
|
+
use_templates: Use template-based configuration if True, path-based if False.
|
|
140
158
|
"""
|
|
141
159
|
rhiza_dir = target / ".rhiza"
|
|
142
160
|
template_file = rhiza_dir / "template.yml"
|
|
@@ -147,8 +165,6 @@ def _create_template_file(
|
|
|
147
165
|
logger.info("Creating default .rhiza/template.yml")
|
|
148
166
|
logger.debug("Using default template configuration")
|
|
149
167
|
|
|
150
|
-
include_paths = _get_include_paths_for_host(git_host)
|
|
151
|
-
|
|
152
168
|
# Use custom template repository/branch if provided, otherwise use defaults
|
|
153
169
|
repo = template_repository or "jebel-quant/rhiza"
|
|
154
170
|
branch = template_branch or "main"
|
|
@@ -159,11 +175,22 @@ def _create_template_file(
|
|
|
159
175
|
if template_branch:
|
|
160
176
|
logger.info(f"Using custom template branch: {branch}")
|
|
161
177
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
if use_templates:
|
|
179
|
+
templates = _get_default_templates_for_host(git_host)
|
|
180
|
+
logger.info(f"Using template-based configuration with templates: {', '.join(templates)}")
|
|
181
|
+
default_template = RhizaTemplate(
|
|
182
|
+
template_repository=repo,
|
|
183
|
+
template_branch=branch,
|
|
184
|
+
templates=templates,
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
include_paths = _get_include_paths_for_host(git_host)
|
|
188
|
+
logger.info("Using path-based configuration")
|
|
189
|
+
default_template = RhizaTemplate(
|
|
190
|
+
template_repository=repo,
|
|
191
|
+
template_branch=branch,
|
|
192
|
+
include=include_paths,
|
|
193
|
+
)
|
|
167
194
|
|
|
168
195
|
logger.debug(f"Writing default template to: {template_file}")
|
|
169
196
|
default_template.to_yaml(template_file)
|
|
@@ -263,7 +290,7 @@ def init(
|
|
|
263
290
|
git_host: str | None = None,
|
|
264
291
|
template_repository: str | None = None,
|
|
265
292
|
template_branch: str | None = None,
|
|
266
|
-
):
|
|
293
|
+
) -> bool:
|
|
267
294
|
"""Initialize or validate .rhiza/template.yml in the target repository.
|
|
268
295
|
|
|
269
296
|
Creates a default .rhiza/template.yml file if it doesn't exist,
|
rhiza/commands/materialize.py
CHANGED
|
@@ -15,11 +15,28 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
from loguru import logger
|
|
17
17
|
|
|
18
|
+
from rhiza.bundle_resolver import load_bundles_from_clone, resolve_include_paths
|
|
18
19
|
from rhiza.commands.validate import validate
|
|
19
20
|
from rhiza.models import RhizaTemplate
|
|
20
21
|
from rhiza.subprocess_utils import get_git_executable
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _log_git_stderr_errors(stderr: str | None) -> None:
|
|
25
|
+
"""Extract and log only relevant error messages from git stderr.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
stderr: Git command stderr output.
|
|
29
|
+
"""
|
|
30
|
+
if stderr:
|
|
31
|
+
# Extract relevant error message from git stderr
|
|
32
|
+
stderr_lines = stderr.strip().split("\n")
|
|
33
|
+
# Show only the most relevant error lines, skip verbose git output
|
|
34
|
+
for line in stderr_lines:
|
|
35
|
+
line = line.strip()
|
|
36
|
+
if line and (line.startswith("fatal:") or line.startswith("error:")):
|
|
37
|
+
logger.error(line)
|
|
38
|
+
|
|
39
|
+
|
|
23
40
|
def _handle_target_branch(
|
|
24
41
|
target: Path, target_branch: str | None, git_executable: str, git_env: dict[str, str]
|
|
25
42
|
) -> None:
|
|
@@ -52,6 +69,8 @@ def _handle_target_branch(
|
|
|
52
69
|
[git_executable, "checkout", target_branch],
|
|
53
70
|
cwd=target,
|
|
54
71
|
check=True,
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
55
74
|
env=git_env,
|
|
56
75
|
)
|
|
57
76
|
else:
|
|
@@ -61,10 +80,14 @@ def _handle_target_branch(
|
|
|
61
80
|
[git_executable, "checkout", "-b", target_branch],
|
|
62
81
|
cwd=target,
|
|
63
82
|
check=True,
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
64
85
|
env=git_env,
|
|
65
86
|
)
|
|
66
87
|
except subprocess.CalledProcessError as e:
|
|
67
|
-
logger.error(f"Failed to create/checkout branch '{target_branch}'
|
|
88
|
+
logger.error(f"Failed to create/checkout branch '{target_branch}'")
|
|
89
|
+
_log_git_stderr_errors(e.stderr)
|
|
90
|
+
logger.error("Please ensure you have no uncommitted changes or conflicts")
|
|
68
91
|
sys.exit(1)
|
|
69
92
|
|
|
70
93
|
|
|
@@ -91,20 +114,32 @@ def _validate_and_load_template(target: Path, branch: str) -> tuple[RhizaTemplat
|
|
|
91
114
|
|
|
92
115
|
# Extract template configuration settings
|
|
93
116
|
rhiza_repo = template.template_repository
|
|
117
|
+
if not rhiza_repo:
|
|
118
|
+
logger.error("template-repository is not configured in template.yml")
|
|
119
|
+
raise RuntimeError("template-repository is required") # noqa: TRY003
|
|
94
120
|
rhiza_branch = template.template_branch or branch
|
|
95
|
-
include_paths = template.include
|
|
96
121
|
excluded_paths = template.exclude
|
|
97
122
|
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
logger.error("Add at least one path to the 'include' list in template.yml")
|
|
102
|
-
raise RuntimeError("No include paths found in template.yml") # noqa: TRY003
|
|
123
|
+
# Note: We'll resolve templates to paths after cloning the template repo,
|
|
124
|
+
# since we need access to bundles.yml from the template
|
|
125
|
+
include_paths = template.include
|
|
103
126
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
logger.
|
|
127
|
+
# Validate that we have either templates or include paths
|
|
128
|
+
if not template.templates and not include_paths:
|
|
129
|
+
logger.error("No templates or include paths found in template.yml")
|
|
130
|
+
logger.error("Add either 'templates' or 'include' list in template.yml")
|
|
131
|
+
raise RuntimeError("No templates or include paths found in template.yml") # noqa: TRY003
|
|
132
|
+
|
|
133
|
+
# Log what we'll be using
|
|
134
|
+
if template.templates:
|
|
135
|
+
logger.info("Templates:")
|
|
136
|
+
for t in template.templates:
|
|
137
|
+
logger.info(f" - {t}")
|
|
138
|
+
|
|
139
|
+
if include_paths:
|
|
140
|
+
logger.info("Include paths:")
|
|
141
|
+
for p in include_paths:
|
|
142
|
+
logger.info(f" - {p}")
|
|
108
143
|
|
|
109
144
|
if excluded_paths:
|
|
110
145
|
logger.info("Exclude paths:")
|
|
@@ -140,6 +175,37 @@ def _construct_git_url(rhiza_repo: str, rhiza_host: str) -> str:
|
|
|
140
175
|
return git_url
|
|
141
176
|
|
|
142
177
|
|
|
178
|
+
def _update_sparse_checkout(
|
|
179
|
+
tmp_dir: Path,
|
|
180
|
+
include_paths: list[str],
|
|
181
|
+
git_executable: str,
|
|
182
|
+
git_env: dict[str, str],
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Update sparse checkout paths in an already-cloned repository.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
tmp_dir: Temporary directory with cloned repository.
|
|
188
|
+
include_paths: Paths to include in sparse checkout.
|
|
189
|
+
git_executable: Path to git executable.
|
|
190
|
+
git_env: Environment variables for git commands.
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
logger.debug(f"Updating sparse checkout paths: {include_paths}")
|
|
194
|
+
subprocess.run( # nosec B603
|
|
195
|
+
[git_executable, "sparse-checkout", "set", "--skip-checks", *include_paths],
|
|
196
|
+
cwd=tmp_dir,
|
|
197
|
+
check=True,
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
env=git_env,
|
|
201
|
+
)
|
|
202
|
+
logger.debug("Sparse checkout paths updated")
|
|
203
|
+
except subprocess.CalledProcessError as e:
|
|
204
|
+
logger.error("Failed to update sparse checkout paths")
|
|
205
|
+
_log_git_stderr_errors(e.stderr)
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
|
|
208
|
+
|
|
143
209
|
def _clone_template_repository(
|
|
144
210
|
tmp_dir: Path,
|
|
145
211
|
git_url: str,
|
|
@@ -154,7 +220,7 @@ def _clone_template_repository(
|
|
|
154
220
|
tmp_dir: Temporary directory for cloning.
|
|
155
221
|
git_url: Git repository URL.
|
|
156
222
|
rhiza_branch: Branch to clone.
|
|
157
|
-
include_paths:
|
|
223
|
+
include_paths: Initial paths to include in sparse checkout.
|
|
158
224
|
git_executable: Path to git executable.
|
|
159
225
|
git_env: Environment variables for git commands.
|
|
160
226
|
"""
|
|
@@ -181,11 +247,13 @@ def _clone_template_repository(
|
|
|
181
247
|
)
|
|
182
248
|
logger.debug("Git clone completed successfully")
|
|
183
249
|
except subprocess.CalledProcessError as e:
|
|
184
|
-
logger.error(f"Failed to clone repository
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
logger.error(
|
|
188
|
-
|
|
250
|
+
logger.error(f"Failed to clone repository from {git_url}")
|
|
251
|
+
_log_git_stderr_errors(e.stderr)
|
|
252
|
+
logger.error("Please check that:")
|
|
253
|
+
logger.error(" - The repository exists and is accessible")
|
|
254
|
+
logger.error(f" - Branch '{rhiza_branch}' exists in the repository")
|
|
255
|
+
logger.error(" - You have network access to the git hosting service")
|
|
256
|
+
sys.exit(1)
|
|
189
257
|
|
|
190
258
|
# Initialize sparse checkout in cone mode
|
|
191
259
|
try:
|
|
@@ -200,10 +268,9 @@ def _clone_template_repository(
|
|
|
200
268
|
)
|
|
201
269
|
logger.debug("Sparse checkout initialized")
|
|
202
270
|
except subprocess.CalledProcessError as e:
|
|
203
|
-
logger.error(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
raise
|
|
271
|
+
logger.error("Failed to initialize sparse checkout")
|
|
272
|
+
_log_git_stderr_errors(e.stderr)
|
|
273
|
+
sys.exit(1)
|
|
207
274
|
|
|
208
275
|
# Set sparse checkout paths
|
|
209
276
|
try:
|
|
@@ -218,10 +285,9 @@ def _clone_template_repository(
|
|
|
218
285
|
)
|
|
219
286
|
logger.debug("Sparse checkout paths configured")
|
|
220
287
|
except subprocess.CalledProcessError as e:
|
|
221
|
-
logger.error(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
raise
|
|
288
|
+
logger.error("Failed to configure sparse checkout paths")
|
|
289
|
+
_log_git_stderr_errors(e.stderr)
|
|
290
|
+
sys.exit(1)
|
|
225
291
|
|
|
226
292
|
|
|
227
293
|
def _copy_files_to_target(
|
|
@@ -479,7 +545,25 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
|
|
|
479
545
|
logger.debug(f"Temporary directory: {tmp_dir}")
|
|
480
546
|
|
|
481
547
|
try:
|
|
482
|
-
|
|
548
|
+
# Clone with initial minimal checkout to load bundles.yml if needed
|
|
549
|
+
initial_paths = [".rhiza"] if template.templates else include_paths
|
|
550
|
+
_clone_template_repository(tmp_dir, git_url, rhiza_branch, initial_paths, git_executable, git_env)
|
|
551
|
+
|
|
552
|
+
# Load bundles.yml and resolve templates to paths if using template mode
|
|
553
|
+
if template.templates:
|
|
554
|
+
logger.info("Resolving templates to file paths...")
|
|
555
|
+
try:
|
|
556
|
+
bundles_config = load_bundles_from_clone(tmp_dir)
|
|
557
|
+
resolved_paths = resolve_include_paths(template, bundles_config)
|
|
558
|
+
logger.info(f"Resolved {len(template.templates)} template(s) to {len(resolved_paths)} path(s)")
|
|
559
|
+
logger.debug(f"Resolved paths: {resolved_paths}")
|
|
560
|
+
# Update sparse checkout with resolved paths
|
|
561
|
+
_update_sparse_checkout(tmp_dir, resolved_paths, git_executable, git_env)
|
|
562
|
+
include_paths = resolved_paths
|
|
563
|
+
except ValueError as e:
|
|
564
|
+
logger.error(f"Failed to resolve templates: {e}")
|
|
565
|
+
sys.exit(1)
|
|
566
|
+
|
|
483
567
|
materialized_files = _copy_files_to_target(tmp_dir, target, include_paths, excluded_paths, force)
|
|
484
568
|
finally:
|
|
485
569
|
logger.debug(f"Cleaning up temporary directory: {tmp_dir}")
|
rhiza/commands/summarise.py
CHANGED
|
@@ -46,7 +46,7 @@ def get_staged_changes(repo_path: Path) -> dict[str, list[str]]:
|
|
|
46
46
|
Returns:
|
|
47
47
|
Dictionary with keys 'added', 'modified', 'deleted' containing file lists
|
|
48
48
|
"""
|
|
49
|
-
changes = {
|
|
49
|
+
changes: dict[str, list[str]] = {
|
|
50
50
|
"added": [],
|
|
51
51
|
"modified": [],
|
|
52
52
|
"deleted": [],
|
rhiza/commands/validate.py
CHANGED
|
@@ -5,8 +5,9 @@ This module provides functionality to validate template.yml files in the
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
|
-
import yaml
|
|
10
|
+
import yaml # type: ignore[import-untyped]
|
|
10
11
|
from loguru import logger
|
|
11
12
|
|
|
12
13
|
|
|
@@ -98,7 +99,7 @@ def _check_template_file_exists(target: Path) -> tuple[bool, Path]:
|
|
|
98
99
|
return True, template_file
|
|
99
100
|
|
|
100
101
|
|
|
101
|
-
def _parse_yaml_file(template_file: Path) -> tuple[bool, dict | None]:
|
|
102
|
+
def _parse_yaml_file(template_file: Path) -> tuple[bool, dict[str, Any] | None]:
|
|
102
103
|
"""Parse YAML file and return configuration.
|
|
103
104
|
|
|
104
105
|
Args:
|
|
@@ -125,7 +126,79 @@ def _parse_yaml_file(template_file: Path) -> tuple[bool, dict | None]:
|
|
|
125
126
|
return True, config
|
|
126
127
|
|
|
127
128
|
|
|
128
|
-
def
|
|
129
|
+
def _validate_configuration_mode(config: dict[str, Any]) -> bool:
|
|
130
|
+
"""Validate that at least one of templates or include is specified.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
config: Configuration dictionary.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if configuration mode is valid, False otherwise.
|
|
137
|
+
"""
|
|
138
|
+
logger.debug("Validating configuration mode")
|
|
139
|
+
has_templates = "templates" in config and config["templates"]
|
|
140
|
+
has_include = "include" in config and config["include"]
|
|
141
|
+
|
|
142
|
+
# Error if old "bundles" field is used
|
|
143
|
+
if "bundles" in config:
|
|
144
|
+
logger.error("Field 'bundles' has been renamed to 'templates'")
|
|
145
|
+
logger.error("Update your .rhiza/template.yml:")
|
|
146
|
+
logger.error(" bundles: [...] → templates: [...]")
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
# Require at least one of templates or include
|
|
150
|
+
if not has_templates and not has_include:
|
|
151
|
+
logger.error("Must specify at least one of 'templates' or 'include' in template.yml")
|
|
152
|
+
logger.error("Options:")
|
|
153
|
+
logger.error(" • Template-based: templates: [core, tests, github]")
|
|
154
|
+
logger.error(" • Path-based: include: [.rhiza, .github, ...]")
|
|
155
|
+
logger.error(" • Hybrid: specify both templates and include")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# Log what mode is being used
|
|
159
|
+
if has_templates and has_include:
|
|
160
|
+
logger.success("Using hybrid mode (templates + include)")
|
|
161
|
+
elif has_templates:
|
|
162
|
+
logger.success("Using template-based mode")
|
|
163
|
+
else:
|
|
164
|
+
logger.success("Using path-based mode")
|
|
165
|
+
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _validate_templates(config: dict[str, Any]) -> bool:
|
|
170
|
+
"""Validate templates field if present.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
config: Configuration dictionary.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
True if templates field is valid, False otherwise.
|
|
177
|
+
"""
|
|
178
|
+
logger.debug("Validating templates field")
|
|
179
|
+
if "templates" not in config:
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
templates = config["templates"]
|
|
183
|
+
if not isinstance(templates, list):
|
|
184
|
+
logger.error(f"templates must be a list, got {type(templates).__name__}")
|
|
185
|
+
logger.error("Example: templates: [core, tests, github]")
|
|
186
|
+
return False
|
|
187
|
+
elif len(templates) == 0:
|
|
188
|
+
logger.error("templates list cannot be empty")
|
|
189
|
+
logger.error("Add at least one template to materialize")
|
|
190
|
+
return False
|
|
191
|
+
else:
|
|
192
|
+
logger.success(f"templates list has {len(templates)} template(s)")
|
|
193
|
+
for template in templates:
|
|
194
|
+
if not isinstance(template, str):
|
|
195
|
+
logger.warning(f"template name should be a string, got {type(template).__name__}: {template}")
|
|
196
|
+
else:
|
|
197
|
+
logger.info(f" - {template}")
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _validate_required_fields(config: dict[str, Any]) -> bool:
|
|
129
202
|
"""Validate required fields exist and have correct types.
|
|
130
203
|
|
|
131
204
|
Args:
|
|
@@ -135,9 +208,10 @@ def _validate_required_fields(config: dict) -> bool:
|
|
|
135
208
|
True if all validations pass, False otherwise.
|
|
136
209
|
"""
|
|
137
210
|
logger.debug("Validating required fields")
|
|
211
|
+
# template-repository is required
|
|
212
|
+
# include or bundles is required (validated separately)
|
|
138
213
|
required_fields = {
|
|
139
214
|
"template-repository": str,
|
|
140
|
-
"include": list,
|
|
141
215
|
}
|
|
142
216
|
|
|
143
217
|
validation_passed = True
|
|
@@ -159,7 +233,7 @@ def _validate_required_fields(config: dict) -> bool:
|
|
|
159
233
|
return validation_passed
|
|
160
234
|
|
|
161
235
|
|
|
162
|
-
def _validate_repository_format(config: dict) -> bool:
|
|
236
|
+
def _validate_repository_format(config: dict[str, Any]) -> bool:
|
|
163
237
|
"""Validate template-repository format.
|
|
164
238
|
|
|
165
239
|
Args:
|
|
@@ -186,7 +260,7 @@ def _validate_repository_format(config: dict) -> bool:
|
|
|
186
260
|
return True
|
|
187
261
|
|
|
188
262
|
|
|
189
|
-
def _validate_include_paths(config: dict) -> bool:
|
|
263
|
+
def _validate_include_paths(config: dict[str, Any]) -> bool:
|
|
190
264
|
"""Validate include paths.
|
|
191
265
|
|
|
192
266
|
Args:
|
|
@@ -218,7 +292,7 @@ def _validate_include_paths(config: dict) -> bool:
|
|
|
218
292
|
return True
|
|
219
293
|
|
|
220
294
|
|
|
221
|
-
def _validate_optional_fields(config: dict) -> None:
|
|
295
|
+
def _validate_optional_fields(config: dict[str, Any]) -> None:
|
|
222
296
|
"""Validate optional fields if present.
|
|
223
297
|
|
|
224
298
|
Args:
|
|
@@ -304,6 +378,10 @@ def validate(target: Path) -> bool:
|
|
|
304
378
|
if not success or config is None:
|
|
305
379
|
return False
|
|
306
380
|
|
|
381
|
+
# Validate configuration mode (templates OR include)
|
|
382
|
+
if not _validate_configuration_mode(config):
|
|
383
|
+
return False
|
|
384
|
+
|
|
307
385
|
# Validate required fields
|
|
308
386
|
validation_passed = _validate_required_fields(config)
|
|
309
387
|
|
|
@@ -311,8 +389,15 @@ def validate(target: Path) -> bool:
|
|
|
311
389
|
if not _validate_repository_format(config):
|
|
312
390
|
validation_passed = False
|
|
313
391
|
|
|
314
|
-
if
|
|
315
|
-
|
|
392
|
+
# Validate templates if present
|
|
393
|
+
if config.get("templates"):
|
|
394
|
+
if not _validate_templates(config):
|
|
395
|
+
validation_passed = False
|
|
396
|
+
|
|
397
|
+
# Validate include if present
|
|
398
|
+
if config.get("include"):
|
|
399
|
+
if not _validate_include_paths(config):
|
|
400
|
+
validation_passed = False
|
|
316
401
|
|
|
317
402
|
# Validate optional fields
|
|
318
403
|
_validate_optional_fields(config)
|
rhiza/commands/welcome.py
CHANGED
|
@@ -10,7 +10,7 @@ and explains what Rhiza is and how it can help manage configuration templates.
|
|
|
10
10
|
from rhiza import __version__
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def welcome():
|
|
13
|
+
def welcome() -> None:
|
|
14
14
|
"""Display a welcome message and explain what Rhiza is.
|
|
15
15
|
|
|
16
16
|
Shows a friendly greeting, explains Rhiza's purpose, and provides
|
rhiza/models.py
CHANGED
|
@@ -7,12 +7,19 @@ YAML parsing.
|
|
|
7
7
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
10
11
|
|
|
11
|
-
import yaml
|
|
12
|
+
import yaml # type: ignore[import-untyped]
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BundleDefinition",
|
|
16
|
+
"RhizaBundles",
|
|
17
|
+
"RhizaTemplate",
|
|
18
|
+
]
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
def _normalize_to_list(value: str | list[str] | None) -> list[str]:
|
|
15
|
-
"""Convert a value to a list of strings.
|
|
22
|
+
r"""Convert a value to a list of strings.
|
|
16
23
|
|
|
17
24
|
Handles the case where YAML multi-line strings (using |) are parsed as
|
|
18
25
|
a single string instead of a list. Splits the string by newlines and
|
|
@@ -23,6 +30,20 @@ def _normalize_to_list(value: str | list[str] | None) -> list[str]:
|
|
|
23
30
|
|
|
24
31
|
Returns:
|
|
25
32
|
A list of strings. Empty list if value is None or empty.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
>>> _normalize_to_list(None)
|
|
36
|
+
[]
|
|
37
|
+
>>> _normalize_to_list([])
|
|
38
|
+
[]
|
|
39
|
+
>>> _normalize_to_list(['a', 'b', 'c'])
|
|
40
|
+
['a', 'b', 'c']
|
|
41
|
+
>>> _normalize_to_list('single line')
|
|
42
|
+
['single line']
|
|
43
|
+
>>> _normalize_to_list('line1\\n' + 'line2\\n' + 'line3')
|
|
44
|
+
['line1', 'line2', 'line3']
|
|
45
|
+
>>> _normalize_to_list(' item1 \\n' + ' item2 ')
|
|
46
|
+
['item1', 'item2']
|
|
26
47
|
"""
|
|
27
48
|
if value is None:
|
|
28
49
|
return []
|
|
@@ -30,10 +51,173 @@ def _normalize_to_list(value: str | list[str] | None) -> list[str]:
|
|
|
30
51
|
return value
|
|
31
52
|
if isinstance(value, str):
|
|
32
53
|
# Split by newlines and filter out empty strings
|
|
33
|
-
|
|
54
|
+
# Handle both actual newlines (\n) and literal backslash-n (\\n)
|
|
55
|
+
if "\\n" in value and "\n" not in value:
|
|
56
|
+
# Contains literal \n but not actual newlines
|
|
57
|
+
items = value.split("\\n")
|
|
58
|
+
else:
|
|
59
|
+
# Contains actual newlines or neither
|
|
60
|
+
items = value.split("\n")
|
|
61
|
+
return [item.strip() for item in items if item.strip()]
|
|
34
62
|
return []
|
|
35
63
|
|
|
36
64
|
|
|
65
|
+
@dataclass
|
|
66
|
+
class BundleDefinition:
|
|
67
|
+
"""Represents a single bundle from bundles.yml.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
name: The bundle identifier (e.g., "core", "tests", "github").
|
|
71
|
+
description: Human-readable description of the bundle.
|
|
72
|
+
files: List of file paths included in this bundle.
|
|
73
|
+
workflows: List of workflow file paths included in this bundle.
|
|
74
|
+
depends_on: List of bundle names that this bundle depends on.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
name: str
|
|
78
|
+
description: str
|
|
79
|
+
files: list[str] = field(default_factory=list)
|
|
80
|
+
workflows: list[str] = field(default_factory=list)
|
|
81
|
+
depends_on: list[str] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
def all_paths(self) -> list[str]:
|
|
84
|
+
"""Return combined files and workflows."""
|
|
85
|
+
return self.files + self.workflows
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class RhizaBundles:
|
|
90
|
+
"""Represents the structure of bundles.yml.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
version: Version string of the bundles configuration format.
|
|
94
|
+
bundles: Dictionary mapping bundle names to their definitions.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
version: str
|
|
98
|
+
bundles: dict[str, BundleDefinition] = field(default_factory=dict)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_yaml(cls, file_path: Path) -> "RhizaBundles":
|
|
102
|
+
"""Load RhizaBundles from a YAML file.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
file_path: Path to the bundles.yml file.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The loaded bundles configuration.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
FileNotFoundError: If the file does not exist.
|
|
112
|
+
yaml.YAMLError: If the YAML is malformed.
|
|
113
|
+
ValueError: If the file is invalid or missing required fields.
|
|
114
|
+
TypeError: If bundle data has invalid types.
|
|
115
|
+
"""
|
|
116
|
+
with open(file_path) as f:
|
|
117
|
+
config = yaml.safe_load(f)
|
|
118
|
+
|
|
119
|
+
if not config:
|
|
120
|
+
raise ValueError("Bundles file is empty") # noqa: TRY003
|
|
121
|
+
|
|
122
|
+
version = config.get("version")
|
|
123
|
+
if not version:
|
|
124
|
+
raise ValueError("Bundles file missing required field: version") # noqa: TRY003
|
|
125
|
+
|
|
126
|
+
bundles_config = config.get("bundles", {})
|
|
127
|
+
if not isinstance(bundles_config, dict):
|
|
128
|
+
msg = "Bundles must be a dictionary"
|
|
129
|
+
raise TypeError(msg)
|
|
130
|
+
|
|
131
|
+
bundles: dict[str, BundleDefinition] = {}
|
|
132
|
+
for bundle_name, bundle_data in bundles_config.items():
|
|
133
|
+
if not isinstance(bundle_data, dict):
|
|
134
|
+
msg = f"Bundle '{bundle_name}' must be a dictionary"
|
|
135
|
+
raise TypeError(msg)
|
|
136
|
+
|
|
137
|
+
files = _normalize_to_list(bundle_data.get("files"))
|
|
138
|
+
workflows = _normalize_to_list(bundle_data.get("workflows"))
|
|
139
|
+
depends_on = _normalize_to_list(bundle_data.get("depends-on"))
|
|
140
|
+
|
|
141
|
+
bundles[bundle_name] = BundleDefinition(
|
|
142
|
+
name=bundle_name,
|
|
143
|
+
description=bundle_data.get("description", ""),
|
|
144
|
+
files=files,
|
|
145
|
+
workflows=workflows,
|
|
146
|
+
depends_on=depends_on,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return cls(version=version, bundles=bundles)
|
|
150
|
+
|
|
151
|
+
def resolve_dependencies(self, bundle_names: list[str]) -> list[str]:
|
|
152
|
+
"""Resolve bundle dependencies using topological sort.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
bundle_names: List of bundle names to resolve.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Ordered list of bundle names with dependencies first, no duplicates.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: If a bundle doesn't exist or circular dependency detected.
|
|
162
|
+
"""
|
|
163
|
+
# Validate all bundles exist
|
|
164
|
+
for name in bundle_names:
|
|
165
|
+
if name not in self.bundles:
|
|
166
|
+
raise ValueError(f"Bundle '{name}' not found in bundles.yml") # noqa: TRY003
|
|
167
|
+
|
|
168
|
+
resolved: list[str] = []
|
|
169
|
+
visiting: set[str] = set()
|
|
170
|
+
visited: set[str] = set()
|
|
171
|
+
|
|
172
|
+
def visit(bundle_name: str) -> None:
|
|
173
|
+
if bundle_name in visited:
|
|
174
|
+
return
|
|
175
|
+
if bundle_name in visiting:
|
|
176
|
+
raise ValueError(f"Circular dependency detected involving '{bundle_name}'") # noqa: TRY003
|
|
177
|
+
|
|
178
|
+
visiting.add(bundle_name)
|
|
179
|
+
bundle = self.bundles[bundle_name]
|
|
180
|
+
|
|
181
|
+
for dep in bundle.depends_on:
|
|
182
|
+
if dep not in self.bundles:
|
|
183
|
+
raise ValueError(f"Bundle '{bundle_name}' depends on unknown bundle '{dep}'") # noqa: TRY003
|
|
184
|
+
visit(dep)
|
|
185
|
+
|
|
186
|
+
visiting.remove(bundle_name)
|
|
187
|
+
visited.add(bundle_name)
|
|
188
|
+
resolved.append(bundle_name)
|
|
189
|
+
|
|
190
|
+
for name in bundle_names:
|
|
191
|
+
visit(name)
|
|
192
|
+
|
|
193
|
+
return resolved
|
|
194
|
+
|
|
195
|
+
def resolve_to_paths(self, bundle_names: list[str]) -> list[str]:
|
|
196
|
+
"""Convert bundle names to deduplicated file paths.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
bundle_names: List of bundle names to resolve.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Deduplicated list of file paths from all bundles and their dependencies.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: If a bundle doesn't exist or circular dependency detected.
|
|
206
|
+
"""
|
|
207
|
+
resolved_bundles = self.resolve_dependencies(bundle_names)
|
|
208
|
+
paths: list[str] = []
|
|
209
|
+
seen: set[str] = set()
|
|
210
|
+
|
|
211
|
+
for bundle_name in resolved_bundles:
|
|
212
|
+
bundle = self.bundles[bundle_name]
|
|
213
|
+
for path in bundle.all_paths():
|
|
214
|
+
if path not in seen:
|
|
215
|
+
paths.append(path)
|
|
216
|
+
seen.add(path)
|
|
217
|
+
|
|
218
|
+
return paths
|
|
219
|
+
|
|
220
|
+
|
|
37
221
|
@dataclass
|
|
38
222
|
class RhizaTemplate:
|
|
39
223
|
"""Represents the structure of .rhiza/template.yml.
|
|
@@ -45,8 +229,10 @@ class RhizaTemplate:
|
|
|
45
229
|
Can be None if not specified in the template file (defaults to "main" when creating).
|
|
46
230
|
template_host: The git hosting platform ("github" or "gitlab").
|
|
47
231
|
Defaults to "github" if not specified in the template file.
|
|
48
|
-
include: List of paths to include from the template repository.
|
|
232
|
+
include: List of paths to include from the template repository (path-based mode).
|
|
49
233
|
exclude: List of paths to exclude from the template repository (default: empty list).
|
|
234
|
+
templates: List of template names to include (template-based mode).
|
|
235
|
+
Can be used together with include to merge paths.
|
|
50
236
|
"""
|
|
51
237
|
|
|
52
238
|
template_repository: str | None = None
|
|
@@ -54,6 +240,7 @@ class RhizaTemplate:
|
|
|
54
240
|
template_host: str = "github"
|
|
55
241
|
include: list[str] = field(default_factory=list)
|
|
56
242
|
exclude: list[str] = field(default_factory=list)
|
|
243
|
+
templates: list[str] = field(default_factory=list)
|
|
57
244
|
|
|
58
245
|
@classmethod
|
|
59
246
|
def from_yaml(cls, file_path: Path) -> "RhizaTemplate":
|
|
@@ -82,6 +269,7 @@ class RhizaTemplate:
|
|
|
82
269
|
template_host=config.get("template-host", "github"),
|
|
83
270
|
include=_normalize_to_list(config.get("include")),
|
|
84
271
|
exclude=_normalize_to_list(config.get("exclude")),
|
|
272
|
+
templates=_normalize_to_list(config.get("templates")),
|
|
85
273
|
)
|
|
86
274
|
|
|
87
275
|
def to_yaml(self, file_path: Path) -> None:
|
|
@@ -94,7 +282,7 @@ class RhizaTemplate:
|
|
|
94
282
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
95
283
|
|
|
96
284
|
# Convert to dictionary with YAML-compatible keys
|
|
97
|
-
config = {}
|
|
285
|
+
config: dict[str, Any] = {}
|
|
98
286
|
|
|
99
287
|
# Only include template-repository if it's not None
|
|
100
288
|
if self.template_repository:
|
|
@@ -108,8 +296,13 @@ class RhizaTemplate:
|
|
|
108
296
|
if self.template_host and self.template_host != "github":
|
|
109
297
|
config["template-host"] = self.template_host
|
|
110
298
|
|
|
111
|
-
#
|
|
112
|
-
|
|
299
|
+
# Write templates if present
|
|
300
|
+
if self.templates:
|
|
301
|
+
config["templates"] = self.templates
|
|
302
|
+
|
|
303
|
+
# Write include if present (can coexist with templates)
|
|
304
|
+
if self.include:
|
|
305
|
+
config["include"] = self.include
|
|
113
306
|
|
|
114
307
|
# Only include exclude if it's not empty
|
|
115
308
|
if self.exclude:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rhiza
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Reusable configuration templates for modern Python projects
|
|
5
5
|
Project-URL: Homepage, https://github.com/jebel-quant/rhiza-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/jebel-quant/rhiza-cli
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
<div align="center">
|
|
31
31
|
|
|
32
|
-
# <img src="https://raw.githubusercontent.com/Jebel-Quant/rhiza/main/assets/rhiza-logo.svg" alt="Rhiza Logo" width="30" style="vertical-align: middle;"> rhiza-cli
|
|
32
|
+
# <img src="https://raw.githubusercontent.com/Jebel-Quant/rhiza/main/.rhiza/assets/rhiza-logo.svg" alt="Rhiza Logo" width="30" style="vertical-align: middle;"> rhiza-cli
|
|
33
33
|

|
|
34
34
|
|
|
35
35
|
[](https://www.python.org/downloads/)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
rhiza/__init__.py,sha256=4-Dy7AKbJneQNBfv30WhSsUin4y-g4Yp4veO6-YdjVg,1926
|
|
2
|
+
rhiza/__main__.py,sha256=GlTahVC8F6fKEpMw_nZyhp4PB-fyC9UQ_RjTZcpD9P4,837
|
|
3
|
+
rhiza/bundle_resolver.py,sha256=_gF2LQqvYfwMCbMKBvlyPr6vfo92463LB6zu948jPu4,2230
|
|
4
|
+
rhiza/cli.py,sha256=y2qzFbrd29-j5OXslM96hZB9vDLvLv5FBnR9SnDdQWE,10613
|
|
5
|
+
rhiza/models.py,sha256=kyrrcbVm5XeceQIY1Ug83TLd2Dd5R725ulOf-VB86yI,10803
|
|
6
|
+
rhiza/subprocess_utils.py,sha256=Pr5TysIKP76hc64fmqhTd6msMGn5DU43hOSR_v_GFb8,745
|
|
7
|
+
rhiza/_templates/basic/__init__.py.jinja2,sha256=gs8qN4LAKcdFd6iO9gZVLuVetODmZP_TGuEjWrbinC0,27
|
|
8
|
+
rhiza/_templates/basic/main.py.jinja2,sha256=uTCahxf9Bftao1IghHue4cSZ9YzBYmBEXeIhEmK9UXQ,362
|
|
9
|
+
rhiza/_templates/basic/pyproject.toml.jinja2,sha256=Mizpnnd_kFQd-pCWOxG-KWhvg4_ZhZaQppTt2pz0WOc,695
|
|
10
|
+
rhiza/commands/__init__.py,sha256=DV1nlcXxeeHXmHobVcfOsGeiZBZ55Xz1mud073AXDGc,1839
|
|
11
|
+
rhiza/commands/init.py,sha256=fD_Nc-EtNo9Fb0PVRLkN2RYMteVv_BUTinAa2jz0_Q4,10745
|
|
12
|
+
rhiza/commands/materialize.py,sha256=oR3OP6vhLlbKPqtFahCyLFCFDksbumNSy6C8KZ2bD44,22229
|
|
13
|
+
rhiza/commands/migrate.py,sha256=A5t4nw7CrdtILCwuSoAqtmM0LpMK8KmX87gzlNgi7fQ,7522
|
|
14
|
+
rhiza/commands/summarise.py,sha256=vgc7M3dwuGjUuVs3XKQr2_g3qURnQH_R3eFoSSrnOro,11758
|
|
15
|
+
rhiza/commands/uninstall.py,sha256=6oO7kdv11Bq4JXjrBg9rsFtoRgttQ4m30zGr6NhZrkQ,7479
|
|
16
|
+
rhiza/commands/validate.py,sha256=_yGx7ARk5K4hqfwzPpJg_7JoG4un0lqHJYJU-4cmYvI,13948
|
|
17
|
+
rhiza/commands/welcome.py,sha256=VknKaUh-bD6dM-zcD1eP4H-D_npyezpfF3bTSl_0Dio,2383
|
|
18
|
+
rhiza-0.10.0.dist-info/METADATA,sha256=T96351Csc2Tu1s75Z57aRJ1JDqZqax5PXr4xZ3BDWCQ,26306
|
|
19
|
+
rhiza-0.10.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
20
|
+
rhiza-0.10.0.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
|
|
21
|
+
rhiza-0.10.0.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
|
|
22
|
+
rhiza-0.10.0.dist-info/RECORD,,
|
rhiza-0.9.0.dist-info/RECORD
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
rhiza/__init__.py,sha256=4-Dy7AKbJneQNBfv30WhSsUin4y-g4Yp4veO6-YdjVg,1926
|
|
2
|
-
rhiza/__main__.py,sha256=Q02upTGaJceknkDABdCwq5_vdMdGY8Cg3ej6WZIHs_s,829
|
|
3
|
-
rhiza/cli.py,sha256=uU0tenEgTe0Wz5lIj0SZIeXy5n_9-8xtUMMP_i71CWA,10609
|
|
4
|
-
rhiza/models.py,sha256=f4TT3XPMEE7ciyrpsXllje4eGJRkYOgpZOoFeRC-rVI,4225
|
|
5
|
-
rhiza/subprocess_utils.py,sha256=Pr5TysIKP76hc64fmqhTd6msMGn5DU43hOSR_v_GFb8,745
|
|
6
|
-
rhiza/_templates/basic/__init__.py.jinja2,sha256=gs8qN4LAKcdFd6iO9gZVLuVetODmZP_TGuEjWrbinC0,27
|
|
7
|
-
rhiza/_templates/basic/main.py.jinja2,sha256=uTCahxf9Bftao1IghHue4cSZ9YzBYmBEXeIhEmK9UXQ,362
|
|
8
|
-
rhiza/_templates/basic/pyproject.toml.jinja2,sha256=Mizpnnd_kFQd-pCWOxG-KWhvg4_ZhZaQppTt2pz0WOc,695
|
|
9
|
-
rhiza/commands/__init__.py,sha256=QWEEVvdW3gKV-FpKgHRJL_H8FpQqvfck9JvnFMDz3gY,1834
|
|
10
|
-
rhiza/commands/init.py,sha256=dpLwFbURyndkgw-v4O6gD0gg6ov6q023uOmaWGup2oA,9785
|
|
11
|
-
rhiza/commands/materialize.py,sha256=1YmzlPz9-IxnV4znDZQijZ3cjfbBXdepmps963z4Hsg,18668
|
|
12
|
-
rhiza/commands/migrate.py,sha256=A5t4nw7CrdtILCwuSoAqtmM0LpMK8KmX87gzlNgi7fQ,7522
|
|
13
|
-
rhiza/commands/summarise.py,sha256=KJ8v4lHSbn8AIt3tJM_lSCizzZ2Bv2mqTfI0z7-kyxI,11736
|
|
14
|
-
rhiza/commands/uninstall.py,sha256=6oO7kdv11Bq4JXjrBg9rsFtoRgttQ4m30zGr6NhZrkQ,7479
|
|
15
|
-
rhiza/commands/validate.py,sha256=VriXJxyuhvGJajw0qiAUmXYKRMbpgKiZ5_MMtE6WlSo,10801
|
|
16
|
-
rhiza/commands/welcome.py,sha256=u197cIlY1tXm-CN6YpyUX4Eq06pLeV0hGyNvT35tE8U,2375
|
|
17
|
-
rhiza-0.9.0.dist-info/METADATA,sha256=6AIK6bVC54vxBE0NB9jdLkBuTUcn9W0jod6ixm1-EB0,26298
|
|
18
|
-
rhiza-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
-
rhiza-0.9.0.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
|
|
20
|
-
rhiza-0.9.0.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
|
|
21
|
-
rhiza-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|