rhiza 0.5.6__py3-none-any.whl → 0.6.1__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/__init__.py CHANGED
@@ -29,7 +29,7 @@ Validate your configuration:
29
29
  rhiza validate
30
30
  ```
31
31
 
32
- Customize `.github/template.yml`, then materialize templates into your project:
32
+ Customize `.github/rhiza/template.yml`, then materialize templates into your project:
33
33
 
34
34
  ```bash
35
35
  rhiza materialize
rhiza/cli.py CHANGED
@@ -71,10 +71,10 @@ def init(
71
71
  help="Target directory (defaults to current directory)",
72
72
  ),
73
73
  ):
74
- r"""Initialize or validate .github/template.yml.
74
+ r"""Initialize or validate .github/rhiza/template.yml.
75
75
 
76
76
  \b
77
- Creates a default `.github/template.yml` configuration file if one
77
+ Creates a default `.github/rhiza/template.yml` configuration file if one
78
78
  doesn't exist, or validates the existing configuration.
79
79
 
80
80
  \b
@@ -117,10 +117,10 @@ def materialize(
117
117
 
118
118
  \b
119
119
  Materializes configuration files from the template repository specified
120
- in .github/template.yml into your project. This command:
120
+ in .github/rhiza/template.yml into your project. This command:
121
121
 
122
122
  \b
123
- - Reads .github/template.yml configuration
123
+ - Reads .github/rhiza/template.yml configuration
124
124
  - Performs a sparse clone of the template repository
125
125
  - Copies specified files/directories to your project
126
126
  - Respects exclusion patterns defined in the configuration
@@ -149,7 +149,7 @@ def validate(
149
149
  ):
150
150
  r"""Validate Rhiza template configuration.
151
151
 
152
- Validates the .github/template.yml file to ensure it is syntactically
152
+ Validates the .github/rhiza/template.yml file to ensure it is syntactically
153
153
  correct and semantically valid.
154
154
 
155
155
  \b
@@ -8,7 +8,7 @@ configuration templates for Python projects.
8
8
 
9
9
  ### init
10
10
 
11
- Initialize or validate `.github/template.yml` in a target directory.
11
+ Initialize or validate `.github/rhiza/template.yml` in a target directory.
12
12
 
13
13
  Creates a default configuration file if it doesn't exist, or validates
14
14
  an existing one. The default configuration includes common Python project
@@ -29,7 +29,7 @@ is used.
29
29
 
30
30
  Validate Rhiza template configuration.
31
31
 
32
- Validates the `.github/template.yml` file to ensure it is syntactically
32
+ Validates the `.github/rhiza/template.yml` file to ensure it is syntactically
33
33
  correct and semantically valid. Performs comprehensive validation including
34
34
  YAML syntax checking, required field verification, field type validation,
35
35
  and repository format verification.
rhiza/commands/init.py CHANGED
@@ -1,10 +1,11 @@
1
- """Command to initialize or validate .github/template.yml.
1
+ """Command to initialize or validate .github/rhiza/template.yml.
2
2
 
3
3
  This module provides the init command that creates or validates the
4
- .github/template.yml file, which defines where templates come from
4
+ .github/rhiza/template.yml file, which defines where templates come from
5
5
  and what paths are governed by Rhiza.
6
6
  """
7
7
 
8
+ import shutil
8
9
  from pathlib import Path
9
10
 
10
11
  from loguru import logger
@@ -14,51 +15,140 @@ from rhiza.models import RhizaTemplate
14
15
 
15
16
 
16
17
  def init(target: Path):
17
- """Initialize or validate .github/template.yml in the target repository.
18
+ """Initialize or validate .github/rhiza/template.yml in the target repository.
18
19
 
19
- Creates a default .github/template.yml file if it doesn't exist,
20
+ Creates a default .github/rhiza/template.yml file if it doesn't exist,
20
21
  or validates an existing one.
21
22
 
22
23
  Args:
23
24
  target: Path to the target directory. Defaults to the current working directory.
25
+
26
+ Returns:
27
+ bool: True if validation passes, False otherwise.
24
28
  """
25
29
  # Convert to absolute path to avoid surprises
26
30
  target = target.resolve()
27
31
 
28
32
  logger.info(f"Initializing Rhiza configuration in: {target}")
29
33
 
30
- # Create .github directory if it doesn't exist
34
+ # Create .github/rhiza directory structure if it doesn't exist
35
+ # This is where Rhiza stores its configuration
31
36
  github_dir = target / ".github"
32
- github_dir.mkdir(parents=True, exist_ok=True)
37
+ rhiza_dir = github_dir / "rhiza"
38
+ logger.debug(f"Ensuring directory exists: {rhiza_dir}")
39
+ rhiza_dir.mkdir(parents=True, exist_ok=True)
33
40
 
34
- # Define the template file path
41
+ # Check for old location and migrate if necessary
42
+ # TODO: This migration logic can be removed in a future version
43
+ # after users have had time to migrate
35
44
  template_file = github_dir / "template.yml"
45
+ if template_file.exists():
46
+ logger.warning(f"Found template.yml in old location: {template_file}")
47
+ logger.info(f"Copying to new location: {rhiza_dir / 'template.yml'}")
48
+ # Copy the file to the new location (not move, to preserve old one temporarily)
49
+ shutil.copyfile(template_file, rhiza_dir / "template.yml")
50
+
51
+ # Define the template file path (new location)
52
+ template_file = rhiza_dir / "template.yml"
36
53
 
37
54
  if not template_file.exists():
38
- # Create default template.yml
39
- logger.info("Creating default .github/template.yml")
55
+ # Create default template.yml with sensible defaults
56
+ logger.info("Creating default .github/rhiza/template.yml")
57
+ logger.debug("Using default template configuration")
40
58
 
59
+ # Default template points to the jebel-quant/rhiza repository
60
+ # and includes common Python project configuration files
41
61
  default_template = RhizaTemplate(
42
62
  template_repository="jebel-quant/rhiza",
43
63
  template_branch="main",
44
64
  include=[
45
- ".github",
46
- ".editorconfig",
47
- ".gitignore",
48
- ".pre-commit-config.yaml",
49
- "Makefile",
50
- "pytest.ini",
65
+ ".github", # GitHub configuration and workflows
66
+ ".editorconfig", # Editor configuration
67
+ ".gitignore", # Git ignore patterns
68
+ ".pre-commit-config.yaml", # Pre-commit hooks
69
+ "Makefile", # Build and development tasks
70
+ "pytest.ini", # Pytest configuration
71
+ "book", # Documentation book
72
+ "presentation", # Presentation materials
73
+ "tests", # Test structure
51
74
  ],
52
75
  )
53
76
 
77
+ # Write the default template to the file
78
+ logger.debug(f"Writing default template to: {template_file}")
54
79
  default_template.to_yaml(template_file)
55
80
 
56
- logger.success("✓ Created .github/template.yml")
81
+ logger.success("✓ Created .github/rhiza/template.yml")
57
82
  logger.info("""
58
83
  Next steps:
59
- 1. Review and customize .github/template.yml to match your project needs
84
+ 1. Review and customize .github/rhiza/template.yml to match your project needs
60
85
  2. Run 'rhiza materialize' to inject templates into your repository
61
86
  """)
62
87
 
63
- # the template file exists, so validate it
88
+ # Bootstrap basic Python project structure if it doesn't exist
89
+ # Get the name of the parent directory to use as package name
90
+ parent = target.parent.name
91
+ logger.debug(f"Parent directory name: {parent}")
92
+
93
+ # Create src/{parent} directory structure following src-layout
94
+ src_folder = target / "src" / parent
95
+ if not src_folder.exists():
96
+ logger.info(f"Creating Python package structure: {src_folder}")
97
+ src_folder.mkdir(parents=True)
98
+
99
+ # Create __init__.py to make it a proper Python package
100
+ init_file = src_folder / "__init__.py"
101
+ logger.debug(f"Creating {init_file}")
102
+ init_file.touch()
103
+
104
+ # Create main.py with a simple "Hello World" example
105
+ main_file = src_folder / "main.py"
106
+ logger.debug(f"Creating {main_file} with example code")
107
+ main_file.touch()
108
+
109
+ # Write example code to main.py
110
+ code = """\
111
+ def say_hello(name: str) -> str:
112
+ return f"Hello, {name}!"
113
+
114
+ def main():
115
+ print(say_hello("World"))
116
+
117
+ if __name__ == "__main__":
118
+ main()
119
+ """
120
+ main_file.write_text(code)
121
+ logger.success(f"Created Python package structure in {src_folder}")
122
+
123
+ # Create pyproject.toml if it doesn't exist
124
+ # This is the standard Python package metadata file (PEP 621)
125
+ pyproject_file = target / "pyproject.toml"
126
+ if not pyproject_file.exists():
127
+ logger.info("Creating pyproject.toml with basic project metadata")
128
+ pyproject_file.touch()
129
+
130
+ # Write minimal pyproject.toml content
131
+ code = f'''\
132
+ [project]
133
+ name = "{parent}"
134
+ version = "0.1.0"
135
+ description = "Add your description here"
136
+ readme = "README.md"
137
+ requires-python = ">=3.11"
138
+ dependencies = []
139
+ '''
140
+ pyproject_file.write_text(code)
141
+ logger.success("Created pyproject.toml")
142
+
143
+ # Create README.md if it doesn't exist
144
+ # Every project should have a README
145
+ readme_file = target / "README.md"
146
+ if not readme_file.exists():
147
+ logger.info("Creating README.md")
148
+ readme_file.touch()
149
+ logger.success("Created README.md")
150
+
151
+ # Validate the template file to ensure it's correct
152
+ # This will catch any issues early
153
+ logger.debug("Validating template configuration")
64
154
  return validate(target)
@@ -24,16 +24,27 @@ def __expand_paths(base_dir: Path, paths: list[str]) -> list[Path]:
24
24
 
25
25
  Given a list of paths relative to ``base_dir``, return a flat list of all
26
26
  individual files.
27
+
28
+ Args:
29
+ base_dir: The base directory to resolve paths against.
30
+ paths: List of relative path strings (files or directories).
31
+
32
+ Returns:
33
+ A flat list of Path objects representing all individual files found.
27
34
  """
28
35
  all_files = []
29
36
  for p in paths:
30
37
  full_path = base_dir / p
38
+ # Check if the path is a regular file
31
39
  if full_path.is_file():
32
40
  all_files.append(full_path)
41
+ # If it's a directory, recursively find all files within it
33
42
  elif full_path.is_dir():
34
43
  all_files.extend([f for f in full_path.rglob("*") if f.is_file()])
35
44
  else:
36
- # Path does not exist could log a warning
45
+ # Path does not exist in the cloned repository - skip it silently
46
+ # This can happen if the template repo doesn't have certain paths
47
+ logger.debug(f"Path not found in template repository: {p}")
37
48
  continue
38
49
  return all_files
39
50
 
@@ -52,22 +63,28 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
52
63
  the target repository.
53
64
  force (bool): Whether to overwrite existing files.
54
65
  """
66
+ # Resolve to absolute path to avoid any ambiguity
55
67
  target = target.resolve()
56
68
 
57
69
  logger.info(f"Target repository: {target}")
58
70
  logger.info(f"Rhiza branch: {branch}")
59
71
 
60
72
  # Set environment to prevent git from prompting for credentials
73
+ # This ensures non-interactive behavior during git operations
61
74
  git_env = os.environ.copy()
62
75
  git_env["GIT_TERMINAL_PROMPT"] = "0"
63
76
 
64
77
  # -----------------------
65
78
  # Handle target branch creation/checkout if specified
66
79
  # -----------------------
80
+ # When a target branch is specified, we either checkout an existing branch
81
+ # or create a new one. This allows users to materialize templates onto a
82
+ # separate branch for review before merging to main.
67
83
  if target_branch:
68
84
  logger.info(f"Creating/checking out target branch: {target_branch}")
69
85
  try:
70
- # Check if branch already exists
86
+ # Check if branch already exists using git rev-parse
87
+ # Returns 0 if the branch exists, non-zero otherwise
71
88
  result = subprocess.run(
72
89
  ["git", "rev-parse", "--verify", target_branch],
73
90
  cwd=target,
@@ -77,7 +94,7 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
77
94
  )
78
95
 
79
96
  if result.returncode == 0:
80
- # Branch exists, checkout
97
+ # Branch exists, switch to it
81
98
  logger.info(f"Branch '{target_branch}' exists, checking out...")
82
99
  subprocess.run(
83
100
  ["git", "checkout", target_branch],
@@ -86,7 +103,7 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
86
103
  env=git_env,
87
104
  )
88
105
  else:
89
- # Branch doesn't exist, create and checkout
106
+ # Branch doesn't exist, create it from current HEAD
90
107
  logger.info(f"Creating new branch '{target_branch}'...")
91
108
  subprocess.run(
92
109
  ["git", "checkout", "-b", target_branch],
@@ -101,49 +118,81 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
101
118
  # -----------------------
102
119
  # Ensure Rhiza is initialized
103
120
  # -----------------------
121
+ # The init function creates template.yml if missing and validates it
122
+ # Returns True if valid, False otherwise
104
123
  valid = init(target)
105
124
 
106
125
  if not valid:
107
- logger.error(f"Rhiza template is invalid. {target}")
126
+ logger.error(f"Rhiza template is invalid in: {target}")
127
+ logger.error("Please fix validation errors and try again")
108
128
  sys.exit(1)
109
129
 
110
- template_file = target / ".github" / "template.yml"
130
+ # Load the template configuration from the validated file
131
+ template_file = target / ".github" / "rhiza" / "template.yml"
132
+ logger.debug(f"Loading template configuration from: {template_file}")
111
133
  template = RhizaTemplate.from_yaml(template_file)
112
134
 
135
+ # Extract template configuration settings
136
+ # These define where to clone from and what to materialize
113
137
  rhiza_repo = template.template_repository
138
+ # Use CLI arg if template doesn't specify a branch
114
139
  rhiza_branch = template.template_branch or branch
140
+ # Default to GitHub if not specified
115
141
  rhiza_host = template.template_host or "github"
116
142
  include_paths = template.include
117
143
  excluded_paths = template.exclude
118
144
 
145
+ # Validate that we have paths to include
119
146
  if not include_paths:
147
+ logger.error("No include paths found in template.yml")
148
+ logger.error("Add at least one path to the 'include' list in template.yml")
120
149
  raise RuntimeError("No include paths found in template.yml")
121
150
 
151
+ # Log the paths we'll be including for transparency
122
152
  logger.info("Include paths:")
123
153
  for p in include_paths:
124
154
  logger.info(f" - {p}")
125
155
 
156
+ # Log excluded paths if any are defined
157
+ if excluded_paths:
158
+ logger.info("Exclude paths:")
159
+ for p in excluded_paths:
160
+ logger.info(f" - {p}")
161
+
126
162
  # -----------------------
127
163
  # Construct git clone URL based on host
128
164
  # -----------------------
165
+ # Support both GitHub and GitLab template repositories
129
166
  if rhiza_host == "gitlab":
130
167
  git_url = f"https://gitlab.com/{rhiza_repo}.git"
168
+ logger.debug(f"Using GitLab repository: {git_url}")
131
169
  elif rhiza_host == "github":
132
170
  git_url = f"https://github.com/{rhiza_repo}.git"
171
+ logger.debug(f"Using GitHub repository: {git_url}")
133
172
  else:
173
+ logger.error(f"Unsupported template-host: {rhiza_host}")
174
+ logger.error("template-host must be 'github' or 'gitlab'")
134
175
  raise ValueError(f"Unsupported template-host: {rhiza_host}. Must be 'github' or 'gitlab'.")
135
176
 
136
177
  # -----------------------
137
178
  # Sparse clone template repo
138
179
  # -----------------------
180
+ # Create a temporary directory for the sparse clone
181
+ # This will be cleaned up in the finally block
139
182
  tmp_dir = Path(tempfile.mkdtemp())
140
183
  materialized_files: list[Path] = []
141
184
 
142
185
  logger.info(f"Cloning {rhiza_repo}@{rhiza_branch} from {rhiza_host} into temporary directory")
186
+ logger.debug(f"Temporary directory: {tmp_dir}")
143
187
 
144
188
  try:
145
- # Clone the repository - capture output to avoid blocking
189
+ # Clone the repository using sparse checkout for efficiency
190
+ # --depth 1: Only fetch the latest commit (shallow clone)
191
+ # --filter=blob:none: Don't download file contents initially
192
+ # --sparse: Enable sparse checkout mode
193
+ # This combination allows us to clone only the paths we need
146
194
  try:
195
+ logger.debug("Executing git clone with sparse checkout")
147
196
  subprocess.run(
148
197
  [
149
198
  "git",
@@ -162,14 +211,18 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
162
211
  text=True,
163
212
  env=git_env,
164
213
  )
214
+ logger.debug("Git clone completed successfully")
165
215
  except subprocess.CalledProcessError as e:
166
216
  logger.error(f"Failed to clone repository: {e}")
167
217
  if e.stderr:
168
218
  logger.error(f"Git error: {e.stderr.strip()}")
219
+ logger.error(f"Check that the repository '{rhiza_repo}' exists and branch '{rhiza_branch}' is valid")
169
220
  raise
170
221
 
171
- # Initialize sparse checkout
222
+ # Initialize sparse checkout in cone mode
223
+ # Cone mode is more efficient and uses pattern matching
172
224
  try:
225
+ logger.debug("Initializing sparse checkout")
173
226
  subprocess.run(
174
227
  ["git", "sparse-checkout", "init", "--cone"],
175
228
  cwd=tmp_dir,
@@ -178,14 +231,17 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
178
231
  text=True,
179
232
  env=git_env,
180
233
  )
234
+ logger.debug("Sparse checkout initialized")
181
235
  except subprocess.CalledProcessError as e:
182
236
  logger.error(f"Failed to initialize sparse checkout: {e}")
183
237
  if e.stderr:
184
238
  logger.error(f"Git error: {e.stderr.strip()}")
185
239
  raise
186
240
 
187
- # Set sparse checkout paths
241
+ # Set sparse checkout paths to only checkout the files/directories we need
242
+ # --skip-checks: Don't validate that patterns match existing files
188
243
  try:
244
+ logger.debug(f"Setting sparse checkout paths: {include_paths}")
189
245
  subprocess.run(
190
246
  ["git", "sparse-checkout", "set", "--skip-checks", *include_paths],
191
247
  cwd=tmp_dir,
@@ -194,6 +250,7 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
194
250
  text=True,
195
251
  env=git_env,
196
252
  )
253
+ logger.debug("Sparse checkout paths configured")
197
254
  except subprocess.CalledProcessError as e:
198
255
  logger.error(f"Failed to set sparse checkout paths: {e}")
199
256
  if e.stderr:
@@ -203,35 +260,57 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
203
260
  # -----------------------
204
261
  # Expand include/exclude paths
205
262
  # -----------------------
263
+ # Convert directory paths to individual file paths for precise control
264
+ logger.debug("Expanding included paths to individual files")
206
265
  all_files = __expand_paths(tmp_dir, include_paths)
266
+ logger.info(f"Found {len(all_files)} file(s) in included paths")
207
267
 
268
+ # Create a set of excluded files for fast lookup
269
+ logger.debug("Expanding excluded paths to individual files")
208
270
  excluded_files = {f.resolve() for f in __expand_paths(tmp_dir, excluded_paths)}
271
+ if excluded_files:
272
+ logger.info(f"Excluding {len(excluded_files)} file(s) based on exclude patterns")
209
273
 
274
+ # Filter out excluded files from the list of files to copy
210
275
  files_to_copy = [f for f in all_files if f.resolve() not in excluded_files]
276
+ logger.info(f"Will materialize {len(files_to_copy)} file(s) to target repository")
211
277
 
212
278
  # -----------------------
213
279
  # Copy files into target repo
214
280
  # -----------------------
281
+ # Copy each file from the temporary clone to the target repository
282
+ # Preserve file metadata (timestamps, permissions) with copy2
283
+ logger.info("Copying files to target repository...")
215
284
  for src_file in files_to_copy:
285
+ # Calculate destination path maintaining relative structure
216
286
  dst_file = target / src_file.relative_to(tmp_dir)
217
287
  relative_path = dst_file.relative_to(target)
218
288
 
289
+ # Track this file for .rhiza.history
219
290
  materialized_files.append(relative_path)
220
291
 
292
+ # Check if file already exists and handle based on force flag
221
293
  if dst_file.exists() and not force:
222
294
  logger.warning(f"{relative_path} already exists — use --force to overwrite")
223
295
  continue
224
296
 
297
+ # Create parent directories if they don't exist
225
298
  dst_file.parent.mkdir(parents=True, exist_ok=True)
299
+
300
+ # Copy file with metadata preservation
226
301
  shutil.copy2(src_file, dst_file)
227
302
  logger.success(f"[ADD] {relative_path}")
228
303
 
229
304
  finally:
305
+ # Clean up the temporary directory
306
+ logger.debug(f"Cleaning up temporary directory: {tmp_dir}")
230
307
  shutil.rmtree(tmp_dir)
231
308
 
232
309
  # -----------------------
233
310
  # Warn about workflow files
234
311
  # -----------------------
312
+ # GitHub Actions workflow files require special permissions to modify
313
+ # Check if any of the materialized files are workflow files
235
314
  workflow_files = [p for p in materialized_files if p.parts[:2] == (".github", "workflows")]
236
315
 
237
316
  if workflow_files:
@@ -239,10 +318,14 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
239
318
  "Workflow files were materialized. Updating these files requires "
240
319
  "a token with the 'workflow' permission in GitHub Actions."
241
320
  )
321
+ logger.info(f"Workflow files affected: {len(workflow_files)}")
242
322
 
243
323
  # -----------------------
244
324
  # Write .rhiza.history
245
325
  # -----------------------
326
+ # This file tracks which files were materialized by Rhiza
327
+ # Useful for understanding which files came from the template
328
+ logger.debug("Writing .rhiza.history file")
246
329
  history_file = target / ".rhiza.history"
247
330
  with history_file.open("w", encoding="utf-8") as f:
248
331
  f.write("# Rhiza Template History\n")
@@ -251,10 +334,11 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
251
334
  f.write(f"# Template branch: {rhiza_branch}\n")
252
335
  f.write("#\n")
253
336
  f.write("# Files under template control:\n")
337
+ # Sort files for consistent ordering
254
338
  for file_path in sorted(materialized_files):
255
339
  f.write(f"{file_path}\n")
256
340
 
257
- logger.info(f"Created {history_file.relative_to(target)} with {len(materialized_files)} files")
341
+ logger.info(f"Created {history_file.relative_to(target)} with {len(materialized_files)} file(s)")
258
342
 
259
343
  logger.success("Rhiza templates materialized successfully")
260
344
 
@@ -1,6 +1,6 @@
1
1
  """Command for validating Rhiza template configuration.
2
2
 
3
- This module provides functionality to validate .github/template.yml files
3
+ This module provides functionality to validate .github/rhiza/template.yml files
4
4
  to ensure they are syntactically correct and semantically valid.
5
5
  """
6
6
 
@@ -25,40 +25,65 @@ def validate(target: Path) -> bool:
25
25
  Returns:
26
26
  True if validation passes, False otherwise.
27
27
  """
28
- # Convert to absolute path
28
+ # Convert to absolute path to avoid path resolution issues
29
29
  target = target.resolve()
30
30
 
31
- # Check if target is a git repository
31
+ # Check if target is a git repository by looking for .git directory
32
+ # Rhiza only works with git repositories
32
33
  if not (target / ".git").is_dir():
33
34
  logger.error(f"Target directory is not a git repository: {target}")
35
+ logger.error("Initialize a git repository with 'git init' first")
34
36
  return False
35
37
 
36
38
  logger.info(f"Validating template configuration in: {target}")
37
39
 
38
- # Check if template.yml exists
39
- template_file = target / ".github" / "template.yml"
40
- if not template_file.exists():
41
- logger.error(f"Template file not found: {template_file}")
40
+ # Check for template.yml in both new and old locations
41
+ # New location: .github/rhiza/template.yml
42
+ # Old location: .github/template.yml (deprecated but still supported)
43
+ new_location = target / ".github" / "rhiza" / "template.yml"
44
+ deprecated_location = target / ".github" / "template.yml"
45
+
46
+ # Check which file(s) exist
47
+ new_exists = new_location.exists()
48
+ deprecated_exists = deprecated_location.exists()
49
+
50
+ if not (new_exists or deprecated_exists):
51
+ logger.error(f"No template file found at: {new_location}")
52
+ logger.error(f"Also checked deprecated location: {deprecated_location}")
42
53
  logger.info("Run 'rhiza init' to create a default template.yml")
43
54
  return False
44
55
 
45
- logger.success(f"Found template file: {template_file}")
56
+ # Prefer the new location but support the old one with a warning
57
+ if new_exists:
58
+ logger.success(f"Template file exists: {new_location}")
59
+ template_file = new_location
60
+ else:
61
+ logger.warning(f"Template file exists but in old location: {deprecated_location}")
62
+ logger.warning("Consider moving it to .github/rhiza/template.yml")
63
+ template_file = deprecated_location
46
64
 
47
- # Validate YAML syntax
65
+ # Validate YAML syntax by attempting to parse the file
66
+ logger.debug(f"Parsing YAML file: {template_file}")
48
67
  try:
49
68
  with open(template_file) as f:
50
69
  config = yaml.safe_load(f)
51
70
  except yaml.YAMLError as e:
52
71
  logger.error(f"Invalid YAML syntax in template.yml: {e}")
72
+ logger.error("Fix the YAML syntax errors and try again")
53
73
  return False
54
74
 
75
+ # Check if the file is completely empty
55
76
  if config is None:
56
77
  logger.error("template.yml is empty")
78
+ logger.error("Add configuration to template.yml or run 'rhiza init' to generate defaults")
57
79
  return False
58
80
 
59
81
  logger.success("YAML syntax is valid")
60
82
 
61
- # Validate required fields
83
+ # Validate required fields exist and have correct types
84
+ # template-repository: Must be a string in 'owner/repo' format
85
+ # include: Must be a non-empty list of paths
86
+ logger.debug("Validating required fields")
62
87
  required_fields = {
63
88
  "template-repository": str,
64
89
  "include": list,
@@ -66,81 +91,103 @@ def validate(target: Path) -> bool:
66
91
 
67
92
  validation_passed = True
68
93
 
94
+ # Check each required field
69
95
  for field, expected_type in required_fields.items():
70
96
  if field not in config:
71
97
  logger.error(f"Missing required field: {field}")
98
+ logger.error(f"Add '{field}' to your template.yml")
72
99
  validation_passed = False
73
100
  elif not isinstance(config[field], expected_type):
74
101
  logger.error(
75
102
  f"Field '{field}' must be of type {expected_type.__name__}, got {type(config[field]).__name__}"
76
103
  )
104
+ logger.error(f"Fix the type of '{field}' in template.yml")
77
105
  validation_passed = False
78
106
  else:
79
107
  logger.success(f"Field '{field}' is present and valid")
80
108
 
81
109
  # Validate template-repository format
110
+ # Must be in 'owner/repo' format (e.g., 'jebel-quant/rhiza')
111
+ logger.debug("Validating template-repository format")
82
112
  if "template-repository" in config:
83
113
  repo = config["template-repository"]
84
114
  if not isinstance(repo, str):
85
115
  logger.error(f"template-repository must be a string, got {type(repo).__name__}")
116
+ logger.error("Example: 'owner/repository'")
86
117
  validation_passed = False
87
118
  elif "/" not in repo:
88
119
  logger.error(f"template-repository must be in format 'owner/repo', got: {repo}")
120
+ logger.error("Example: 'jebel-quant/rhiza'")
89
121
  validation_passed = False
90
122
  else:
91
123
  logger.success(f"template-repository format is valid: {repo}")
92
124
 
93
125
  # Validate include paths
126
+ # Must be a non-empty list of strings
127
+ logger.debug("Validating include paths")
94
128
  if "include" in config:
95
129
  include = config["include"]
96
130
  if not isinstance(include, list):
97
131
  logger.error(f"include must be a list, got {type(include).__name__}")
132
+ logger.error("Example: include: ['.github', '.gitignore']")
98
133
  validation_passed = False
99
134
  elif len(include) == 0:
100
135
  logger.error("include list cannot be empty")
136
+ logger.error("Add at least one path to materialize")
101
137
  validation_passed = False
102
138
  else:
103
139
  logger.success(f"include list has {len(include)} path(s)")
140
+ # Log each included path for transparency
104
141
  for path in include:
105
142
  if not isinstance(path, str):
106
143
  logger.warning(f"include path should be a string, got {type(path).__name__}: {path}")
107
144
  else:
108
145
  logger.info(f" - {path}")
109
146
 
110
- # Validate optional fields
147
+ # Validate optional fields if present
148
+ # template-branch: Branch name in the template repository
149
+ logger.debug("Validating optional fields")
111
150
  if "template-branch" in config:
112
151
  branch = config["template-branch"]
113
152
  if not isinstance(branch, str):
114
153
  logger.warning(f"template-branch should be a string, got {type(branch).__name__}: {branch}")
154
+ logger.warning("Example: 'main' or 'develop'")
115
155
  else:
116
156
  logger.success(f"template-branch is valid: {branch}")
117
157
 
158
+ # template-host: Git hosting platform (github or gitlab)
118
159
  if "template-host" in config:
119
160
  host = config["template-host"]
120
161
  if not isinstance(host, str):
121
162
  logger.warning(f"template-host should be a string, got {type(host).__name__}: {host}")
163
+ logger.warning("Must be 'github' or 'gitlab'")
122
164
  elif host not in ("github", "gitlab"):
123
165
  logger.warning(f"template-host should be 'github' or 'gitlab', got: {host}")
166
+ logger.warning("Other hosts are not currently supported")
124
167
  else:
125
168
  logger.success(f"template-host is valid: {host}")
126
169
 
170
+ # exclude: Optional list of paths to exclude from materialization
127
171
  if "exclude" in config:
128
172
  exclude = config["exclude"]
129
173
  if not isinstance(exclude, list):
130
174
  logger.warning(f"exclude should be a list, got {type(exclude).__name__}")
175
+ logger.warning("Example: exclude: ['.github/workflows/ci.yml']")
131
176
  else:
132
177
  logger.success(f"exclude list has {len(exclude)} path(s)")
178
+ # Log each excluded path for transparency
133
179
  for path in exclude:
134
180
  if not isinstance(path, str):
135
181
  logger.warning(f"exclude path should be a string, got {type(path).__name__}: {path}")
136
182
  else:
137
183
  logger.info(f" - {path}")
138
184
 
139
- # Final verdict
185
+ # Final verdict on validation
186
+ logger.debug("Validation complete, determining final result")
140
187
  if validation_passed:
141
188
  logger.success("✓ Validation passed: template.yml is valid")
142
189
  return True
143
190
  else:
144
191
  logger.error("✗ Validation failed: template.yml has errors")
145
- # raise AssertionError("Invalid template.yml")
192
+ logger.error("Fix the errors above and run 'rhiza validate' again")
146
193
  return False
rhiza/commands/welcome.py CHANGED
@@ -15,11 +15,17 @@ def welcome():
15
15
 
16
16
  Shows a friendly greeting, explains Rhiza's purpose, and provides
17
17
  next steps for getting started with the tool.
18
+
19
+ This command is useful for new users to understand what Rhiza does
20
+ and how to get started. It provides a high-level overview without
21
+ performing any operations on the file system.
18
22
  """
23
+ # Construct a nicely formatted welcome message with ASCII art border
24
+ # The version is dynamically inserted from the package metadata
19
25
  welcome_message = f"""
20
26
  ╭───────────────────────────────────────────────────────────────╮
21
27
  │ │
22
- │ 🌿 Welcome to Rhiza v{__version__:<43} │
28
+ │ 🌿 Welcome to Rhiza v{__version__:<39} │
23
29
  │ │
24
30
  ╰───────────────────────────────────────────────────────────────╯
25
31
 
@@ -38,7 +44,7 @@ Python projects using reusable templates stored in a central repository.
38
44
  1. Initialize a project:
39
45
  $ rhiza init
40
46
 
41
- 2. Customize .github/template.yml to match your needs
47
+ 2. Customize .github/rhiza/template.yml to match your needs
42
48
 
43
49
  3. Materialize templates into your project:
44
50
  $ rhiza materialize
@@ -52,4 +58,5 @@ Python projects using reusable templates stored in a central repository.
52
58
  Happy templating! 🎉
53
59
  """
54
60
 
61
+ # Print the welcome message to stdout
55
62
  print(welcome_message)
rhiza/models.py CHANGED
@@ -13,7 +13,7 @@ import yaml
13
13
 
14
14
  @dataclass
15
15
  class RhizaTemplate:
16
- """Represents the structure of .github/template.yml.
16
+ """Represents the structure of .github/rhiza/template.yml.
17
17
 
18
18
  Attributes:
19
19
  template_repository: The GitHub or GitLab repository containing templates (e.g., "jebel-quant/rhiza").
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhiza
3
- Version: 0.5.6
3
+ Version: 0.6.1
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
@@ -41,6 +41,8 @@ Description-Content-Type: text/markdown
41
41
 
42
42
  Command-line interface for managing reusable configuration templates for modern Python projects.
43
43
 
44
+ **📖 New to Rhiza? Check out the [Getting Started Guide](GETTING_STARTED.md) for a beginner-friendly introduction!**
45
+
44
46
  ## Overview
45
47
 
46
48
  Rhiza is a CLI tool that helps you maintain consistent configuration across multiple Python projects by using templates stored in a central repository. It allows you to:
@@ -68,6 +70,7 @@ Rhiza is a CLI tool that helps you maintain consistent configuration across mult
68
70
 
69
71
  For more detailed information, see:
70
72
 
73
+ - **[Getting Started Guide](GETTING_STARTED.md)** - Beginner-friendly introduction and walkthrough
71
74
  - **[CLI Quick Reference](CLI.md)** - Command syntax and quick examples
72
75
  - **[Usage Guide](USAGE.md)** - Practical tutorials and workflows
73
76
  - **[Contributing Guidelines](CONTRIBUTING.md)** - How to contribute to the project
@@ -81,6 +84,35 @@ For more detailed information, see:
81
84
  pip install rhiza
82
85
  ```
83
86
 
87
+ To update to the latest version:
88
+
89
+ ```bash
90
+ pip install --upgrade rhiza
91
+ ```
92
+
93
+ ### Using uvx (run without installation)
94
+
95
+ [uvx](https://docs.astral.sh/uv/) is part of the `uv` package manager and allows you to run CLI tools directly without installing them:
96
+
97
+ ```bash
98
+ uvx rhiza --help
99
+ ```
100
+
101
+ With uvx, you don't need to install rhiza globally. Each time you run `uvx rhiza`, it will automatically use the latest version available on PyPI. To ensure you're using the latest version, simply run your command - uvx will fetch updates as needed:
102
+
103
+ ```bash
104
+ # Always uses the latest version
105
+ uvx rhiza init
106
+ uvx rhiza materialize
107
+ uvx rhiza validate
108
+ ```
109
+
110
+ If you want to use a specific version:
111
+
112
+ ```bash
113
+ uvx rhiza@0.5.6 --help
114
+ ```
115
+
84
116
  ### From source
85
117
 
86
118
  ```bash
@@ -112,11 +144,11 @@ rhiza --help
112
144
  rhiza init
113
145
  ```
114
146
 
115
- This creates a `.github/template.yml` file with default configuration.
147
+ This creates a `.github/rhiza/template.yml` file with default configuration.
116
148
 
117
149
  2. **Customize the template configuration:**
118
150
 
119
- Edit `.github/template.yml` to specify which files/directories to include from your template repository.
151
+ Edit `.github/rhiza/template.yml` to specify which files/directories to include from your template repository.
120
152
 
121
153
  3. **Materialize templates into your project:**
122
154
 
@@ -132,13 +164,13 @@ rhiza --help
132
164
  rhiza validate
133
165
  ```
134
166
 
135
- This checks that your `.github/template.yml` is correctly formatted and valid.
167
+ This checks that your `.github/rhiza/template.yml` is correctly formatted and valid.
136
168
 
137
169
  ## Commands
138
170
 
139
171
  ### `rhiza init`
140
172
 
141
- Initialize or validate `.github/template.yml` in a target directory.
173
+ Initialize or validate `.github/rhiza/template.yml` in a target directory.
142
174
 
143
175
  **Usage:**
144
176
 
@@ -152,7 +184,7 @@ rhiza init [TARGET]
152
184
 
153
185
  **Description:**
154
186
 
155
- Creates a default `.github/template.yml` file if it doesn't exist, or validates an existing one. The default configuration includes common Python project files like `.github`, `.editorconfig`, `.gitignore`, `.pre-commit-config.yaml`, `Makefile`, and `pytest.ini`.
187
+ Creates a default `.github/rhiza/template.yml` file if it doesn't exist, or validates an existing one. The default configuration includes common Python project files like `.github`, `.editorconfig`, `.gitignore`, `.pre-commit-config.yaml`, `Makefile`, and `pytest.ini`.
156
188
 
157
189
  **Examples:**
158
190
 
@@ -172,18 +204,18 @@ rhiza init ..
172
204
  When creating a new template file:
173
205
  ```
174
206
  [INFO] Initializing Rhiza configuration in: /path/to/project
175
- [INFO] Creating default .github/template.yml
176
- ✓ Created .github/template.yml
207
+ [INFO] Creating default .github/rhiza/template.yml
208
+ ✓ Created .github/rhiza/template.yml
177
209
 
178
210
  Next steps:
179
- 1. Review and customize .github/template.yml to match your project needs
211
+ 1. Review and customize .github/rhiza/template.yml to match your project needs
180
212
  2. Run 'rhiza materialize' to inject templates into your repository
181
213
  ```
182
214
 
183
215
  When validating an existing file:
184
216
  ```
185
217
  [INFO] Validating template configuration in: /path/to/project
186
- ✓ Found template file: /path/to/project/.github/template.yml
218
+ ✓ Found template file: /path/to/project/.github/rhiza/template.yml
187
219
  ✓ YAML syntax is valid
188
220
  ✓ Field 'template-repository' is present and valid
189
221
  ✓ Field 'include' is present and valid
@@ -218,7 +250,7 @@ rhiza materialize [OPTIONS] [TARGET]
218
250
 
219
251
  Materializes template files from the configured template repository into your target project. This command:
220
252
 
221
- 1. Reads the `.github/template.yml` configuration
253
+ 1. Reads the `.github/rhiza/template.yml` configuration
222
254
  2. Performs a sparse clone of the template repository
223
255
  3. Copies specified files/directories to your project
224
256
  4. Respects exclusion patterns defined in the configuration
@@ -300,7 +332,7 @@ rhiza validate [TARGET]
300
332
 
301
333
  **Description:**
302
334
 
303
- Validates the `.github/template.yml` file to ensure it is syntactically correct and semantically valid. This performs authoritative validation including:
335
+ Validates the `.github/rhiza/template.yml` file to ensure it is syntactically correct and semantically valid. This performs authoritative validation including:
304
336
 
305
337
  - Checking if the file exists
306
338
  - Validating YAML syntax
@@ -331,7 +363,7 @@ rhiza validate ..
331
363
 
332
364
  ```
333
365
  [INFO] Validating template configuration in: /path/to/project
334
- ✓ Found template file: /path/to/project/.github/template.yml
366
+ ✓ Found template file: /path/to/project/.github/rhiza/template.yml
335
367
  ✓ YAML syntax is valid
336
368
  ✓ Field 'template-repository' is present and valid
337
369
  ✓ Field 'include' is present and valid
@@ -355,7 +387,7 @@ rhiza validate ..
355
387
  or
356
388
 
357
389
  ```
358
- [ERROR] Template file not found: /path/to/project/.github/template.yml
390
+ [ERROR] Template file not found: /path/to/project/.github/rhiza/template.yml
359
391
  [INFO] Run 'rhiza materialize' or 'rhiza init' to create a default template.yml
360
392
  ```
361
393
 
@@ -363,7 +395,7 @@ or
363
395
 
364
396
  ## Configuration
365
397
 
366
- Rhiza uses a `.github/template.yml` file to define template sources and what to include in your project.
398
+ Rhiza uses a `.github/rhiza/template.yml` file to define template sources and what to include in your project.
367
399
 
368
400
  ### Configuration File Format
369
401
 
@@ -504,7 +536,7 @@ git init
504
536
  rhiza init
505
537
 
506
538
  # Review the generated template.yml
507
- cat .github/template.yml
539
+ cat .github/rhiza/template.yml
508
540
 
509
541
  # Materialize templates
510
542
  rhiza materialize
@@ -539,7 +571,7 @@ git commit -m "chore: update rhiza templates"
539
571
 
540
572
  ### Example 3: Using a custom template repository
541
573
 
542
- Edit `.github/template.yml`:
574
+ Edit `.github/rhiza/template.yml`:
543
575
 
544
576
  ```yaml
545
577
  template-repository: myorg/my-templates
@@ -561,7 +593,7 @@ rhiza materialize --force
561
593
 
562
594
  ### Example 4: Using a GitLab template repository
563
595
 
564
- Edit `.github/template.yml`:
596
+ Edit `.github/rhiza/template.yml`:
565
597
 
566
598
  ```yaml
567
599
  template-repository: mygroup/python-templates
@@ -701,7 +733,7 @@ Releasing and Versioning
701
733
  post-release perform post-release tasks
702
734
 
703
735
  Meta
704
- sync sync with template repository as defined in .github/template.yml
736
+ sync sync with template repository as defined in .github/rhiza/template.yml
705
737
  help Display this help message
706
738
  customisations list available customisation scripts
707
739
  update-readme update README.md with current Makefile help output
@@ -773,7 +805,7 @@ export PATH="$HOME/.local/bin:$PATH"
773
805
  ### Template validation fails
774
806
 
775
807
  Check that:
776
- 1. Your `.github/template.yml` file exists
808
+ 1. Your `.github/rhiza/template.yml` file exists
777
809
  2. The YAML syntax is valid
778
810
  3. Required fields (`template-repository` and `include`) are present
779
811
  4. The repository format is `owner/repo`
@@ -805,11 +837,11 @@ A: Yes, as long as you have Git credentials configured that allow access to the
805
837
 
806
838
  **Q: Does Rhiza support template repositories hosted outside GitHub?**
807
839
 
808
- A: Yes! Rhiza supports both GitHub and GitLab repositories. Use the `template-host` field in your `.github/template.yml` to specify "github" (default) or "gitlab".
840
+ A: Yes! Rhiza supports both GitHub and GitLab repositories. Use the `template-host` field in your `.github/rhiza/template.yml` to specify "github" (default) or "gitlab".
809
841
 
810
842
  **Q: How do I use a GitLab repository as a template source?**
811
843
 
812
- A: Add `template-host: gitlab` to your `.github/template.yml` file. For example:
844
+ A: Add `template-host: gitlab` to your `.github/rhiza/template.yml` file. For example:
813
845
  ```yaml
814
846
  template-repository: mygroup/myproject
815
847
  template-host: gitlab
@@ -824,15 +856,23 @@ A: Not directly. However, you can run `rhiza materialize` multiple times with di
824
856
 
825
857
  **Q: What's the difference between `rhiza init` and `rhiza materialize`?**
826
858
 
827
- A: `init` creates or validates the `.github/template.yml` configuration file. `materialize` reads that configuration and actually copies the template files into your project.
859
+ A: `init` creates or validates the `.github/rhiza/template.yml` configuration file. `materialize` reads that configuration and actually copies the template files into your project.
828
860
 
829
861
  **Q: How do I update my project's templates?**
830
862
 
831
863
  A: Simply run `rhiza materialize --force` to fetch and overwrite with the latest versions from your template repository.
832
864
 
865
+ **Q: How do I update rhiza-cli itself?**
866
+
867
+ A: The update method depends on how you installed rhiza:
868
+
869
+ - **Using pip**: Run `pip install --upgrade rhiza`
870
+ - **Using uvx**: No action needed! `uvx` automatically uses the latest version each time you run it. Just run your command: `uvx rhiza <command>`
871
+ - **From source**: Run `git pull` in the repository directory and then `pip install -e .` again
872
+
833
873
  **Q: Can I customize which files are included?**
834
874
 
835
- A: Yes, edit the `include` and `exclude` lists in `.github/template.yml` to control exactly which files are copied.
875
+ A: Yes, edit the `include` and `exclude` lists in `.github/rhiza/template.yml` to control exactly which files are copied.
836
876
 
837
877
  ## Acknowledgments
838
878
 
@@ -0,0 +1,14 @@
1
+ rhiza/__init__.py,sha256=iW3niLBjwRKxcMhIV_1eb78putjUTo2tbZsadofluJk,1939
2
+ rhiza/__main__.py,sha256=Lx0GqVZo6ymm0f18_uYB6E7_SOWwJNYjb73Vr31oLoM,236
3
+ rhiza/cli.py,sha256=faCIOKDzEDRvL4doLZhiIAyHUUGESGrwWLtjLjimCUY,5111
4
+ rhiza/models.py,sha256=fW9lofkkid-bghk2bXEgBdGbZ4scSqG726fMrVfKX_M,3454
5
+ rhiza/commands/__init__.py,sha256=Z5CeMh7ylX27H6dvwqRbEKzYo5pwQq-5TyTxABUSaQg,1848
6
+ rhiza/commands/init.py,sha256=Hrox_o8hnyWMkx4SuE0rd4jqGIBEl_V_wh7BAiW9IFU,5726
7
+ rhiza/commands/materialize.py,sha256=Sxuvh9GLDRkcl6LVTjvWo5bkYzQ5GLwlhOwLo2jNptI,14387
8
+ rhiza/commands/validate.py,sha256=cxStfXbY_ifsc_yRDCg0TOnv8jG05hxE9rteta-X9hQ,8093
9
+ rhiza/commands/welcome.py,sha256=w3BziR042o6oYincd3EqDsFzF6qqInU7iYhWjF3yJqY,2382
10
+ rhiza-0.6.1.dist-info/METADATA,sha256=tx98rTT3r_muUBFnlZ_L8KxDbzhQqC_sAocefsoGLf8,22742
11
+ rhiza-0.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ rhiza-0.6.1.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
13
+ rhiza-0.6.1.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
14
+ rhiza-0.6.1.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- rhiza/__init__.py,sha256=1jfkGAONm7dH4KwYjvNEyuxrQ-1m2YncxREYCJnTrHA,1933
2
- rhiza/__main__.py,sha256=Lx0GqVZo6ymm0f18_uYB6E7_SOWwJNYjb73Vr31oLoM,236
3
- rhiza/cli.py,sha256=Jqe285OWOvbvembO5DXTVITfnFefsjVGZCJuK3EKRT8,5081
4
- rhiza/models.py,sha256=R2nu_bf-j-U0kPfQXg6u-MSykrdGO9ixOzZoWy8mLCc,3448
5
- rhiza/commands/__init__.py,sha256=lIkN15MIat-wn9CB1cgUjTzTUQB95LBBAKFK1sGHdCc,1836
6
- rhiza/commands/init.py,sha256=QsOV_VBnRfSPebydH-fMe3haadboNIAYlOpAIYHtgUs,1936
7
- rhiza/commands/materialize.py,sha256=FUStZGUr2oDEC-M8V61JnV3W7USj-VS0EHI10a0Mn6Y,9378
8
- rhiza/commands/validate.py,sha256=_0t9kfylMncm9JmKULn5e7V71XcQdFjlrtuOqZeshPM,5282
9
- rhiza/commands/welcome.py,sha256=GpDbRSIUigaxS7Di9RIpl2jCOFOlhlQT2vNvCzBR-8U,2001
10
- rhiza-0.5.6.dist-info/METADATA,sha256=PI_8F886fva98V4uq_toyZ9O8gC8tnOPy-FPSUn0eE4,21278
11
- rhiza-0.5.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- rhiza-0.5.6.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
13
- rhiza-0.5.6.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
14
- rhiza-0.5.6.dist-info/RECORD,,
File without changes