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.
@@ -10,37 +10,28 @@ import yaml
10
10
  from loguru import logger
11
11
 
12
12
 
13
- def validate(target: Path) -> bool:
14
- """Validate template.yml configuration in the target repository.
15
-
16
- Performs authoritative validation of the template configuration:
17
- - Checks if target is a git repository
18
- - Checks for standard project structure (src and tests folders)
19
- - Checks for pyproject.toml (required)
20
- - Checks if template.yml exists
21
- - Validates YAML syntax
22
- - Validates required fields
23
- - Validates field values are appropriate
13
+ def _check_git_repository(target: Path) -> bool:
14
+ """Check if target is a git repository.
24
15
 
25
16
  Args:
26
- target: Path to the target Git repository directory.
17
+ target: Path to check.
27
18
 
28
19
  Returns:
29
- True if validation passes, False otherwise.
20
+ True if valid git repository, False otherwise.
30
21
  """
31
- # Convert to absolute path to avoid path resolution issues
32
- target = target.resolve()
33
-
34
- # Check if target is a git repository by looking for .git directory
35
- # Rhiza only works with git repositories
36
22
  if not (target / ".git").is_dir():
37
23
  logger.error(f"Target directory is not a git repository: {target}")
38
24
  logger.error("Initialize a git repository with 'git init' first")
39
25
  return False
26
+ return True
40
27
 
41
- logger.info(f"Validating template configuration in: {target}")
42
28
 
43
- # Check for standard project structure (src and tests folders)
29
+ def _check_project_structure(target: Path) -> None:
30
+ """Check for standard project structure.
31
+
32
+ Args:
33
+ target: Path to project.
34
+ """
44
35
  logger.debug("Validating project structure")
45
36
  src_dir = target / "src"
46
37
  tests_dir = target / "tests"
@@ -57,7 +48,16 @@ def validate(target: Path) -> bool:
57
48
  else:
58
49
  logger.success(f"'tests' folder exists: {tests_dir}")
59
50
 
60
- # Check for pyproject.toml - this is always required
51
+
52
+ def _check_pyproject_toml(target: Path) -> bool:
53
+ """Check for pyproject.toml file.
54
+
55
+ Args:
56
+ target: Path to project.
57
+
58
+ Returns:
59
+ True if pyproject.toml exists, False otherwise.
60
+ """
61
61
  logger.debug("Validating pyproject.toml")
62
62
  pyproject_file = target / "pyproject.toml"
63
63
 
@@ -68,8 +68,18 @@ def validate(target: Path) -> bool:
68
68
  return False
69
69
  else:
70
70
  logger.success(f"pyproject.toml exists: {pyproject_file}")
71
+ return True
72
+
73
+
74
+ def _check_template_file_exists(target: Path) -> tuple[bool, Path]:
75
+ """Check if template file exists.
71
76
 
72
- # Check for template.yml in new location only
77
+ Args:
78
+ target: Path to project.
79
+
80
+ Returns:
81
+ Tuple of (exists, template_file_path).
82
+ """
73
83
  template_file = target / ".rhiza" / "template.yml"
74
84
 
75
85
  if not template_file.exists():
@@ -82,11 +92,21 @@ def validate(target: Path) -> bool:
82
92
  logger.info("")
83
93
  logger.info("The 'rhiza migrate' command will move your configuration from")
84
94
  logger.info(" .github/rhiza/template.yml → .rhiza/template.yml")
85
- return False
95
+ return False, template_file
86
96
 
87
97
  logger.success(f"Template file exists: {template_file.relative_to(target)}")
98
+ return True, template_file
99
+
100
+
101
+ def _parse_yaml_file(template_file: Path) -> tuple[bool, dict | None]:
102
+ """Parse YAML file and return configuration.
88
103
 
89
- # Validate YAML syntax by attempting to parse the file
104
+ Args:
105
+ template_file: Path to template file.
106
+
107
+ Returns:
108
+ Tuple of (success, config_dict).
109
+ """
90
110
  logger.debug(f"Parsing YAML file: {template_file}")
91
111
  try:
92
112
  with open(template_file) as f:
@@ -94,19 +114,26 @@ def validate(target: Path) -> bool:
94
114
  except yaml.YAMLError as e:
95
115
  logger.error(f"Invalid YAML syntax in template.yml: {e}")
96
116
  logger.error("Fix the YAML syntax errors and try again")
97
- return False
117
+ return False, None
98
118
 
99
- # Check if the file is completely empty
100
119
  if config is None:
101
120
  logger.error("template.yml is empty")
102
121
  logger.error("Add configuration to template.yml or run 'rhiza init' to generate defaults")
103
- return False
122
+ return False, None
104
123
 
105
124
  logger.success("YAML syntax is valid")
125
+ return True, config
126
+
127
+
128
+ def _validate_required_fields(config: dict) -> bool:
129
+ """Validate required fields exist and have correct types.
106
130
 
107
- # Validate required fields exist and have correct types
108
- # template-repository: Must be a string in 'owner/repo' format
109
- # include: Must be a non-empty list of paths
131
+ Args:
132
+ config: Configuration dictionary.
133
+
134
+ Returns:
135
+ True if all validations pass, False otherwise.
136
+ """
110
137
  logger.debug("Validating required fields")
111
138
  required_fields = {
112
139
  "template-repository": str,
@@ -115,7 +142,6 @@ def validate(target: Path) -> bool:
115
142
 
116
143
  validation_passed = True
117
144
 
118
- # Check each required field
119
145
  for field, expected_type in required_fields.items():
120
146
  if field not in config:
121
147
  logger.error(f"Missing required field: {field}")
@@ -130,47 +156,77 @@ def validate(target: Path) -> bool:
130
156
  else:
131
157
  logger.success(f"Field '{field}' is present and valid")
132
158
 
133
- # Validate template-repository format
134
- # Must be in 'owner/repo' format (e.g., 'jebel-quant/rhiza')
159
+ return validation_passed
160
+
161
+
162
+ def _validate_repository_format(config: dict) -> bool:
163
+ """Validate template-repository format.
164
+
165
+ Args:
166
+ config: Configuration dictionary.
167
+
168
+ Returns:
169
+ True if valid, False otherwise.
170
+ """
135
171
  logger.debug("Validating template-repository format")
136
- if "template-repository" in config:
137
- repo = config["template-repository"]
138
- if not isinstance(repo, str):
139
- logger.error(f"template-repository must be a string, got {type(repo).__name__}")
140
- logger.error("Example: 'owner/repository'")
141
- validation_passed = False
142
- elif "/" not in repo:
143
- logger.error(f"template-repository must be in format 'owner/repo', got: {repo}")
144
- logger.error("Example: 'jebel-quant/rhiza'")
145
- validation_passed = False
146
- else:
147
- logger.success(f"template-repository format is valid: {repo}")
172
+ if "template-repository" not in config:
173
+ return True
148
174
 
149
- # Validate include paths
150
- # Must be a non-empty list of strings
175
+ repo = config["template-repository"]
176
+ if not isinstance(repo, str):
177
+ logger.error(f"template-repository must be a string, got {type(repo).__name__}")
178
+ logger.error("Example: 'owner/repository'")
179
+ return False
180
+ elif "/" not in repo:
181
+ logger.error(f"template-repository must be in format 'owner/repo', got: {repo}")
182
+ logger.error("Example: 'jebel-quant/rhiza'")
183
+ return False
184
+ else:
185
+ logger.success(f"template-repository format is valid: {repo}")
186
+ return True
187
+
188
+
189
+ def _validate_include_paths(config: dict) -> bool:
190
+ """Validate include paths.
191
+
192
+ Args:
193
+ config: Configuration dictionary.
194
+
195
+ Returns:
196
+ True if valid, False otherwise.
197
+ """
151
198
  logger.debug("Validating include paths")
152
- if "include" in config:
153
- include = config["include"]
154
- if not isinstance(include, list):
155
- logger.error(f"include must be a list, got {type(include).__name__}")
156
- logger.error("Example: include: ['.github', '.gitignore']")
157
- validation_passed = False
158
- elif len(include) == 0:
159
- logger.error("include list cannot be empty")
160
- logger.error("Add at least one path to materialize")
161
- validation_passed = False
162
- else:
163
- logger.success(f"include list has {len(include)} path(s)")
164
- # Log each included path for transparency
165
- for path in include:
166
- if not isinstance(path, str):
167
- logger.warning(f"include path should be a string, got {type(path).__name__}: {path}")
168
- else:
169
- logger.info(f" - {path}")
199
+ if "include" not in config:
200
+ return True
170
201
 
171
- # Validate optional fields if present
172
- # template-branch: Branch name in the template repository
202
+ include = config["include"]
203
+ if not isinstance(include, list):
204
+ logger.error(f"include must be a list, got {type(include).__name__}")
205
+ logger.error("Example: include: ['.github', '.gitignore']")
206
+ return False
207
+ elif len(include) == 0:
208
+ logger.error("include list cannot be empty")
209
+ logger.error("Add at least one path to materialize")
210
+ return False
211
+ else:
212
+ logger.success(f"include list has {len(include)} path(s)")
213
+ for path in include:
214
+ if not isinstance(path, str):
215
+ logger.warning(f"include path should be a string, got {type(path).__name__}: {path}")
216
+ else:
217
+ logger.info(f" - {path}")
218
+ return True
219
+
220
+
221
+ def _validate_optional_fields(config: dict) -> None:
222
+ """Validate optional fields if present.
223
+
224
+ Args:
225
+ config: Configuration dictionary.
226
+ """
173
227
  logger.debug("Validating optional fields")
228
+
229
+ # template-branch
174
230
  if "template-branch" in config:
175
231
  branch = config["template-branch"]
176
232
  if not isinstance(branch, str):
@@ -179,7 +235,7 @@ def validate(target: Path) -> bool:
179
235
  else:
180
236
  logger.success(f"template-branch is valid: {branch}")
181
237
 
182
- # template-host: Git hosting platform (github or gitlab)
238
+ # template-host
183
239
  if "template-host" in config:
184
240
  host = config["template-host"]
185
241
  if not isinstance(host, str):
@@ -191,7 +247,7 @@ def validate(target: Path) -> bool:
191
247
  else:
192
248
  logger.success(f"template-host is valid: {host}")
193
249
 
194
- # exclude: Optional list of paths to exclude from materialization
250
+ # exclude
195
251
  if "exclude" in config:
196
252
  exclude = config["exclude"]
197
253
  if not isinstance(exclude, list):
@@ -199,14 +255,69 @@ def validate(target: Path) -> bool:
199
255
  logger.warning("Example: exclude: ['.github/workflows/ci.yml']")
200
256
  else:
201
257
  logger.success(f"exclude list has {len(exclude)} path(s)")
202
- # Log each excluded path for transparency
203
258
  for path in exclude:
204
259
  if not isinstance(path, str):
205
260
  logger.warning(f"exclude path should be a string, got {type(path).__name__}: {path}")
206
261
  else:
207
262
  logger.info(f" - {path}")
208
263
 
209
- # Final verdict on validation
264
+
265
+ def validate(target: Path) -> bool:
266
+ """Validate template.yml configuration in the target repository.
267
+
268
+ Performs authoritative validation of the template configuration:
269
+ - Checks if target is a git repository
270
+ - Checks for standard project structure (src and tests folders)
271
+ - Checks for pyproject.toml (required)
272
+ - Checks if template.yml exists
273
+ - Validates YAML syntax
274
+ - Validates required fields
275
+ - Validates field values are appropriate
276
+
277
+ Args:
278
+ target: Path to the target Git repository directory.
279
+
280
+ Returns:
281
+ True if validation passes, False otherwise.
282
+ """
283
+ target = target.resolve()
284
+ logger.info(f"Validating template configuration in: {target}")
285
+
286
+ # Check if target is a git repository
287
+ if not _check_git_repository(target):
288
+ return False
289
+
290
+ # Check for standard project structure
291
+ _check_project_structure(target)
292
+
293
+ # Check for pyproject.toml
294
+ if not _check_pyproject_toml(target):
295
+ return False
296
+
297
+ # Check for template file
298
+ exists, template_file = _check_template_file_exists(target)
299
+ if not exists:
300
+ return False
301
+
302
+ # Parse YAML file
303
+ success, config = _parse_yaml_file(template_file)
304
+ if not success or config is None:
305
+ return False
306
+
307
+ # Validate required fields
308
+ validation_passed = _validate_required_fields(config)
309
+
310
+ # Validate specific field formats
311
+ if not _validate_repository_format(config):
312
+ validation_passed = False
313
+
314
+ if not _validate_include_paths(config):
315
+ validation_passed = False
316
+
317
+ # Validate optional fields
318
+ _validate_optional_fields(config)
319
+
320
+ # Final verdict
210
321
  logger.debug("Validation complete, determining final result")
211
322
  if validation_passed:
212
323
  logger.success("✓ Validation passed: template.yml is valid")
@@ -0,0 +1,26 @@
1
+ """Utilities for secure subprocess execution.
2
+
3
+ This module provides helper functions to resolve executable paths
4
+ to prevent PATH manipulation security vulnerabilities.
5
+ """
6
+
7
+ import shutil
8
+
9
+
10
+ def get_git_executable() -> str:
11
+ """Get the absolute path to the git executable.
12
+
13
+ This function ensures we use the full path to git to prevent
14
+ security issues related to PATH manipulation.
15
+
16
+ Returns:
17
+ str: Absolute path to the git executable.
18
+
19
+ Raises:
20
+ RuntimeError: If git executable is not found in PATH.
21
+ """
22
+ git_path = shutil.which("git")
23
+ if git_path is None:
24
+ msg = "git executable not found in PATH. Please ensure git is installed and available."
25
+ raise RuntimeError(msg)
26
+ return git_path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhiza
3
- Version: 0.8.2
3
+ Version: 0.8.4
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
@@ -30,19 +30,27 @@ Requires-Dist: pre-commit==4.5.1; extra == 'dev'
30
30
  Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
31
31
  Requires-Dist: pytest-html>=4.1.1; extra == 'dev'
32
32
  Requires-Dist: pytest==9.0.2; extra == 'dev'
33
+ Requires-Dist: python-dotenv==1.2.1; extra == 'dev'
34
+ Provides-Extra: tools
35
+ Requires-Dist: rhiza-tools; extra == 'tools'
33
36
  Description-Content-Type: text/markdown
34
37
 
35
- # rhiza-cli
38
+ <div align="center">
39
+
40
+ # <img src="https://raw.githubusercontent.com/Jebel-Quant/rhiza/main/assets/rhiza-logo.svg" alt="Rhiza Logo" width="30" style="vertical-align: middle;"> rhiza-cli
41
+ ![Synced with Rhiza](https://img.shields.io/badge/synced%20with-rhiza-2FA4A9?color=2FA4A9)
36
42
 
37
43
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
38
44
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
39
45
  [![PyPI version](https://img.shields.io/pypi/v/rhiza.svg)](https://pypi.org/project/rhiza/)
40
- [![Coverage](https://img.shields.io/badge/coverage-report-brightgreen.svg)](https://jebel-quant.github.io/rhiza-cli/tests/html-coverage/index.html)
46
+ [![Coverage](https://img.shields.io/endpoint?url=https://jebel-quant.github.io/rhiza-cli/tests/coverage-badge.json)](https://jebel-quant.github.io/rhiza-cli/tests/html-coverage/index.html)
41
47
  [![Downloads](https://static.pepy.tech/personalized-badge/rhiza?period=month&units=international_system&left_color=black&right_color=orange&left_text=PyPI%20downloads%20per%20month)](https://pepy.tech/project/rhiza)
48
+ [![CodeFactor](https://www.codefactor.io/repository/github/jebel-quant/rhiza-cli/badge)](https://www.codefactor.io/repository/github/jebel-quant/rhiza-cli)
42
49
 
43
50
  Command-line interface for managing reusable configuration templates for modern Python projects.
44
51
 
45
52
  **📖 New to Rhiza? Check out the [Getting Started Guide](GETTING_STARTED.md) for a beginner-friendly introduction!**
53
+ </div>
46
54
 
47
55
  ## Overview
48
56
 
@@ -0,0 +1,20 @@
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=fW9lofkkid-bghk2bXEgBdGbZ4scSqG726fMrVfKX_M,3454
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=CgT6x1huRCWKrRQ_-YUoRFGHSB_mpt6M6shObKna_rY,17716
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.4.dist-info/METADATA,sha256=9SF-BJOICgG-HW7oab4nX9UvzpxhMZPygLtoKIWXtPk,25743
17
+ rhiza-0.8.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
+ rhiza-0.8.4.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
19
+ rhiza-0.8.4.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
20
+ rhiza-0.8.4.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- rhiza/__init__.py,sha256=iW3niLBjwRKxcMhIV_1eb78putjUTo2tbZsadofluJk,1939
2
- rhiza/__main__.py,sha256=Lx0GqVZo6ymm0f18_uYB6E7_SOWwJNYjb73Vr31oLoM,236
3
- rhiza/cli.py,sha256=I5A5d1-3xrL2gdh5H9Itm9uiQjoPiGEbHYyxXddHEOk,8196
4
- rhiza/models.py,sha256=fW9lofkkid-bghk2bXEgBdGbZ4scSqG726fMrVfKX_M,3454
5
- rhiza/_templates/basic/__init__.py.jinja2,sha256=gs8qN4LAKcdFd6iO9gZVLuVetODmZP_TGuEjWrbinC0,27
6
- rhiza/_templates/basic/main.py.jinja2,sha256=uTCahxf9Bftao1IghHue4cSZ9YzBYmBEXeIhEmK9UXQ,362
7
- rhiza/_templates/basic/pyproject.toml.jinja2,sha256=Mizpnnd_kFQd-pCWOxG-KWhvg4_ZhZaQppTt2pz0WOc,695
8
- rhiza/commands/__init__.py,sha256=Z5CeMh7ylX27H6dvwqRbEKzYo5pwQq-5TyTxABUSaQg,1848
9
- rhiza/commands/init.py,sha256=hQTd1y4fcT6JVNVVAxnr463jnt0dNG_Ku-fny4DSIfI,6830
10
- rhiza/commands/materialize.py,sha256=AbVXJrR8faa3t7m_Xl5TR8VE3ODmkh2oiAwCYFA36wA,17343
11
- rhiza/commands/migrate.py,sha256=hhSkj2iafCCxKrrVOfnPWDjK7fTJ5ReAJsWNDGz71s0,6307
12
- rhiza/commands/uninstall.py,sha256=z95xqamV7wGPr8PBveWzaRmtD5bPhOrTLI0GcOvpnAo,5371
13
- rhiza/commands/validate.py,sha256=1HMQWF9Syv7JKC31AkaPYMTnqy8HOvAMxMLrYty36FQ,9112
14
- rhiza/commands/welcome.py,sha256=w3BziR042o6oYincd3EqDsFzF6qqInU7iYhWjF3yJqY,2382
15
- rhiza-0.8.2.dist-info/METADATA,sha256=YXsp3ECxAvgQUjWkksYpaRNTUbENYnhQPKcdDJKJJ_A,25156
16
- rhiza-0.8.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
- rhiza-0.8.2.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
18
- rhiza-0.8.2.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
19
- rhiza-0.8.2.dist-info/RECORD,,
File without changes