rhiza 0.8.2__py3-none-any.whl → 0.8.4__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 +22 -0
- rhiza/cli.py +13 -7
- rhiza/commands/init.py +218 -108
- rhiza/commands/materialize.py +328 -267
- rhiza/commands/migrate.py +99 -46
- rhiza/commands/uninstall.py +136 -57
- rhiza/commands/validate.py +182 -71
- rhiza/subprocess_utils.py +26 -0
- {rhiza-0.8.2.dist-info → rhiza-0.8.4.dist-info}/METADATA +11 -3
- rhiza-0.8.4.dist-info/RECORD +20 -0
- rhiza-0.8.2.dist-info/RECORD +0 -19
- {rhiza-0.8.2.dist-info → rhiza-0.8.4.dist-info}/WHEEL +0 -0
- {rhiza-0.8.2.dist-info → rhiza-0.8.4.dist-info}/entry_points.txt +0 -0
- {rhiza-0.8.2.dist-info → rhiza-0.8.4.dist-info}/licenses/LICENSE +0 -0
rhiza/__main__.py
CHANGED
|
@@ -4,7 +4,29 @@ This module allows running the Rhiza CLI with `python -m rhiza` by
|
|
|
4
4
|
delegating execution to the Typer application defined in `rhiza.cli`.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from importlib.metadata import entry_points
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
7
11
|
from rhiza.cli import app
|
|
8
12
|
|
|
13
|
+
|
|
14
|
+
def load_plugins(app: typer.Typer):
|
|
15
|
+
"""Load plugins from entry points."""
|
|
16
|
+
# 'rhiza.plugins' matches the group we defined in rhiza-tools
|
|
17
|
+
plugin_entries = entry_points(group="rhiza.plugins")
|
|
18
|
+
|
|
19
|
+
for entry in plugin_entries:
|
|
20
|
+
try:
|
|
21
|
+
plugin_app = entry.load()
|
|
22
|
+
# This adds the plugin as a subcommand, e.g., 'rhiza tools bump'
|
|
23
|
+
app.add_typer(plugin_app, name=entry.name)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(f"Failed to load plugin {entry.name}: {e}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
load_plugins(app)
|
|
29
|
+
|
|
30
|
+
|
|
9
31
|
if __name__ == "__main__":
|
|
10
32
|
app()
|
rhiza/cli.py
CHANGED
|
@@ -87,22 +87,27 @@ def init(
|
|
|
87
87
|
"--with-dev-dependencies",
|
|
88
88
|
help="Include development dependencies in pyproject.toml",
|
|
89
89
|
),
|
|
90
|
+
git_host: str = typer.Option(
|
|
91
|
+
None,
|
|
92
|
+
"--git-host",
|
|
93
|
+
help="Target Git hosting platform (github or gitlab). Determines which CI/CD files to include. "
|
|
94
|
+
"If not provided, will prompt interactively.",
|
|
95
|
+
),
|
|
90
96
|
):
|
|
91
97
|
r"""Initialize or validate .github/rhiza/template.yml.
|
|
92
98
|
|
|
93
99
|
Creates a default `.github/rhiza/template.yml` configuration file if one
|
|
94
100
|
doesn't exist, or validates the existing configuration.
|
|
95
101
|
|
|
96
|
-
The default template includes common Python project files
|
|
97
|
-
-
|
|
98
|
-
- .
|
|
99
|
-
- .
|
|
100
|
-
- .pre-commit-config.yaml
|
|
101
|
-
- Makefile
|
|
102
|
-
- pytest.ini
|
|
102
|
+
The default template includes common Python project files.
|
|
103
|
+
The --git-host option determines which CI/CD configuration to include:
|
|
104
|
+
- github: includes .github folder (GitHub Actions workflows)
|
|
105
|
+
- gitlab: includes .gitlab-ci.yml (GitLab CI configuration)
|
|
103
106
|
|
|
104
107
|
Examples:
|
|
105
108
|
rhiza init
|
|
109
|
+
rhiza init --git-host github
|
|
110
|
+
rhiza init --git-host gitlab
|
|
106
111
|
rhiza init /path/to/project
|
|
107
112
|
rhiza init ..
|
|
108
113
|
"""
|
|
@@ -111,6 +116,7 @@ def init(
|
|
|
111
116
|
project_name=project_name,
|
|
112
117
|
package_name=package_name,
|
|
113
118
|
with_dev_dependencies=with_dev_dependencies,
|
|
119
|
+
git_host=git_host,
|
|
114
120
|
)
|
|
115
121
|
|
|
116
122
|
|
rhiza/commands/init.py
CHANGED
|
@@ -8,8 +8,10 @@ and what paths are governed by Rhiza.
|
|
|
8
8
|
import importlib.resources
|
|
9
9
|
import keyword
|
|
10
10
|
import re
|
|
11
|
+
import sys
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
14
|
+
import typer
|
|
13
15
|
from jinja2 import Template
|
|
14
16
|
from loguru import logger
|
|
15
17
|
|
|
@@ -26,25 +28,221 @@ def _normalize_package_name(name: str) -> str:
|
|
|
26
28
|
Returns:
|
|
27
29
|
A valid Python identifier safe for use as a package name.
|
|
28
30
|
"""
|
|
29
|
-
# Replace any character that is not a letter, number, or underscore with an underscore
|
|
30
31
|
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
31
|
-
|
|
32
|
-
# Ensure it doesn't start with a number
|
|
33
32
|
if name[0].isdigit():
|
|
34
33
|
name = f"_{name}"
|
|
35
|
-
|
|
36
|
-
# Ensure it's not a Python keyword
|
|
37
34
|
if keyword.iskeyword(name):
|
|
38
35
|
name = f"{name}_"
|
|
39
|
-
|
|
40
36
|
return name
|
|
41
37
|
|
|
42
38
|
|
|
39
|
+
def _validate_git_host(git_host: str | None) -> str | None:
|
|
40
|
+
"""Validate git_host parameter.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
git_host: Git hosting platform.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Validated git_host or None.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If git_host is invalid.
|
|
50
|
+
"""
|
|
51
|
+
if git_host is not None:
|
|
52
|
+
git_host = git_host.lower()
|
|
53
|
+
if git_host not in ["github", "gitlab"]:
|
|
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'")
|
|
56
|
+
return git_host
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _prompt_git_host() -> str:
|
|
60
|
+
"""Prompt user for git hosting platform.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Git hosting platform choice.
|
|
64
|
+
"""
|
|
65
|
+
if sys.stdin.isatty():
|
|
66
|
+
logger.info("Where will your project be hosted?")
|
|
67
|
+
git_host = typer.prompt(
|
|
68
|
+
"Target Git hosting platform (github/gitlab)",
|
|
69
|
+
type=str,
|
|
70
|
+
default="github",
|
|
71
|
+
).lower()
|
|
72
|
+
|
|
73
|
+
while git_host not in ["github", "gitlab"]:
|
|
74
|
+
logger.warning(f"Invalid choice: {git_host}. Please choose 'github' or 'gitlab'")
|
|
75
|
+
git_host = typer.prompt(
|
|
76
|
+
"Target Git hosting platform (github/gitlab)",
|
|
77
|
+
type=str,
|
|
78
|
+
default="github",
|
|
79
|
+
).lower()
|
|
80
|
+
else:
|
|
81
|
+
git_host = "github"
|
|
82
|
+
logger.debug("Non-interactive mode detected, defaulting to github")
|
|
83
|
+
|
|
84
|
+
return git_host
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_include_paths_for_host(git_host: str) -> list[str]:
|
|
88
|
+
"""Get include paths based on git hosting platform.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
git_host: Git hosting platform.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of include paths.
|
|
95
|
+
"""
|
|
96
|
+
if git_host == "gitlab":
|
|
97
|
+
return [
|
|
98
|
+
".rhiza",
|
|
99
|
+
".gitlab",
|
|
100
|
+
".gitlab-ci.yml",
|
|
101
|
+
".editorconfig",
|
|
102
|
+
".gitignore",
|
|
103
|
+
".pre-commit-config.yaml",
|
|
104
|
+
"ruff.toml",
|
|
105
|
+
"Makefile",
|
|
106
|
+
"pytest.ini",
|
|
107
|
+
"book",
|
|
108
|
+
"presentation",
|
|
109
|
+
"tests",
|
|
110
|
+
]
|
|
111
|
+
else:
|
|
112
|
+
return [
|
|
113
|
+
".rhiza",
|
|
114
|
+
".github",
|
|
115
|
+
".editorconfig",
|
|
116
|
+
".gitignore",
|
|
117
|
+
".pre-commit-config.yaml",
|
|
118
|
+
"ruff.toml",
|
|
119
|
+
"Makefile",
|
|
120
|
+
"pytest.ini",
|
|
121
|
+
"book",
|
|
122
|
+
"presentation",
|
|
123
|
+
"tests",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _create_template_file(target: Path, git_host: str) -> None:
|
|
128
|
+
"""Create default template.yml file.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
target: Target repository path.
|
|
132
|
+
git_host: Git hosting platform.
|
|
133
|
+
"""
|
|
134
|
+
rhiza_dir = target / ".rhiza"
|
|
135
|
+
template_file = rhiza_dir / "template.yml"
|
|
136
|
+
|
|
137
|
+
if template_file.exists():
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
logger.info("Creating default .rhiza/template.yml")
|
|
141
|
+
logger.debug("Using default template configuration")
|
|
142
|
+
|
|
143
|
+
include_paths = _get_include_paths_for_host(git_host)
|
|
144
|
+
default_template = RhizaTemplate(
|
|
145
|
+
template_repository="jebel-quant/rhiza",
|
|
146
|
+
template_branch="main",
|
|
147
|
+
include=include_paths,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
logger.debug(f"Writing default template to: {template_file}")
|
|
151
|
+
default_template.to_yaml(template_file)
|
|
152
|
+
|
|
153
|
+
logger.success("✓ Created .rhiza/template.yml")
|
|
154
|
+
logger.info("""
|
|
155
|
+
Next steps:
|
|
156
|
+
1. Review and customize .rhiza/template.yml to match your project needs
|
|
157
|
+
2. Run 'rhiza materialize' to inject templates into your repository
|
|
158
|
+
""")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _create_python_package(target: Path, project_name: str, package_name: str) -> None:
|
|
162
|
+
"""Create basic Python package structure.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
target: Target repository path.
|
|
166
|
+
project_name: Project name.
|
|
167
|
+
package_name: Package name.
|
|
168
|
+
"""
|
|
169
|
+
src_folder = target / "src" / package_name
|
|
170
|
+
if (target / "src").exists():
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
logger.info(f"Creating Python package structure: {src_folder}")
|
|
174
|
+
src_folder.mkdir(parents=True)
|
|
175
|
+
|
|
176
|
+
# Create __init__.py
|
|
177
|
+
init_file = src_folder / "__init__.py"
|
|
178
|
+
logger.debug(f"Creating {init_file}")
|
|
179
|
+
init_file.touch()
|
|
180
|
+
|
|
181
|
+
template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/__init__.py.jinja2").read_text()
|
|
182
|
+
template = Template(template_content)
|
|
183
|
+
code = template.render(project_name=project_name)
|
|
184
|
+
init_file.write_text(code)
|
|
185
|
+
|
|
186
|
+
# Create main.py
|
|
187
|
+
main_file = src_folder / "main.py"
|
|
188
|
+
logger.debug(f"Creating {main_file} with example code")
|
|
189
|
+
main_file.touch()
|
|
190
|
+
|
|
191
|
+
template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/main.py.jinja2").read_text()
|
|
192
|
+
template = Template(template_content)
|
|
193
|
+
code = template.render(project_name=project_name)
|
|
194
|
+
main_file.write_text(code)
|
|
195
|
+
logger.success(f"Created Python package structure in {src_folder}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _create_pyproject_toml(target: Path, project_name: str, package_name: str, with_dev_dependencies: bool) -> None:
|
|
199
|
+
"""Create pyproject.toml file.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
target: Target repository path.
|
|
203
|
+
project_name: Project name.
|
|
204
|
+
package_name: Package name.
|
|
205
|
+
with_dev_dependencies: Whether to include dev dependencies.
|
|
206
|
+
"""
|
|
207
|
+
pyproject_file = target / "pyproject.toml"
|
|
208
|
+
if pyproject_file.exists():
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
logger.info("Creating pyproject.toml with basic project metadata")
|
|
212
|
+
pyproject_file.touch()
|
|
213
|
+
|
|
214
|
+
template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/pyproject.toml.jinja2").read_text()
|
|
215
|
+
template = Template(template_content)
|
|
216
|
+
code = template.render(
|
|
217
|
+
project_name=project_name,
|
|
218
|
+
package_name=package_name,
|
|
219
|
+
with_dev_dependencies=with_dev_dependencies,
|
|
220
|
+
)
|
|
221
|
+
pyproject_file.write_text(code)
|
|
222
|
+
logger.success("Created pyproject.toml")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _create_readme(target: Path) -> None:
|
|
226
|
+
"""Create README.md file.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
target: Target repository path.
|
|
230
|
+
"""
|
|
231
|
+
readme_file = target / "README.md"
|
|
232
|
+
if readme_file.exists():
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
logger.info("Creating README.md")
|
|
236
|
+
readme_file.touch()
|
|
237
|
+
logger.success("Created README.md")
|
|
238
|
+
|
|
239
|
+
|
|
43
240
|
def init(
|
|
44
241
|
target: Path,
|
|
45
242
|
project_name: str | None = None,
|
|
46
243
|
package_name: str | None = None,
|
|
47
244
|
with_dev_dependencies: bool = False,
|
|
245
|
+
git_host: str | None = None,
|
|
48
246
|
):
|
|
49
247
|
"""Initialize or validate .github/rhiza/template.yml in the target repository.
|
|
50
248
|
|
|
@@ -56,130 +254,42 @@ def init(
|
|
|
56
254
|
project_name: Custom project name. Defaults to target directory name.
|
|
57
255
|
package_name: Custom package name. Defaults to normalized project name.
|
|
58
256
|
with_dev_dependencies: Include development dependencies in pyproject.toml.
|
|
257
|
+
git_host: Target Git hosting platform ("github" or "gitlab"). Determines which
|
|
258
|
+
CI/CD configuration files to include. If None, will prompt user interactively.
|
|
59
259
|
|
|
60
260
|
Returns:
|
|
61
261
|
bool: True if validation passes, False otherwise.
|
|
62
262
|
"""
|
|
63
|
-
# Convert to absolute path to avoid surprises
|
|
64
263
|
target = target.resolve()
|
|
264
|
+
git_host = _validate_git_host(git_host)
|
|
65
265
|
|
|
66
266
|
logger.info(f"Initializing Rhiza configuration in: {target}")
|
|
67
267
|
|
|
68
|
-
# Create .rhiza directory
|
|
69
|
-
# This is where Rhiza stores its configuration
|
|
268
|
+
# Create .rhiza directory
|
|
70
269
|
rhiza_dir = target / ".rhiza"
|
|
71
270
|
logger.debug(f"Ensuring directory exists: {rhiza_dir}")
|
|
72
271
|
rhiza_dir.mkdir(parents=True, exist_ok=True)
|
|
73
272
|
|
|
74
|
-
#
|
|
75
|
-
|
|
273
|
+
# Determine git host
|
|
274
|
+
if git_host is None:
|
|
275
|
+
git_host = _prompt_git_host()
|
|
76
276
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
logger.info("Creating default .rhiza/template.yml")
|
|
80
|
-
logger.debug("Using default template configuration")
|
|
81
|
-
|
|
82
|
-
# Default template points to the jebel-quant/rhiza repository
|
|
83
|
-
# and includes common Python project configuration files
|
|
84
|
-
default_template = RhizaTemplate(
|
|
85
|
-
template_repository="jebel-quant/rhiza",
|
|
86
|
-
template_branch="main",
|
|
87
|
-
include=[
|
|
88
|
-
".rhiza", # .rhiza folder
|
|
89
|
-
".github", # GitHub configuration and workflows
|
|
90
|
-
".editorconfig", # Editor configuration
|
|
91
|
-
".gitignore", # Git ignore patterns
|
|
92
|
-
".pre-commit-config.yaml", # Pre-commit hooks
|
|
93
|
-
"ruff.toml", # Ruff linter configuration
|
|
94
|
-
"Makefile", # Build and development tasks
|
|
95
|
-
"pytest.ini", # Pytest configuration
|
|
96
|
-
"book", # Documentation book
|
|
97
|
-
"presentation", # Presentation materials
|
|
98
|
-
"tests", # Test structure
|
|
99
|
-
],
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
# Write the default template to the file
|
|
103
|
-
logger.debug(f"Writing default template to: {template_file}")
|
|
104
|
-
default_template.to_yaml(template_file)
|
|
105
|
-
|
|
106
|
-
logger.success("✓ Created .rhiza/template.yml")
|
|
107
|
-
logger.info("""
|
|
108
|
-
Next steps:
|
|
109
|
-
1. Review and customize .rhiza/template.yml to match your project needs
|
|
110
|
-
2. Run 'rhiza materialize' to inject templates into your repository
|
|
111
|
-
""")
|
|
277
|
+
# Create template file
|
|
278
|
+
_create_template_file(target, git_host)
|
|
112
279
|
|
|
113
|
-
# Bootstrap
|
|
114
|
-
# Get the name of the parent directory to use as package name
|
|
280
|
+
# Bootstrap Python project structure
|
|
115
281
|
if project_name is None:
|
|
116
282
|
project_name = target.name
|
|
117
|
-
|
|
118
283
|
if package_name is None:
|
|
119
284
|
package_name = _normalize_package_name(project_name)
|
|
120
285
|
|
|
121
286
|
logger.debug(f"Project name: {project_name}")
|
|
122
287
|
logger.debug(f"Package name: {package_name}")
|
|
123
288
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
logger.info(f"Creating Python package structure: {src_folder}")
|
|
128
|
-
src_folder.mkdir(parents=True)
|
|
129
|
-
|
|
130
|
-
# Create __init__.py to make it a proper Python package
|
|
131
|
-
init_file = src_folder / "__init__.py"
|
|
132
|
-
logger.debug(f"Creating {init_file}")
|
|
133
|
-
init_file.touch()
|
|
134
|
-
|
|
135
|
-
template_content = (
|
|
136
|
-
importlib.resources.files("rhiza").joinpath("_templates/basic/__init__.py.jinja2").read_text()
|
|
137
|
-
)
|
|
138
|
-
template = Template(template_content)
|
|
139
|
-
code = template.render(project_name=project_name)
|
|
140
|
-
init_file.write_text(code)
|
|
141
|
-
|
|
142
|
-
# Create main.py with a simple "Hello World" example
|
|
143
|
-
main_file = src_folder / "main.py"
|
|
144
|
-
logger.debug(f"Creating {main_file} with example code")
|
|
145
|
-
main_file.touch()
|
|
146
|
-
|
|
147
|
-
# Write example code to main.py
|
|
148
|
-
template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/main.py.jinja2").read_text()
|
|
149
|
-
template = Template(template_content)
|
|
150
|
-
code = template.render(project_name=project_name)
|
|
151
|
-
main_file.write_text(code)
|
|
152
|
-
logger.success(f"Created Python package structure in {src_folder}")
|
|
153
|
-
|
|
154
|
-
# Create pyproject.toml if it doesn't exist
|
|
155
|
-
# This is the standard Python package metadata file (PEP 621)
|
|
156
|
-
pyproject_file = target / "pyproject.toml"
|
|
157
|
-
if not pyproject_file.exists():
|
|
158
|
-
logger.info("Creating pyproject.toml with basic project metadata")
|
|
159
|
-
pyproject_file.touch()
|
|
160
|
-
|
|
161
|
-
# Write minimal pyproject.toml content
|
|
162
|
-
template_content = (
|
|
163
|
-
importlib.resources.files("rhiza").joinpath("_templates/basic/pyproject.toml.jinja2").read_text()
|
|
164
|
-
)
|
|
165
|
-
template = Template(template_content)
|
|
166
|
-
code = template.render(
|
|
167
|
-
project_name=project_name,
|
|
168
|
-
package_name=package_name,
|
|
169
|
-
with_dev_dependencies=with_dev_dependencies,
|
|
170
|
-
)
|
|
171
|
-
pyproject_file.write_text(code)
|
|
172
|
-
logger.success("Created pyproject.toml")
|
|
173
|
-
|
|
174
|
-
# Create README.md if it doesn't exist
|
|
175
|
-
# Every project should have a README
|
|
176
|
-
readme_file = target / "README.md"
|
|
177
|
-
if not readme_file.exists():
|
|
178
|
-
logger.info("Creating README.md")
|
|
179
|
-
readme_file.touch()
|
|
180
|
-
logger.success("Created README.md")
|
|
289
|
+
_create_python_package(target, project_name, package_name)
|
|
290
|
+
_create_pyproject_toml(target, project_name, package_name, with_dev_dependencies)
|
|
291
|
+
_create_readme(target)
|
|
181
292
|
|
|
182
|
-
# Validate the template file
|
|
183
|
-
# This will catch any issues early
|
|
293
|
+
# Validate the template file
|
|
184
294
|
logger.debug("Validating template configuration")
|
|
185
295
|
return validate(target)
|