rhiza 0.8.8__py3-none-any.whl → 0.9.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/__init__.py +4 -4
- rhiza/cli.py +113 -40
- rhiza/commands/__init__.py +2 -2
- rhiza/commands/init.py +32 -9
- rhiza/commands/materialize.py +9 -9
- rhiza/commands/migrate.py +1 -1
- rhiza/commands/summarise.py +416 -0
- rhiza/commands/uninstall.py +2 -1
- rhiza/commands/validate.py +1 -1
- rhiza/commands/welcome.py +1 -1
- rhiza/models.py +2 -2
- {rhiza-0.8.8.dist-info → rhiza-0.9.0.dist-info}/METADATA +46 -26
- rhiza-0.9.0.dist-info/RECORD +21 -0
- rhiza-0.8.8.dist-info/RECORD +0 -20
- {rhiza-0.8.8.dist-info → rhiza-0.9.0.dist-info}/WHEEL +0 -0
- {rhiza-0.8.8.dist-info → rhiza-0.9.0.dist-info}/entry_points.txt +0 -0
- {rhiza-0.8.8.dist-info → rhiza-0.9.0.dist-info}/licenses/LICENSE +0 -0
rhiza/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Rhiza — Manage reusable configuration templates for Python projects.
|
|
2
2
|
|
|
3
|
-
Rhiza is a command
|
|
3
|
+
Rhiza is a command-line interface (CLI) that helps you maintain consistent
|
|
4
4
|
configuration across multiple Python projects using templates stored in a
|
|
5
5
|
central repository. It can initialize projects with standard configuration,
|
|
6
6
|
materialize (inject) template files into a target repository, and validate the
|
|
@@ -11,8 +11,8 @@ template configuration.
|
|
|
11
11
|
- Template initialization for new or existing projects.
|
|
12
12
|
- Template materialization with selective include/exclude support.
|
|
13
13
|
- Configuration validation (syntax and basic semantics).
|
|
14
|
-
- Multi
|
|
15
|
-
- Non
|
|
14
|
+
- Multi-host support (GitHub and GitLab).
|
|
15
|
+
- Non-destructive updates by default, with an explicit `--force` flag.
|
|
16
16
|
|
|
17
17
|
## Quick start
|
|
18
18
|
|
|
@@ -29,7 +29,7 @@ Validate your configuration:
|
|
|
29
29
|
rhiza validate
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Customize `.
|
|
32
|
+
Customize `.rhiza/template.yml`, then materialize templates into your project:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
rhiza materialize
|
rhiza/cli.py
CHANGED
|
@@ -5,6 +5,7 @@ Commands are thin wrappers around implementations in `rhiza.commands.*`.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import Annotated
|
|
8
9
|
|
|
9
10
|
import typer
|
|
10
11
|
|
|
@@ -13,6 +14,7 @@ from rhiza.commands import init as init_cmd
|
|
|
13
14
|
from rhiza.commands import materialize as materialize_cmd
|
|
14
15
|
from rhiza.commands import validate as validate_cmd
|
|
15
16
|
from rhiza.commands.migrate import migrate as migrate_cmd
|
|
17
|
+
from rhiza.commands.summarise import summarise as summarise_cmd
|
|
16
18
|
from rhiza.commands.uninstall import uninstall as uninstall_cmd
|
|
17
19
|
from rhiza.commands.welcome import welcome as welcome_cmd
|
|
18
20
|
|
|
@@ -65,13 +67,15 @@ def main(
|
|
|
65
67
|
|
|
66
68
|
@app.command()
|
|
67
69
|
def init(
|
|
68
|
-
target:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
target: Annotated[
|
|
71
|
+
Path,
|
|
72
|
+
typer.Argument(
|
|
73
|
+
exists=True,
|
|
74
|
+
file_okay=False,
|
|
75
|
+
dir_okay=True,
|
|
76
|
+
help="Target directory (defaults to current directory)",
|
|
77
|
+
),
|
|
78
|
+
] = Path("."),
|
|
75
79
|
project_name: str = typer.Option(
|
|
76
80
|
None,
|
|
77
81
|
"--project-name",
|
|
@@ -93,10 +97,20 @@ def init(
|
|
|
93
97
|
help="Target Git hosting platform (github or gitlab). Determines which CI/CD files to include. "
|
|
94
98
|
"If not provided, will prompt interactively.",
|
|
95
99
|
),
|
|
100
|
+
template_repository: str = typer.Option(
|
|
101
|
+
None,
|
|
102
|
+
"--template-repository",
|
|
103
|
+
help="Custom template repository (format: owner/repo). Defaults to 'jebel-quant/rhiza'.",
|
|
104
|
+
),
|
|
105
|
+
template_branch: str = typer.Option(
|
|
106
|
+
None,
|
|
107
|
+
"--template-branch",
|
|
108
|
+
help="Custom template branch. Defaults to 'main'.",
|
|
109
|
+
),
|
|
96
110
|
):
|
|
97
|
-
r"""Initialize or validate .
|
|
111
|
+
r"""Initialize or validate .rhiza/template.yml.
|
|
98
112
|
|
|
99
|
-
Creates a default `.
|
|
113
|
+
Creates a default `.rhiza/template.yml` configuration file if one
|
|
100
114
|
doesn't exist, or validates the existing configuration.
|
|
101
115
|
|
|
102
116
|
The default template includes common Python project files.
|
|
@@ -108,6 +122,8 @@ def init(
|
|
|
108
122
|
rhiza init
|
|
109
123
|
rhiza init --git-host github
|
|
110
124
|
rhiza init --git-host gitlab
|
|
125
|
+
rhiza init --template-repository myorg/my-templates
|
|
126
|
+
rhiza init --template-repository myorg/my-templates --template-branch develop
|
|
111
127
|
rhiza init /path/to/project
|
|
112
128
|
rhiza init ..
|
|
113
129
|
"""
|
|
@@ -117,18 +133,22 @@ def init(
|
|
|
117
133
|
package_name=package_name,
|
|
118
134
|
with_dev_dependencies=with_dev_dependencies,
|
|
119
135
|
git_host=git_host,
|
|
136
|
+
template_repository=template_repository,
|
|
137
|
+
template_branch=template_branch,
|
|
120
138
|
)
|
|
121
139
|
|
|
122
140
|
|
|
123
141
|
@app.command()
|
|
124
142
|
def materialize(
|
|
125
|
-
target:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
target: Annotated[
|
|
144
|
+
Path,
|
|
145
|
+
typer.Argument(
|
|
146
|
+
exists=True,
|
|
147
|
+
file_okay=False,
|
|
148
|
+
dir_okay=True,
|
|
149
|
+
help="Target git repository (defaults to current directory)",
|
|
150
|
+
),
|
|
151
|
+
] = Path("."),
|
|
132
152
|
branch: str = typer.Option("main", "--branch", "-b", help="Rhiza branch to use"),
|
|
133
153
|
target_branch: str = typer.Option(
|
|
134
154
|
None,
|
|
@@ -141,9 +161,9 @@ def materialize(
|
|
|
141
161
|
r"""Inject Rhiza configuration templates into a target repository.
|
|
142
162
|
|
|
143
163
|
Materializes configuration files from the template repository specified
|
|
144
|
-
in .
|
|
164
|
+
in .rhiza/template.yml into your project. This command:
|
|
145
165
|
|
|
146
|
-
- Reads .
|
|
166
|
+
- Reads .rhiza/template.yml configuration
|
|
147
167
|
- Performs a sparse clone of the template repository
|
|
148
168
|
- Copies specified files/directories to your project
|
|
149
169
|
- Respects exclusion patterns defined in the configuration
|
|
@@ -161,17 +181,19 @@ def materialize(
|
|
|
161
181
|
|
|
162
182
|
@app.command()
|
|
163
183
|
def validate(
|
|
164
|
-
target:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
184
|
+
target: Annotated[
|
|
185
|
+
Path,
|
|
186
|
+
typer.Argument(
|
|
187
|
+
exists=True,
|
|
188
|
+
file_okay=False,
|
|
189
|
+
dir_okay=True,
|
|
190
|
+
help="Target git repository (defaults to current directory)",
|
|
191
|
+
),
|
|
192
|
+
] = Path("."),
|
|
171
193
|
):
|
|
172
194
|
r"""Validate Rhiza template configuration.
|
|
173
195
|
|
|
174
|
-
Validates the .
|
|
196
|
+
Validates the .rhiza/template.yml file to ensure it is syntactically
|
|
175
197
|
correct and semantically valid.
|
|
176
198
|
|
|
177
199
|
Performs comprehensive validation:
|
|
@@ -196,13 +218,15 @@ def validate(
|
|
|
196
218
|
|
|
197
219
|
@app.command()
|
|
198
220
|
def migrate(
|
|
199
|
-
target:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
target: Annotated[
|
|
222
|
+
Path,
|
|
223
|
+
typer.Argument(
|
|
224
|
+
exists=True,
|
|
225
|
+
file_okay=False,
|
|
226
|
+
dir_okay=True,
|
|
227
|
+
help="Target git repository (defaults to current directory)",
|
|
228
|
+
),
|
|
229
|
+
] = Path("."),
|
|
206
230
|
):
|
|
207
231
|
r"""Migrate project to the new .rhiza folder structure.
|
|
208
232
|
|
|
@@ -243,13 +267,15 @@ def welcome():
|
|
|
243
267
|
|
|
244
268
|
@app.command()
|
|
245
269
|
def uninstall(
|
|
246
|
-
target:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
270
|
+
target: Annotated[
|
|
271
|
+
Path,
|
|
272
|
+
typer.Argument(
|
|
273
|
+
exists=True,
|
|
274
|
+
file_okay=False,
|
|
275
|
+
dir_okay=True,
|
|
276
|
+
help="Target git repository (defaults to current directory)",
|
|
277
|
+
),
|
|
278
|
+
] = Path("."),
|
|
253
279
|
force: bool = typer.Option(
|
|
254
280
|
False,
|
|
255
281
|
"--force",
|
|
@@ -280,3 +306,50 @@ def uninstall(
|
|
|
280
306
|
rhiza uninstall /path/to/project -y
|
|
281
307
|
"""
|
|
282
308
|
uninstall_cmd(target, force)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@app.command()
|
|
312
|
+
def summarise(
|
|
313
|
+
target: Annotated[
|
|
314
|
+
Path,
|
|
315
|
+
typer.Argument(
|
|
316
|
+
exists=True,
|
|
317
|
+
file_okay=False,
|
|
318
|
+
dir_okay=True,
|
|
319
|
+
help="Target git repository (defaults to current directory)",
|
|
320
|
+
),
|
|
321
|
+
] = Path("."),
|
|
322
|
+
output: Annotated[
|
|
323
|
+
Path | None,
|
|
324
|
+
typer.Option(
|
|
325
|
+
"--output",
|
|
326
|
+
"-o",
|
|
327
|
+
help="Output file path (defaults to stdout)",
|
|
328
|
+
),
|
|
329
|
+
] = None,
|
|
330
|
+
):
|
|
331
|
+
r"""Generate a summary of staged changes for PR descriptions.
|
|
332
|
+
|
|
333
|
+
Analyzes staged git changes and generates a structured PR description
|
|
334
|
+
that includes:
|
|
335
|
+
|
|
336
|
+
- Summary statistics (files added/modified/deleted)
|
|
337
|
+
- Changes categorized by type (workflows, configs, docs, tests, etc.)
|
|
338
|
+
- Template repository information
|
|
339
|
+
- Last sync date
|
|
340
|
+
|
|
341
|
+
This is useful when creating pull requests after running `rhiza materialize`
|
|
342
|
+
to provide reviewers with a clear overview of what changed.
|
|
343
|
+
|
|
344
|
+
Examples:
|
|
345
|
+
rhiza summarise
|
|
346
|
+
rhiza summarise --output pr-description.md
|
|
347
|
+
rhiza summarise /path/to/project -o description.md
|
|
348
|
+
|
|
349
|
+
Typical workflow:
|
|
350
|
+
rhiza materialize
|
|
351
|
+
git add .
|
|
352
|
+
rhiza summarise --output pr-body.md
|
|
353
|
+
gh pr create --title "chore: Sync with rhiza" --body-file pr-body.md
|
|
354
|
+
"""
|
|
355
|
+
summarise_cmd(target, output)
|
rhiza/commands/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ configuration templates for Python projects.
|
|
|
8
8
|
|
|
9
9
|
### init
|
|
10
10
|
|
|
11
|
-
Initialize or validate `.
|
|
11
|
+
Initialize or validate `.rhiza/template.yml` in a target directory.
|
|
12
12
|
|
|
13
13
|
Creates a default configuration file if it doesn't exist, or validates
|
|
14
14
|
an existing one. The default configuration includes common Python project
|
|
@@ -29,7 +29,7 @@ is used.
|
|
|
29
29
|
|
|
30
30
|
Validate Rhiza template configuration.
|
|
31
31
|
|
|
32
|
-
Validates the `.
|
|
32
|
+
Validates the `.rhiza/template.yml` file to ensure it is syntactically
|
|
33
33
|
correct and semantically valid. Performs comprehensive validation including
|
|
34
34
|
YAML syntax checking, required field verification, field type validation,
|
|
35
35
|
and repository format verification.
|
rhiza/commands/init.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""Command to initialize or validate .
|
|
1
|
+
"""Command to initialize or validate .rhiza/template.yml.
|
|
2
2
|
|
|
3
3
|
This module provides the init command that creates or validates the
|
|
4
|
-
.
|
|
4
|
+
.rhiza/template.yml file, which defines where templates come from
|
|
5
5
|
and what paths are governed by Rhiza.
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -52,7 +52,7 @@ def _validate_git_host(git_host: str | None) -> str | None:
|
|
|
52
52
|
git_host = git_host.lower()
|
|
53
53
|
if git_host not in ["github", "gitlab"]:
|
|
54
54
|
logger.error(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'")
|
|
55
|
-
raise ValueError(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'")
|
|
55
|
+
raise ValueError(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'") # noqa: TRY003
|
|
56
56
|
return git_host
|
|
57
57
|
|
|
58
58
|
|
|
@@ -124,12 +124,19 @@ def _get_include_paths_for_host(git_host: str) -> list[str]:
|
|
|
124
124
|
]
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def _create_template_file(
|
|
127
|
+
def _create_template_file(
|
|
128
|
+
target: Path,
|
|
129
|
+
git_host: str,
|
|
130
|
+
template_repository: str | None = None,
|
|
131
|
+
template_branch: str | None = None,
|
|
132
|
+
) -> None:
|
|
128
133
|
"""Create default template.yml file.
|
|
129
134
|
|
|
130
135
|
Args:
|
|
131
136
|
target: Target repository path.
|
|
132
137
|
git_host: Git hosting platform.
|
|
138
|
+
template_repository: Custom template repository (format: owner/repo).
|
|
139
|
+
template_branch: Custom template branch.
|
|
133
140
|
"""
|
|
134
141
|
rhiza_dir = target / ".rhiza"
|
|
135
142
|
template_file = rhiza_dir / "template.yml"
|
|
@@ -141,9 +148,20 @@ def _create_template_file(target: Path, git_host: str) -> None:
|
|
|
141
148
|
logger.debug("Using default template configuration")
|
|
142
149
|
|
|
143
150
|
include_paths = _get_include_paths_for_host(git_host)
|
|
151
|
+
|
|
152
|
+
# Use custom template repository/branch if provided, otherwise use defaults
|
|
153
|
+
repo = template_repository or "jebel-quant/rhiza"
|
|
154
|
+
branch = template_branch or "main"
|
|
155
|
+
|
|
156
|
+
# Log when custom values are used
|
|
157
|
+
if template_repository:
|
|
158
|
+
logger.info(f"Using custom template repository: {repo}")
|
|
159
|
+
if template_branch:
|
|
160
|
+
logger.info(f"Using custom template branch: {branch}")
|
|
161
|
+
|
|
144
162
|
default_template = RhizaTemplate(
|
|
145
|
-
template_repository=
|
|
146
|
-
template_branch=
|
|
163
|
+
template_repository=repo,
|
|
164
|
+
template_branch=branch,
|
|
147
165
|
include=include_paths,
|
|
148
166
|
)
|
|
149
167
|
|
|
@@ -243,10 +261,12 @@ def init(
|
|
|
243
261
|
package_name: str | None = None,
|
|
244
262
|
with_dev_dependencies: bool = False,
|
|
245
263
|
git_host: str | None = None,
|
|
264
|
+
template_repository: str | None = None,
|
|
265
|
+
template_branch: str | None = None,
|
|
246
266
|
):
|
|
247
|
-
"""Initialize or validate .
|
|
267
|
+
"""Initialize or validate .rhiza/template.yml in the target repository.
|
|
248
268
|
|
|
249
|
-
Creates a default .
|
|
269
|
+
Creates a default .rhiza/template.yml file if it doesn't exist,
|
|
250
270
|
or validates an existing one.
|
|
251
271
|
|
|
252
272
|
Args:
|
|
@@ -256,6 +276,9 @@ def init(
|
|
|
256
276
|
with_dev_dependencies: Include development dependencies in pyproject.toml.
|
|
257
277
|
git_host: Target Git hosting platform ("github" or "gitlab"). Determines which
|
|
258
278
|
CI/CD configuration files to include. If None, will prompt user interactively.
|
|
279
|
+
template_repository: Custom template repository (format: owner/repo).
|
|
280
|
+
Defaults to 'jebel-quant/rhiza'.
|
|
281
|
+
template_branch: Custom template branch. Defaults to 'main'.
|
|
259
282
|
|
|
260
283
|
Returns:
|
|
261
284
|
bool: True if validation passes, False otherwise.
|
|
@@ -275,7 +298,7 @@ def init(
|
|
|
275
298
|
git_host = _prompt_git_host()
|
|
276
299
|
|
|
277
300
|
# Create template file
|
|
278
|
-
_create_template_file(target, git_host)
|
|
301
|
+
_create_template_file(target, git_host, template_repository, template_branch)
|
|
279
302
|
|
|
280
303
|
# Bootstrap Python project structure
|
|
281
304
|
if project_name is None:
|
rhiza/commands/materialize.py
CHANGED
|
@@ -8,7 +8,7 @@ into the target Git repository, and records managed files in
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
import shutil
|
|
11
|
-
import subprocess
|
|
11
|
+
import subprocess # nosec B404
|
|
12
12
|
import sys
|
|
13
13
|
import tempfile
|
|
14
14
|
from pathlib import Path
|
|
@@ -37,7 +37,7 @@ def _handle_target_branch(
|
|
|
37
37
|
logger.info(f"Creating/checking out target branch: {target_branch}")
|
|
38
38
|
try:
|
|
39
39
|
# Check if branch already exists using git rev-parse
|
|
40
|
-
result = subprocess.run(
|
|
40
|
+
result = subprocess.run( # nosec B603
|
|
41
41
|
[git_executable, "rev-parse", "--verify", target_branch],
|
|
42
42
|
cwd=target,
|
|
43
43
|
capture_output=True,
|
|
@@ -48,7 +48,7 @@ def _handle_target_branch(
|
|
|
48
48
|
if result.returncode == 0:
|
|
49
49
|
# Branch exists, switch to it
|
|
50
50
|
logger.info(f"Branch '{target_branch}' exists, checking out...")
|
|
51
|
-
subprocess.run(
|
|
51
|
+
subprocess.run( # nosec B603
|
|
52
52
|
[git_executable, "checkout", target_branch],
|
|
53
53
|
cwd=target,
|
|
54
54
|
check=True,
|
|
@@ -57,7 +57,7 @@ def _handle_target_branch(
|
|
|
57
57
|
else:
|
|
58
58
|
# Branch doesn't exist, create it from current HEAD
|
|
59
59
|
logger.info(f"Creating new branch '{target_branch}'...")
|
|
60
|
-
subprocess.run(
|
|
60
|
+
subprocess.run( # nosec B603
|
|
61
61
|
[git_executable, "checkout", "-b", target_branch],
|
|
62
62
|
cwd=target,
|
|
63
63
|
check=True,
|
|
@@ -99,7 +99,7 @@ def _validate_and_load_template(target: Path, branch: str) -> tuple[RhizaTemplat
|
|
|
99
99
|
if not include_paths:
|
|
100
100
|
logger.error("No include paths found in template.yml")
|
|
101
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")
|
|
102
|
+
raise RuntimeError("No include paths found in template.yml") # noqa: TRY003
|
|
103
103
|
|
|
104
104
|
# Log the paths we'll be including
|
|
105
105
|
logger.info("Include paths:")
|
|
@@ -136,7 +136,7 @@ def _construct_git_url(rhiza_repo: str, rhiza_host: str) -> str:
|
|
|
136
136
|
else:
|
|
137
137
|
logger.error(f"Unsupported template-host: {rhiza_host}")
|
|
138
138
|
logger.error("template-host must be 'github' or 'gitlab'")
|
|
139
|
-
raise ValueError(f"Unsupported template-host: {rhiza_host}. Must be 'github' or 'gitlab'.")
|
|
139
|
+
raise ValueError(f"Unsupported template-host: {rhiza_host}. Must be 'github' or 'gitlab'.") # noqa: TRY003
|
|
140
140
|
return git_url
|
|
141
141
|
|
|
142
142
|
|
|
@@ -161,7 +161,7 @@ def _clone_template_repository(
|
|
|
161
161
|
# Clone the repository using sparse checkout
|
|
162
162
|
try:
|
|
163
163
|
logger.debug("Executing git clone with sparse checkout")
|
|
164
|
-
subprocess.run(
|
|
164
|
+
subprocess.run( # nosec B603
|
|
165
165
|
[
|
|
166
166
|
git_executable,
|
|
167
167
|
"clone",
|
|
@@ -190,7 +190,7 @@ def _clone_template_repository(
|
|
|
190
190
|
# Initialize sparse checkout in cone mode
|
|
191
191
|
try:
|
|
192
192
|
logger.debug("Initializing sparse checkout")
|
|
193
|
-
subprocess.run(
|
|
193
|
+
subprocess.run( # nosec B603
|
|
194
194
|
[git_executable, "sparse-checkout", "init", "--cone"],
|
|
195
195
|
cwd=tmp_dir,
|
|
196
196
|
check=True,
|
|
@@ -208,7 +208,7 @@ def _clone_template_repository(
|
|
|
208
208
|
# Set sparse checkout paths
|
|
209
209
|
try:
|
|
210
210
|
logger.debug(f"Setting sparse checkout paths: {include_paths}")
|
|
211
|
-
subprocess.run(
|
|
211
|
+
subprocess.run( # nosec B603
|
|
212
212
|
[git_executable, "sparse-checkout", "set", "--skip-checks", *include_paths],
|
|
213
213
|
cwd=tmp_dir,
|
|
214
214
|
check=True,
|
rhiza/commands/migrate.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This module implements the `migrate` command. It helps transition projects to use
|
|
4
4
|
the new `.rhiza/` folder structure for storing Rhiza state and configuration files,
|
|
5
|
-
separate from `.github
|
|
5
|
+
separate from `.github/` which contains GitHub-specific configurations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import shutil
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Command for generating PR descriptions from staged changes.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to analyze staged git changes and generate
|
|
4
|
+
structured PR descriptions for rhiza sync operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess # nosec B404
|
|
8
|
+
import sys
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_git_command(args: list[str], cwd: Path | None = None) -> str:
|
|
17
|
+
"""Run a git command and return the output.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
args: Git command arguments (without 'git' prefix)
|
|
21
|
+
cwd: Working directory for the command
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Command output as string
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run( # nosec B603 B607
|
|
28
|
+
["git", *args],
|
|
29
|
+
cwd=cwd,
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
check=True,
|
|
33
|
+
)
|
|
34
|
+
return result.stdout.strip()
|
|
35
|
+
except subprocess.CalledProcessError as e:
|
|
36
|
+
logger.error(f"Error running git {' '.join(args)}: {e.stderr}")
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_staged_changes(repo_path: Path) -> dict[str, list[str]]:
|
|
41
|
+
"""Get list of staged changes categorized by type.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
repo_path: Path to the repository
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary with keys 'added', 'modified', 'deleted' containing file lists
|
|
48
|
+
"""
|
|
49
|
+
changes = {
|
|
50
|
+
"added": [],
|
|
51
|
+
"modified": [],
|
|
52
|
+
"deleted": [],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Get staged changes
|
|
56
|
+
output = run_git_command(["diff", "--cached", "--name-status"], cwd=repo_path)
|
|
57
|
+
|
|
58
|
+
for line in output.split("\n"):
|
|
59
|
+
if not line:
|
|
60
|
+
continue
|
|
61
|
+
parts = line.split("\t", 1)
|
|
62
|
+
if len(parts) != 2:
|
|
63
|
+
continue
|
|
64
|
+
status, filepath = parts
|
|
65
|
+
|
|
66
|
+
if status == "A":
|
|
67
|
+
changes["added"].append(filepath)
|
|
68
|
+
elif status == "M":
|
|
69
|
+
changes["modified"].append(filepath)
|
|
70
|
+
elif status == "D":
|
|
71
|
+
changes["deleted"].append(filepath)
|
|
72
|
+
elif status.startswith("R"):
|
|
73
|
+
# Renamed file - treat as modified
|
|
74
|
+
changes["modified"].append(filepath)
|
|
75
|
+
|
|
76
|
+
return changes
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_config_files() -> set[str]:
|
|
80
|
+
"""Get set of known configuration files.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Set of configuration file names
|
|
84
|
+
"""
|
|
85
|
+
return {
|
|
86
|
+
"Makefile",
|
|
87
|
+
"ruff.toml",
|
|
88
|
+
"pytest.ini",
|
|
89
|
+
".editorconfig",
|
|
90
|
+
".gitignore",
|
|
91
|
+
".pre-commit-config.yaml",
|
|
92
|
+
"renovate.json",
|
|
93
|
+
".python-version",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _categorize_by_directory(first_dir: str, filepath: str) -> str | None:
|
|
98
|
+
"""Categorize file based on its first directory.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
first_dir: First directory in the path
|
|
102
|
+
filepath: Full file path
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Category name or None if no match
|
|
106
|
+
"""
|
|
107
|
+
if first_dir == ".github":
|
|
108
|
+
path_parts = Path(filepath).parts
|
|
109
|
+
if len(path_parts) > 1 and path_parts[1] == "workflows":
|
|
110
|
+
return "GitHub Actions Workflows"
|
|
111
|
+
return "GitHub Configuration"
|
|
112
|
+
|
|
113
|
+
if first_dir == ".rhiza":
|
|
114
|
+
if "script" in filepath.lower():
|
|
115
|
+
return "Rhiza Scripts"
|
|
116
|
+
if "Makefile" in filepath:
|
|
117
|
+
return "Makefiles"
|
|
118
|
+
return "Rhiza Configuration"
|
|
119
|
+
|
|
120
|
+
if first_dir == "tests":
|
|
121
|
+
return "Tests"
|
|
122
|
+
|
|
123
|
+
if first_dir == "book":
|
|
124
|
+
return "Documentation"
|
|
125
|
+
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _categorize_single_file(filepath: str) -> str:
|
|
130
|
+
"""Categorize a single file path.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
filepath: File path to categorize
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Category name
|
|
137
|
+
"""
|
|
138
|
+
path_parts = Path(filepath).parts
|
|
139
|
+
|
|
140
|
+
if not path_parts:
|
|
141
|
+
return "Other"
|
|
142
|
+
|
|
143
|
+
# Try directory-based categorization first
|
|
144
|
+
category = _categorize_by_directory(path_parts[0], filepath)
|
|
145
|
+
if category:
|
|
146
|
+
return category
|
|
147
|
+
|
|
148
|
+
# Check file-based categories
|
|
149
|
+
if filepath.endswith(".md"):
|
|
150
|
+
return "Documentation"
|
|
151
|
+
|
|
152
|
+
if filepath in _get_config_files():
|
|
153
|
+
return "Configuration Files"
|
|
154
|
+
|
|
155
|
+
return "Other"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def categorize_files(files: list[str]) -> dict[str, list[str]]:
|
|
159
|
+
"""Categorize files by type.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
files: List of file paths
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dictionary mapping category names to file lists
|
|
166
|
+
"""
|
|
167
|
+
categories = defaultdict(list)
|
|
168
|
+
|
|
169
|
+
for filepath in files:
|
|
170
|
+
category = _categorize_single_file(filepath)
|
|
171
|
+
categories[category].append(filepath)
|
|
172
|
+
|
|
173
|
+
return dict(categories)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_template_info(repo_path: Path) -> tuple[str, str]:
|
|
177
|
+
"""Get template repository and branch from template.yml.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
repo_path: Path to the repository
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Tuple of (template_repo, template_branch)
|
|
184
|
+
"""
|
|
185
|
+
template_file = repo_path / ".rhiza" / "template.yml"
|
|
186
|
+
|
|
187
|
+
if not template_file.exists():
|
|
188
|
+
return ("jebel-quant/rhiza", "main")
|
|
189
|
+
|
|
190
|
+
template_repo = "jebel-quant/rhiza"
|
|
191
|
+
template_branch = "main"
|
|
192
|
+
|
|
193
|
+
with open(template_file) as f:
|
|
194
|
+
for line in f:
|
|
195
|
+
line = line.strip()
|
|
196
|
+
if line.startswith("template-repository:"):
|
|
197
|
+
template_repo = line.split(":", 1)[1].strip().strip('"')
|
|
198
|
+
elif line.startswith("template-branch:"):
|
|
199
|
+
template_branch = line.split(":", 1)[1].strip().strip('"')
|
|
200
|
+
|
|
201
|
+
return template_repo, template_branch
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_last_sync_date(repo_path: Path) -> str | None:
|
|
205
|
+
"""Get the date of the last sync commit.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
repo_path: Path to the repository
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
ISO format date string or None if not found
|
|
212
|
+
"""
|
|
213
|
+
# Look for the most recent commit with "rhiza" in the message
|
|
214
|
+
output = run_git_command(
|
|
215
|
+
["log", "--grep=rhiza", "--grep=Sync", "--grep=template", "-i", "--format=%cI", "-1"], cwd=repo_path
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if output:
|
|
219
|
+
return output
|
|
220
|
+
|
|
221
|
+
# Fallback: try to get date from history file if it exists
|
|
222
|
+
history_file = repo_path / ".rhiza" / "history"
|
|
223
|
+
if history_file.exists():
|
|
224
|
+
# Get the file modification time
|
|
225
|
+
stat = history_file.stat()
|
|
226
|
+
return datetime.fromtimestamp(stat.st_mtime).isoformat()
|
|
227
|
+
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _format_file_list(files: list[str], status_emoji: str) -> list[str]:
|
|
232
|
+
"""Format a list of files with the given status emoji.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
files: List of file paths
|
|
236
|
+
status_emoji: Emoji to use (✅ for added, 📝 for modified, ❌ for deleted)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
List of formatted lines
|
|
240
|
+
"""
|
|
241
|
+
lines = []
|
|
242
|
+
for f in sorted(files):
|
|
243
|
+
lines.append(f"- {status_emoji} `{f}`")
|
|
244
|
+
return lines
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _add_category_section(lines: list[str], title: str, count: int, files: list[str], emoji: str) -> None:
|
|
248
|
+
"""Add a collapsible section for a category and change type.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
lines: List to append lines to
|
|
252
|
+
title: Section title (e.g., "Added", "Modified")
|
|
253
|
+
count: Number of files
|
|
254
|
+
files: List of file paths
|
|
255
|
+
emoji: Status emoji
|
|
256
|
+
"""
|
|
257
|
+
if not files:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
lines.append("<details>")
|
|
261
|
+
lines.append(f"<summary>{title} ({count})</summary>")
|
|
262
|
+
lines.append("")
|
|
263
|
+
lines.extend(_format_file_list(files, emoji))
|
|
264
|
+
lines.append("")
|
|
265
|
+
lines.append("</details>")
|
|
266
|
+
lines.append("")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _build_header(template_repo: str) -> list[str]:
|
|
270
|
+
"""Build the PR description header.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
template_repo: Template repository name
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of header lines
|
|
277
|
+
"""
|
|
278
|
+
return [
|
|
279
|
+
"## 🔄 Template Synchronization",
|
|
280
|
+
"",
|
|
281
|
+
f"This PR synchronizes the repository with the [{template_repo}](https://github.com/{template_repo}) template.",
|
|
282
|
+
"",
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _build_summary(changes: dict[str, list[str]]) -> list[str]:
|
|
287
|
+
"""Build the change summary section.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
changes: Dictionary of changes by type
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
List of summary lines
|
|
294
|
+
"""
|
|
295
|
+
return [
|
|
296
|
+
"### 📊 Change Summary",
|
|
297
|
+
"",
|
|
298
|
+
f"- **{len(changes['added'])}** files added",
|
|
299
|
+
f"- **{len(changes['modified'])}** files modified",
|
|
300
|
+
f"- **{len(changes['deleted'])}** files deleted",
|
|
301
|
+
"",
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _build_footer(template_repo: str, template_branch: str, last_sync: str | None) -> list[str]:
|
|
306
|
+
"""Build the PR description footer with metadata.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
template_repo: Template repository name
|
|
310
|
+
template_branch: Template branch name
|
|
311
|
+
last_sync: Last sync date string or None
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
List of footer lines
|
|
315
|
+
"""
|
|
316
|
+
lines = [
|
|
317
|
+
"---",
|
|
318
|
+
"",
|
|
319
|
+
"**🤖 Generated by [rhiza](https://github.com/jebel-quant/rhiza-cli)**",
|
|
320
|
+
"",
|
|
321
|
+
f"- Template: `{template_repo}@{template_branch}`",
|
|
322
|
+
]
|
|
323
|
+
if last_sync:
|
|
324
|
+
lines.append(f"- Last sync: {last_sync}")
|
|
325
|
+
lines.append(f"- Sync date: {datetime.now().astimezone().isoformat()}")
|
|
326
|
+
return lines
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def generate_pr_description(repo_path: Path) -> str:
|
|
330
|
+
"""Generate PR description based on staged changes.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
repo_path: Path to the repository
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Formatted PR description
|
|
337
|
+
"""
|
|
338
|
+
changes = get_staged_changes(repo_path)
|
|
339
|
+
template_repo, template_branch = get_template_info(repo_path)
|
|
340
|
+
last_sync = get_last_sync_date(repo_path)
|
|
341
|
+
|
|
342
|
+
# Build header
|
|
343
|
+
lines = _build_header(template_repo)
|
|
344
|
+
|
|
345
|
+
# Check if there are any changes
|
|
346
|
+
total_changes = sum(len(files) for files in changes.values())
|
|
347
|
+
if total_changes == 0:
|
|
348
|
+
lines.append("No changes detected.")
|
|
349
|
+
return "\n".join(lines)
|
|
350
|
+
|
|
351
|
+
# Add summary
|
|
352
|
+
lines.extend(_build_summary(changes))
|
|
353
|
+
|
|
354
|
+
# Add detailed changes by category
|
|
355
|
+
all_changed_files = changes["added"] + changes["modified"] + changes["deleted"]
|
|
356
|
+
categories = categorize_files(all_changed_files)
|
|
357
|
+
|
|
358
|
+
if categories:
|
|
359
|
+
lines.append("### 📁 Changes by Category")
|
|
360
|
+
lines.append("")
|
|
361
|
+
|
|
362
|
+
for category, files in sorted(categories.items()):
|
|
363
|
+
lines.append(f"#### {category}")
|
|
364
|
+
lines.append("")
|
|
365
|
+
|
|
366
|
+
# Group files by change type
|
|
367
|
+
category_added = [f for f in files if f in changes["added"]]
|
|
368
|
+
category_modified = [f for f in files if f in changes["modified"]]
|
|
369
|
+
category_deleted = [f for f in files if f in changes["deleted"]]
|
|
370
|
+
|
|
371
|
+
_add_category_section(lines, "Added", len(category_added), category_added, "✅")
|
|
372
|
+
_add_category_section(lines, "Modified", len(category_modified), category_modified, "📝")
|
|
373
|
+
_add_category_section(lines, "Deleted", len(category_deleted), category_deleted, "❌")
|
|
374
|
+
|
|
375
|
+
# Add footer
|
|
376
|
+
lines.extend(_build_footer(template_repo, template_branch, last_sync))
|
|
377
|
+
|
|
378
|
+
return "\n".join(lines)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def summarise(target: Path, output: Path | None = None) -> None:
|
|
382
|
+
"""Generate a summary of staged changes for rhiza sync operations.
|
|
383
|
+
|
|
384
|
+
This command analyzes staged git changes and generates a structured
|
|
385
|
+
PR description with:
|
|
386
|
+
- Summary statistics (files added/modified/deleted)
|
|
387
|
+
- Changes categorized by type (workflows, configs, docs, tests, etc.)
|
|
388
|
+
- Template repository information
|
|
389
|
+
- Last sync date
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
target: Path to the target repository.
|
|
393
|
+
output: Optional output file path. If not provided, prints to stdout.
|
|
394
|
+
"""
|
|
395
|
+
target = target.resolve()
|
|
396
|
+
logger.info(f"Target repository: {target}")
|
|
397
|
+
|
|
398
|
+
# Check if target is a git repository
|
|
399
|
+
if not (target / ".git").is_dir():
|
|
400
|
+
logger.error(f"Target directory is not a git repository: {target}")
|
|
401
|
+
logger.error("Initialize a git repository with 'git init' first")
|
|
402
|
+
sys.exit(1)
|
|
403
|
+
|
|
404
|
+
# Generate the PR description
|
|
405
|
+
description = generate_pr_description(target)
|
|
406
|
+
|
|
407
|
+
# Output the description
|
|
408
|
+
if output:
|
|
409
|
+
output_path = output.resolve()
|
|
410
|
+
output_path.write_text(description)
|
|
411
|
+
logger.success(f"PR description written to {output_path}")
|
|
412
|
+
else:
|
|
413
|
+
# Print to stdout
|
|
414
|
+
print(description)
|
|
415
|
+
|
|
416
|
+
logger.success("Summary generated successfully")
|
rhiza/commands/uninstall.py
CHANGED
|
@@ -143,10 +143,11 @@ def _remove_history_file(history_file: Path, target: Path) -> tuple[int, int]:
|
|
|
143
143
|
try:
|
|
144
144
|
history_file.unlink()
|
|
145
145
|
logger.success(f"[DEL] {history_file.relative_to(target)}")
|
|
146
|
-
return 1, 0
|
|
147
146
|
except Exception as e:
|
|
148
147
|
logger.error(f"Failed to delete {history_file.relative_to(target)}: {e}")
|
|
149
148
|
return 0, 1
|
|
149
|
+
else:
|
|
150
|
+
return 1, 0
|
|
150
151
|
|
|
151
152
|
|
|
152
153
|
def _print_summary(removed_count: int, skipped_count: int, empty_dirs_removed: int, error_count: int) -> None:
|
rhiza/commands/validate.py
CHANGED
|
@@ -91,7 +91,7 @@ def _check_template_file_exists(target: Path) -> tuple[bool, Path]:
|
|
|
91
91
|
logger.info(" • If you have an existing configuration, run: rhiza migrate")
|
|
92
92
|
logger.info("")
|
|
93
93
|
logger.info("The 'rhiza migrate' command will move your configuration from")
|
|
94
|
-
logger.info("
|
|
94
|
+
logger.info(" the old location → .rhiza/template.yml")
|
|
95
95
|
return False, template_file
|
|
96
96
|
|
|
97
97
|
logger.success(f"Template file exists: {template_file.relative_to(target)}")
|
rhiza/commands/welcome.py
CHANGED
|
@@ -44,7 +44,7 @@ Python projects using reusable templates stored in a central repository.
|
|
|
44
44
|
1. Initialize a project:
|
|
45
45
|
$ rhiza init
|
|
46
46
|
|
|
47
|
-
2. Customize .
|
|
47
|
+
2. Customize .rhiza/template.yml to match your needs
|
|
48
48
|
|
|
49
49
|
3. Materialize templates into your project:
|
|
50
50
|
$ rhiza materialize
|
rhiza/models.py
CHANGED
|
@@ -36,7 +36,7 @@ def _normalize_to_list(value: str | list[str] | None) -> list[str]:
|
|
|
36
36
|
|
|
37
37
|
@dataclass
|
|
38
38
|
class RhizaTemplate:
|
|
39
|
-
"""Represents the structure of .
|
|
39
|
+
"""Represents the structure of .rhiza/template.yml.
|
|
40
40
|
|
|
41
41
|
Attributes:
|
|
42
42
|
template_repository: The GitHub or GitLab repository containing templates (e.g., "jebel-quant/rhiza").
|
|
@@ -74,7 +74,7 @@ class RhizaTemplate:
|
|
|
74
74
|
config = yaml.safe_load(f)
|
|
75
75
|
|
|
76
76
|
if not config:
|
|
77
|
-
raise ValueError("Template file is empty")
|
|
77
|
+
raise ValueError("Template file is empty") # noqa: TRY003
|
|
78
78
|
|
|
79
79
|
return cls(
|
|
80
80
|
template_repository=config.get("template-repository"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rhiza
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -146,11 +146,11 @@ rhiza --help
|
|
|
146
146
|
rhiza init
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
This creates a `.
|
|
149
|
+
This creates a `.rhiza/template.yml` file with default configuration.
|
|
150
150
|
|
|
151
151
|
2. **Customize the template configuration:**
|
|
152
152
|
|
|
153
|
-
Edit `.
|
|
153
|
+
Edit `.rhiza/template.yml` to specify which files/directories to include from your template repository.
|
|
154
154
|
|
|
155
155
|
3. **Materialize templates into your project:**
|
|
156
156
|
|
|
@@ -166,27 +166,38 @@ rhiza --help
|
|
|
166
166
|
rhiza validate
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
This checks that your `.
|
|
169
|
+
This checks that your `.rhiza/template.yml` is correctly formatted and valid.
|
|
170
170
|
|
|
171
171
|
## Commands
|
|
172
172
|
|
|
173
173
|
### `rhiza init`
|
|
174
174
|
|
|
175
|
-
Initialize or validate `.
|
|
175
|
+
Initialize or validate `.rhiza/template.yml` in a target directory.
|
|
176
176
|
|
|
177
177
|
**Usage:**
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
|
-
rhiza init [TARGET]
|
|
180
|
+
rhiza init [OPTIONS] [TARGET]
|
|
181
181
|
```
|
|
182
182
|
|
|
183
183
|
**Arguments:**
|
|
184
184
|
|
|
185
185
|
- `TARGET` - Target directory (defaults to current directory)
|
|
186
186
|
|
|
187
|
+
**Options:**
|
|
188
|
+
|
|
189
|
+
- `--project-name <name>` - Custom project name (defaults to directory name)
|
|
190
|
+
- `--package-name <name>` - Custom package name (defaults to normalized project name)
|
|
191
|
+
- `--with-dev-dependencies` - Include development dependencies in pyproject.toml
|
|
192
|
+
- `--git-host <host>` - Target Git hosting platform (github or gitlab). Determines which CI/CD files to include. If not provided, will prompt interactively.
|
|
193
|
+
- `--template-repository <owner/repo>` - Custom template repository (format: owner/repo). Defaults to 'jebel-quant/rhiza'.
|
|
194
|
+
- `--template-branch <branch>` - Custom template branch. Defaults to 'main'.
|
|
195
|
+
|
|
187
196
|
**Description:**
|
|
188
197
|
|
|
189
|
-
Creates a default `.
|
|
198
|
+
Creates a default `.rhiza/template.yml` file if it doesn't exist, or validates an existing one. The default configuration includes common Python project files like `.github`, `.editorconfig`, `.gitignore`, `.pre-commit-config.yaml`, `Makefile`, and `pytest.ini`.
|
|
199
|
+
|
|
200
|
+
You can customize the template source by specifying your own template repository and branch using the `--template-repository` and `--template-branch` options.
|
|
190
201
|
|
|
191
202
|
**Examples:**
|
|
192
203
|
|
|
@@ -197,6 +208,15 @@ rhiza init
|
|
|
197
208
|
# Initialize in a specific directory
|
|
198
209
|
rhiza init /path/to/project
|
|
199
210
|
|
|
211
|
+
# Initialize with GitLab CI configuration
|
|
212
|
+
rhiza init --git-host gitlab
|
|
213
|
+
|
|
214
|
+
# Use a custom template repository
|
|
215
|
+
rhiza init --template-repository myorg/my-templates
|
|
216
|
+
|
|
217
|
+
# Use a custom template repository and branch
|
|
218
|
+
rhiza init --template-repository myorg/my-templates --template-branch develop
|
|
219
|
+
|
|
200
220
|
# Initialize in parent directory
|
|
201
221
|
rhiza init ..
|
|
202
222
|
```
|
|
@@ -206,18 +226,18 @@ rhiza init ..
|
|
|
206
226
|
When creating a new template file:
|
|
207
227
|
```
|
|
208
228
|
[INFO] Initializing Rhiza configuration in: /path/to/project
|
|
209
|
-
[INFO] Creating default .
|
|
210
|
-
✓ Created .
|
|
229
|
+
[INFO] Creating default .rhiza/template.yml
|
|
230
|
+
✓ Created .rhiza/template.yml
|
|
211
231
|
|
|
212
232
|
Next steps:
|
|
213
|
-
1. Review and customize .
|
|
233
|
+
1. Review and customize .rhiza/template.yml to match your project needs
|
|
214
234
|
2. Run 'rhiza materialize' to inject templates into your repository
|
|
215
235
|
```
|
|
216
236
|
|
|
217
237
|
When validating an existing file:
|
|
218
238
|
```
|
|
219
239
|
[INFO] Validating template configuration in: /path/to/project
|
|
220
|
-
✓ Found template file: /path/to/project/.
|
|
240
|
+
✓ Found template file: /path/to/project/.rhiza/template.yml
|
|
221
241
|
✓ YAML syntax is valid
|
|
222
242
|
✓ Field 'template-repository' is present and valid
|
|
223
243
|
✓ Field 'include' is present and valid
|
|
@@ -252,7 +272,7 @@ rhiza materialize [OPTIONS] [TARGET]
|
|
|
252
272
|
|
|
253
273
|
Materializes template files from the configured template repository into your target project. This command:
|
|
254
274
|
|
|
255
|
-
1. Reads the `.
|
|
275
|
+
1. Reads the `.rhiza/template.yml` configuration
|
|
256
276
|
2. Performs a sparse clone of the template repository
|
|
257
277
|
3. Copies specified files/directories to your project
|
|
258
278
|
4. Respects exclusion patterns defined in the configuration
|
|
@@ -366,7 +386,7 @@ rhiza migrate /path/to/project
|
|
|
366
386
|
[INFO] This will create the .rhiza folder and migrate configuration files
|
|
367
387
|
[INFO] Creating .rhiza directory at: .rhiza
|
|
368
388
|
✓ Created .rhiza
|
|
369
|
-
[INFO] Found template.yml at: .
|
|
389
|
+
[INFO] Found template.yml at: .rhiza/template.yml
|
|
370
390
|
[INFO] Moving to new location: .rhiza/template.yml
|
|
371
391
|
✓ Moved template.yml to .rhiza/template.yml
|
|
372
392
|
✓ Migration completed successfully
|
|
@@ -414,7 +434,7 @@ rhiza validate [TARGET]
|
|
|
414
434
|
|
|
415
435
|
**Description:**
|
|
416
436
|
|
|
417
|
-
Validates the `.
|
|
437
|
+
Validates the `.rhiza/template.yml` file to ensure it is syntactically correct and semantically valid. This performs authoritative validation including:
|
|
418
438
|
|
|
419
439
|
- Checking if the file exists
|
|
420
440
|
- Validating YAML syntax
|
|
@@ -445,7 +465,7 @@ rhiza validate ..
|
|
|
445
465
|
|
|
446
466
|
```
|
|
447
467
|
[INFO] Validating template configuration in: /path/to/project
|
|
448
|
-
✓ Found template file: /path/to/project/.
|
|
468
|
+
✓ Found template file: /path/to/project/.rhiza/template.yml
|
|
449
469
|
✓ YAML syntax is valid
|
|
450
470
|
✓ Field 'template-repository' is present and valid
|
|
451
471
|
✓ Field 'include' is present and valid
|
|
@@ -469,7 +489,7 @@ rhiza validate ..
|
|
|
469
489
|
or
|
|
470
490
|
|
|
471
491
|
```
|
|
472
|
-
[ERROR] Template file not found: /path/to/project/.
|
|
492
|
+
[ERROR] Template file not found: /path/to/project/.rhiza/template.yml
|
|
473
493
|
[INFO] Run 'rhiza materialize' or 'rhiza init' to create a default template.yml
|
|
474
494
|
```
|
|
475
495
|
|
|
@@ -477,7 +497,7 @@ or
|
|
|
477
497
|
|
|
478
498
|
## Configuration
|
|
479
499
|
|
|
480
|
-
Rhiza uses a `.
|
|
500
|
+
Rhiza uses a `.rhiza/template.yml` file to define template sources and what to include in your project.
|
|
481
501
|
|
|
482
502
|
### Configuration File Format
|
|
483
503
|
|
|
@@ -618,7 +638,7 @@ git init
|
|
|
618
638
|
rhiza init
|
|
619
639
|
|
|
620
640
|
# Review the generated template.yml
|
|
621
|
-
cat .
|
|
641
|
+
cat .rhiza/template.yml
|
|
622
642
|
|
|
623
643
|
# Materialize templates
|
|
624
644
|
rhiza materialize
|
|
@@ -653,7 +673,7 @@ git commit -m "chore: update rhiza templates"
|
|
|
653
673
|
|
|
654
674
|
### Example 3: Using a custom template repository
|
|
655
675
|
|
|
656
|
-
Edit `.
|
|
676
|
+
Edit `.rhiza/template.yml`:
|
|
657
677
|
|
|
658
678
|
```yaml
|
|
659
679
|
template-repository: myorg/my-templates
|
|
@@ -675,7 +695,7 @@ rhiza materialize --force
|
|
|
675
695
|
|
|
676
696
|
### Example 4: Using a GitLab template repository
|
|
677
697
|
|
|
678
|
-
Edit `.
|
|
698
|
+
Edit `.rhiza/template.yml`:
|
|
679
699
|
|
|
680
700
|
```yaml
|
|
681
701
|
template-repository: mygroup/python-templates
|
|
@@ -815,7 +835,7 @@ Releasing and Versioning
|
|
|
815
835
|
post-release perform post-release tasks
|
|
816
836
|
|
|
817
837
|
Meta
|
|
818
|
-
sync sync with template repository as defined in .
|
|
838
|
+
sync sync with template repository as defined in .rhiza/template.yml
|
|
819
839
|
help Display this help message
|
|
820
840
|
customisations list available customisation scripts
|
|
821
841
|
update-readme update README.md with current Makefile help output
|
|
@@ -887,7 +907,7 @@ export PATH="$HOME/.local/bin:$PATH"
|
|
|
887
907
|
### Template validation fails
|
|
888
908
|
|
|
889
909
|
Check that:
|
|
890
|
-
1. Your `.
|
|
910
|
+
1. Your `.rhiza/template.yml` file exists
|
|
891
911
|
2. The YAML syntax is valid
|
|
892
912
|
3. Required fields (`template-repository` and `include`) are present
|
|
893
913
|
4. The repository format is `owner/repo`
|
|
@@ -919,11 +939,11 @@ A: Yes, as long as you have Git credentials configured that allow access to the
|
|
|
919
939
|
|
|
920
940
|
**Q: Does Rhiza support template repositories hosted outside GitHub?**
|
|
921
941
|
|
|
922
|
-
A: Yes! Rhiza supports both GitHub and GitLab repositories. Use the `template-host` field in your `.
|
|
942
|
+
A: Yes! Rhiza supports both GitHub and GitLab repositories. Use the `template-host` field in your `.rhiza/template.yml` to specify "github" (default) or "gitlab".
|
|
923
943
|
|
|
924
944
|
**Q: How do I use a GitLab repository as a template source?**
|
|
925
945
|
|
|
926
|
-
A: Add `template-host: gitlab` to your `.
|
|
946
|
+
A: Add `template-host: gitlab` to your `.rhiza/template.yml` file. For example:
|
|
927
947
|
```yaml
|
|
928
948
|
template-repository: mygroup/myproject
|
|
929
949
|
template-host: gitlab
|
|
@@ -938,7 +958,7 @@ A: Not directly. However, you can run `rhiza materialize` multiple times with di
|
|
|
938
958
|
|
|
939
959
|
**Q: What's the difference between `rhiza init` and `rhiza materialize`?**
|
|
940
960
|
|
|
941
|
-
A: `init` creates or validates the `.
|
|
961
|
+
A: `init` creates or validates the `.rhiza/template.yml` configuration file. `materialize` reads that configuration and actually copies the template files into your project.
|
|
942
962
|
|
|
943
963
|
**Q: How do I update my project's templates?**
|
|
944
964
|
|
|
@@ -954,7 +974,7 @@ A: The update method depends on how you installed rhiza:
|
|
|
954
974
|
|
|
955
975
|
**Q: Can I customize which files are included?**
|
|
956
976
|
|
|
957
|
-
A: Yes, edit the `include` and `exclude` lists in `.
|
|
977
|
+
A: Yes, edit the `include` and `exclude` lists in `.rhiza/template.yml` to control exactly which files are copied.
|
|
958
978
|
|
|
959
979
|
## Acknowledgments
|
|
960
980
|
|
|
@@ -0,0 +1,21 @@
|
|
|
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,,
|
rhiza-0.8.8.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
rhiza/__init__.py,sha256=iW3niLBjwRKxcMhIV_1eb78putjUTo2tbZsadofluJk,1939
|
|
2
|
-
rhiza/__main__.py,sha256=Q02upTGaJceknkDABdCwq5_vdMdGY8Cg3ej6WZIHs_s,829
|
|
3
|
-
rhiza/cli.py,sha256=xIsyfKjSFjVLjCS7o2om5o_YLZx9lIhsI0MTMI5Zs2k,8594
|
|
4
|
-
rhiza/models.py,sha256=o_iD8P7d7RAHH6J91tSPmD6lY-2HUsCaIl8auWqm2NM,4216
|
|
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=Z5CeMh7ylX27H6dvwqRbEKzYo5pwQq-5TyTxABUSaQg,1848
|
|
10
|
-
rhiza/commands/init.py,sha256=73MLPLp-M8U4fP8J5RXghS6FsZjx2PpeeBbKRZvLQ7U,8882
|
|
11
|
-
rhiza/commands/materialize.py,sha256=U6MouBNrg_GjYIPD0vBb3nacFBKGP4l8RD-HH0u-mYk,18538
|
|
12
|
-
rhiza/commands/migrate.py,sha256=pT8izKuX2eXCAkmNfcy4AU5HTB1DoOZoBXcZo2AOpXs,7520
|
|
13
|
-
rhiza/commands/uninstall.py,sha256=MJbQtmdTgbzMvQz0gGLW3aw6S1dSV8nLv0SqWSDpyPk,7469
|
|
14
|
-
rhiza/commands/validate.py,sha256=pg7SpgavvrjDyuZIphJ_GOMnXkwdVs9WtL2caa1XjcM,10811
|
|
15
|
-
rhiza/commands/welcome.py,sha256=w3BziR042o6oYincd3EqDsFzF6qqInU7iYhWjF3yJqY,2382
|
|
16
|
-
rhiza-0.8.8.dist-info/METADATA,sha256=PGkXXTlS2XGjxAm5ODJmDvrRerXnEmG1ZoUcJvYhiyo,25395
|
|
17
|
-
rhiza-0.8.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
-
rhiza-0.8.8.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
|
|
19
|
-
rhiza-0.8.8.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
|
|
20
|
-
rhiza-0.8.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|