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 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
- - .github (workflows, actions, etc.)
98
- - .editorconfig
99
- - .gitignore
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 structure if it doesn't exist
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
- # Define the template file path
75
- template_file = rhiza_dir / "template.yml"
273
+ # Determine git host
274
+ if git_host is None:
275
+ git_host = _prompt_git_host()
76
276
 
77
- if not template_file.exists():
78
- # Create default template.yml with sensible defaults
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 basic Python project structure if it doesn't exist
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
- # Create src/{package_name} directory structure following src-layout
125
- src_folder = target / "src" / package_name
126
- if not (target / "src").exists():
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 to ensure it's correct
183
- # This will catch any issues early
293
+ # Validate the template file
184
294
  logger.debug("Validating template configuration")
185
295
  return validate(target)