rhiza 0.8.3__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/commands/init.py +213 -162
- 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.3.dist-info → rhiza-0.8.4.dist-info}/METADATA +11 -3
- rhiza-0.8.4.dist-info/RECORD +20 -0
- rhiza-0.8.3.dist-info/RECORD +0 -19
- {rhiza-0.8.3.dist-info → rhiza-0.8.4.dist-info}/WHEEL +0 -0
- {rhiza-0.8.3.dist-info → rhiza-0.8.4.dist-info}/entry_points.txt +0 -0
- {rhiza-0.8.3.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/commands/init.py
CHANGED
|
@@ -28,20 +28,215 @@ def _normalize_package_name(name: str) -> str:
|
|
|
28
28
|
Returns:
|
|
29
29
|
A valid Python identifier safe for use as a package name.
|
|
30
30
|
"""
|
|
31
|
-
# Replace any character that is not a letter, number, or underscore with an underscore
|
|
32
31
|
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
|
|
33
|
-
|
|
34
|
-
# Ensure it doesn't start with a number
|
|
35
32
|
if name[0].isdigit():
|
|
36
33
|
name = f"_{name}"
|
|
37
|
-
|
|
38
|
-
# Ensure it's not a Python keyword
|
|
39
34
|
if keyword.iskeyword(name):
|
|
40
35
|
name = f"{name}_"
|
|
41
|
-
|
|
42
36
|
return name
|
|
43
37
|
|
|
44
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
|
+
|
|
45
240
|
def init(
|
|
46
241
|
target: Path,
|
|
47
242
|
project_name: str | None = None,
|
|
@@ -65,180 +260,36 @@ def init(
|
|
|
65
260
|
Returns:
|
|
66
261
|
bool: True if validation passes, False otherwise.
|
|
67
262
|
"""
|
|
68
|
-
# Convert to absolute path to avoid surprises
|
|
69
263
|
target = target.resolve()
|
|
70
|
-
|
|
71
|
-
# Validate git_host if provided
|
|
72
|
-
if git_host is not None:
|
|
73
|
-
git_host = git_host.lower()
|
|
74
|
-
if git_host not in ["github", "gitlab"]:
|
|
75
|
-
logger.error(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'")
|
|
76
|
-
raise ValueError(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'")
|
|
264
|
+
git_host = _validate_git_host(git_host)
|
|
77
265
|
|
|
78
266
|
logger.info(f"Initializing Rhiza configuration in: {target}")
|
|
79
267
|
|
|
80
|
-
# Create .rhiza directory
|
|
81
|
-
# This is where Rhiza stores its configuration
|
|
268
|
+
# Create .rhiza directory
|
|
82
269
|
rhiza_dir = target / ".rhiza"
|
|
83
270
|
logger.debug(f"Ensuring directory exists: {rhiza_dir}")
|
|
84
271
|
rhiza_dir.mkdir(parents=True, exist_ok=True)
|
|
85
272
|
|
|
86
|
-
#
|
|
87
|
-
|
|
273
|
+
# Determine git host
|
|
274
|
+
if git_host is None:
|
|
275
|
+
git_host = _prompt_git_host()
|
|
88
276
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
logger.info("Creating default .rhiza/template.yml")
|
|
92
|
-
logger.debug("Using default template configuration")
|
|
93
|
-
|
|
94
|
-
# Prompt for target git hosting platform if not provided
|
|
95
|
-
if git_host is None:
|
|
96
|
-
# Only prompt if running in an interactive terminal
|
|
97
|
-
if sys.stdin.isatty():
|
|
98
|
-
logger.info("Where will your project be hosted?")
|
|
99
|
-
git_host = typer.prompt(
|
|
100
|
-
"Target Git hosting platform (github/gitlab)",
|
|
101
|
-
type=str,
|
|
102
|
-
default="github",
|
|
103
|
-
).lower()
|
|
104
|
-
|
|
105
|
-
# Validate the input
|
|
106
|
-
while git_host not in ["github", "gitlab"]:
|
|
107
|
-
logger.warning(f"Invalid choice: {git_host}. Please choose 'github' or 'gitlab'")
|
|
108
|
-
git_host = typer.prompt(
|
|
109
|
-
"Target Git hosting platform (github/gitlab)",
|
|
110
|
-
type=str,
|
|
111
|
-
default="github",
|
|
112
|
-
).lower()
|
|
113
|
-
else:
|
|
114
|
-
# Non-interactive mode (e.g., tests), default to github
|
|
115
|
-
git_host = "github"
|
|
116
|
-
logger.debug("Non-interactive mode detected, defaulting to github")
|
|
117
|
-
|
|
118
|
-
# Adjust template based on target git hosting platform
|
|
119
|
-
# The template repository is always on GitHub (jebel-quant/rhiza)
|
|
120
|
-
# but we include different files based on where the target project will be
|
|
121
|
-
if git_host == "gitlab":
|
|
122
|
-
include_paths = [
|
|
123
|
-
".rhiza", # .rhiza folder
|
|
124
|
-
".gitlab", # .gitlab folder
|
|
125
|
-
".gitlab-ci.yml", # GitLab CI configuration
|
|
126
|
-
".editorconfig", # Editor configuration
|
|
127
|
-
".gitignore", # Git ignore patterns
|
|
128
|
-
".pre-commit-config.yaml", # Pre-commit hooks
|
|
129
|
-
"ruff.toml", # Ruff linter configuration
|
|
130
|
-
"Makefile", # Build and development tasks
|
|
131
|
-
"pytest.ini", # Pytest configuration
|
|
132
|
-
"book", # Documentation book
|
|
133
|
-
"presentation", # Presentation materials
|
|
134
|
-
"tests", # Test structure
|
|
135
|
-
]
|
|
136
|
-
else:
|
|
137
|
-
include_paths = [
|
|
138
|
-
".rhiza", # .rhiza folder
|
|
139
|
-
".github", # GitHub configuration and workflows
|
|
140
|
-
".editorconfig", # Editor configuration
|
|
141
|
-
".gitignore", # Git ignore patterns
|
|
142
|
-
".pre-commit-config.yaml", # Pre-commit hooks
|
|
143
|
-
"ruff.toml", # Ruff linter configuration
|
|
144
|
-
"Makefile", # Build and development tasks
|
|
145
|
-
"pytest.ini", # Pytest configuration
|
|
146
|
-
"book", # Documentation book
|
|
147
|
-
"presentation", # Presentation materials
|
|
148
|
-
"tests", # Test structure
|
|
149
|
-
]
|
|
150
|
-
|
|
151
|
-
# Default template points to the jebel-quant/rhiza repository on GitHub
|
|
152
|
-
# and includes files appropriate for the target platform
|
|
153
|
-
default_template = RhizaTemplate(
|
|
154
|
-
template_repository="jebel-quant/rhiza",
|
|
155
|
-
template_branch="main",
|
|
156
|
-
# template_host is not set here - it defaults to "github" in the model
|
|
157
|
-
# because the template repository is on GitHub
|
|
158
|
-
include=include_paths,
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
# Write the default template to the file
|
|
162
|
-
logger.debug(f"Writing default template to: {template_file}")
|
|
163
|
-
default_template.to_yaml(template_file)
|
|
164
|
-
|
|
165
|
-
logger.success("✓ Created .rhiza/template.yml")
|
|
166
|
-
logger.info("""
|
|
167
|
-
Next steps:
|
|
168
|
-
1. Review and customize .rhiza/template.yml to match your project needs
|
|
169
|
-
2. Run 'rhiza materialize' to inject templates into your repository
|
|
170
|
-
""")
|
|
277
|
+
# Create template file
|
|
278
|
+
_create_template_file(target, git_host)
|
|
171
279
|
|
|
172
|
-
# Bootstrap
|
|
173
|
-
# Get the name of the parent directory to use as package name
|
|
280
|
+
# Bootstrap Python project structure
|
|
174
281
|
if project_name is None:
|
|
175
282
|
project_name = target.name
|
|
176
|
-
|
|
177
283
|
if package_name is None:
|
|
178
284
|
package_name = _normalize_package_name(project_name)
|
|
179
285
|
|
|
180
286
|
logger.debug(f"Project name: {project_name}")
|
|
181
287
|
logger.debug(f"Package name: {package_name}")
|
|
182
288
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
logger.info(f"Creating Python package structure: {src_folder}")
|
|
187
|
-
src_folder.mkdir(parents=True)
|
|
188
|
-
|
|
189
|
-
# Create __init__.py to make it a proper Python package
|
|
190
|
-
init_file = src_folder / "__init__.py"
|
|
191
|
-
logger.debug(f"Creating {init_file}")
|
|
192
|
-
init_file.touch()
|
|
193
|
-
|
|
194
|
-
template_content = (
|
|
195
|
-
importlib.resources.files("rhiza").joinpath("_templates/basic/__init__.py.jinja2").read_text()
|
|
196
|
-
)
|
|
197
|
-
template = Template(template_content)
|
|
198
|
-
code = template.render(project_name=project_name)
|
|
199
|
-
init_file.write_text(code)
|
|
200
|
-
|
|
201
|
-
# Create main.py with a simple "Hello World" example
|
|
202
|
-
main_file = src_folder / "main.py"
|
|
203
|
-
logger.debug(f"Creating {main_file} with example code")
|
|
204
|
-
main_file.touch()
|
|
205
|
-
|
|
206
|
-
# Write example code to main.py
|
|
207
|
-
template_content = importlib.resources.files("rhiza").joinpath("_templates/basic/main.py.jinja2").read_text()
|
|
208
|
-
template = Template(template_content)
|
|
209
|
-
code = template.render(project_name=project_name)
|
|
210
|
-
main_file.write_text(code)
|
|
211
|
-
logger.success(f"Created Python package structure in {src_folder}")
|
|
212
|
-
|
|
213
|
-
# Create pyproject.toml if it doesn't exist
|
|
214
|
-
# This is the standard Python package metadata file (PEP 621)
|
|
215
|
-
pyproject_file = target / "pyproject.toml"
|
|
216
|
-
if not pyproject_file.exists():
|
|
217
|
-
logger.info("Creating pyproject.toml with basic project metadata")
|
|
218
|
-
pyproject_file.touch()
|
|
219
|
-
|
|
220
|
-
# Write minimal pyproject.toml content
|
|
221
|
-
template_content = (
|
|
222
|
-
importlib.resources.files("rhiza").joinpath("_templates/basic/pyproject.toml.jinja2").read_text()
|
|
223
|
-
)
|
|
224
|
-
template = Template(template_content)
|
|
225
|
-
code = template.render(
|
|
226
|
-
project_name=project_name,
|
|
227
|
-
package_name=package_name,
|
|
228
|
-
with_dev_dependencies=with_dev_dependencies,
|
|
229
|
-
)
|
|
230
|
-
pyproject_file.write_text(code)
|
|
231
|
-
logger.success("Created pyproject.toml")
|
|
232
|
-
|
|
233
|
-
# Create README.md if it doesn't exist
|
|
234
|
-
# Every project should have a README
|
|
235
|
-
readme_file = target / "README.md"
|
|
236
|
-
if not readme_file.exists():
|
|
237
|
-
logger.info("Creating README.md")
|
|
238
|
-
readme_file.touch()
|
|
239
|
-
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)
|
|
240
292
|
|
|
241
|
-
# Validate the template file
|
|
242
|
-
# This will catch any issues early
|
|
293
|
+
# Validate the template file
|
|
243
294
|
logger.debug("Validating template configuration")
|
|
244
295
|
return validate(target)
|