rhiza 0.8.3__py3-none-any.whl → 0.8.5__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 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 structure if it doesn't exist
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
- # Define the template file path
87
- template_file = rhiza_dir / "template.yml"
273
+ # Determine git host
274
+ if git_host is None:
275
+ git_host = _prompt_git_host()
88
276
 
89
- if not template_file.exists():
90
- # Create default template.yml with sensible defaults
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 basic Python project structure if it doesn't exist
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
- # Create src/{package_name} directory structure following src-layout
184
- src_folder = target / "src" / package_name
185
- if not (target / "src").exists():
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 to ensure it's correct
242
- # This will catch any issues early
293
+ # Validate the template file
243
294
  logger.debug("Validating template configuration")
244
295
  return validate(target)