rhiza 0.6.0__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/commands/init.py CHANGED
@@ -22,44 +22,60 @@ def init(target: Path):
22
22
 
23
23
  Args:
24
24
  target: Path to the target directory. Defaults to the current working directory.
25
+
26
+ Returns:
27
+ bool: True if validation passes, False otherwise.
25
28
  """
26
29
  # Convert to absolute path to avoid surprises
27
30
  target = target.resolve()
28
31
 
29
32
  logger.info(f"Initializing Rhiza configuration in: {target}")
30
33
 
31
- # Create .github/rhiza 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
32
36
  github_dir = target / ".github"
33
37
  rhiza_dir = github_dir / "rhiza"
38
+ logger.debug(f"Ensuring directory exists: {rhiza_dir}")
34
39
  rhiza_dir.mkdir(parents=True, exist_ok=True)
35
40
 
36
- # check the old location and copy over if existent
37
- # todo: remove this logic later
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
38
44
  template_file = github_dir / "template.yml"
39
45
  if template_file.exists():
40
- # move the file into rhiza_dir
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)
41
49
  shutil.copyfile(template_file, rhiza_dir / "template.yml")
42
50
 
43
- # Define the template file path
51
+ # Define the template file path (new location)
44
52
  template_file = rhiza_dir / "template.yml"
45
53
 
46
54
  if not template_file.exists():
47
- # Create default template.yml
55
+ # Create default template.yml with sensible defaults
48
56
  logger.info("Creating default .github/rhiza/template.yml")
57
+ logger.debug("Using default template configuration")
49
58
 
59
+ # Default template points to the jebel-quant/rhiza repository
60
+ # and includes common Python project configuration files
50
61
  default_template = RhizaTemplate(
51
62
  template_repository="jebel-quant/rhiza",
52
63
  template_branch="main",
53
64
  include=[
54
- ".github",
55
- ".editorconfig",
56
- ".gitignore",
57
- ".pre-commit-config.yaml",
58
- "Makefile",
59
- "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
60
74
  ],
61
75
  )
62
76
 
77
+ # Write the default template to the file
78
+ logger.debug(f"Writing default template to: {template_file}")
63
79
  default_template.to_yaml(template_file)
64
80
 
65
81
  logger.success("✓ Created .github/rhiza/template.yml")
@@ -69,5 +85,70 @@ Next steps:
69
85
  2. Run 'rhiza materialize' to inject templates into your repository
70
86
  """)
71
87
 
72
- # 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")
73
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,53 +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
- # init will make sure the template_file exists at the new location!
107
-
108
125
  if not valid:
109
- 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")
110
128
  sys.exit(1)
111
129
 
130
+ # Load the template configuration from the validated file
112
131
  template_file = target / ".github" / "rhiza" / "template.yml"
132
+ logger.debug(f"Loading template configuration from: {template_file}")
113
133
  template = RhizaTemplate.from_yaml(template_file)
114
134
 
115
- # init will make sure the template_file exists at the new location!
116
-
135
+ # Extract template configuration settings
136
+ # These define where to clone from and what to materialize
117
137
  rhiza_repo = template.template_repository
138
+ # Use CLI arg if template doesn't specify a branch
118
139
  rhiza_branch = template.template_branch or branch
140
+ # Default to GitHub if not specified
119
141
  rhiza_host = template.template_host or "github"
120
142
  include_paths = template.include
121
143
  excluded_paths = template.exclude
122
144
 
145
+ # Validate that we have paths to include
123
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")
124
149
  raise RuntimeError("No include paths found in template.yml")
125
150
 
151
+ # Log the paths we'll be including for transparency
126
152
  logger.info("Include paths:")
127
153
  for p in include_paths:
128
154
  logger.info(f" - {p}")
129
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
+
130
162
  # -----------------------
131
163
  # Construct git clone URL based on host
132
164
  # -----------------------
165
+ # Support both GitHub and GitLab template repositories
133
166
  if rhiza_host == "gitlab":
134
167
  git_url = f"https://gitlab.com/{rhiza_repo}.git"
168
+ logger.debug(f"Using GitLab repository: {git_url}")
135
169
  elif rhiza_host == "github":
136
170
  git_url = f"https://github.com/{rhiza_repo}.git"
171
+ logger.debug(f"Using GitHub repository: {git_url}")
137
172
  else:
173
+ logger.error(f"Unsupported template-host: {rhiza_host}")
174
+ logger.error("template-host must be 'github' or 'gitlab'")
138
175
  raise ValueError(f"Unsupported template-host: {rhiza_host}. Must be 'github' or 'gitlab'.")
139
176
 
140
177
  # -----------------------
141
178
  # Sparse clone template repo
142
179
  # -----------------------
180
+ # Create a temporary directory for the sparse clone
181
+ # This will be cleaned up in the finally block
143
182
  tmp_dir = Path(tempfile.mkdtemp())
144
183
  materialized_files: list[Path] = []
145
184
 
146
185
  logger.info(f"Cloning {rhiza_repo}@{rhiza_branch} from {rhiza_host} into temporary directory")
186
+ logger.debug(f"Temporary directory: {tmp_dir}")
147
187
 
148
188
  try:
149
- # 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
150
194
  try:
195
+ logger.debug("Executing git clone with sparse checkout")
151
196
  subprocess.run(
152
197
  [
153
198
  "git",
@@ -166,14 +211,18 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
166
211
  text=True,
167
212
  env=git_env,
168
213
  )
214
+ logger.debug("Git clone completed successfully")
169
215
  except subprocess.CalledProcessError as e:
170
216
  logger.error(f"Failed to clone repository: {e}")
171
217
  if e.stderr:
172
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")
173
220
  raise
174
221
 
175
- # Initialize sparse checkout
222
+ # Initialize sparse checkout in cone mode
223
+ # Cone mode is more efficient and uses pattern matching
176
224
  try:
225
+ logger.debug("Initializing sparse checkout")
177
226
  subprocess.run(
178
227
  ["git", "sparse-checkout", "init", "--cone"],
179
228
  cwd=tmp_dir,
@@ -182,14 +231,17 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
182
231
  text=True,
183
232
  env=git_env,
184
233
  )
234
+ logger.debug("Sparse checkout initialized")
185
235
  except subprocess.CalledProcessError as e:
186
236
  logger.error(f"Failed to initialize sparse checkout: {e}")
187
237
  if e.stderr:
188
238
  logger.error(f"Git error: {e.stderr.strip()}")
189
239
  raise
190
240
 
191
- # 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
192
243
  try:
244
+ logger.debug(f"Setting sparse checkout paths: {include_paths}")
193
245
  subprocess.run(
194
246
  ["git", "sparse-checkout", "set", "--skip-checks", *include_paths],
195
247
  cwd=tmp_dir,
@@ -198,6 +250,7 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
198
250
  text=True,
199
251
  env=git_env,
200
252
  )
253
+ logger.debug("Sparse checkout paths configured")
201
254
  except subprocess.CalledProcessError as e:
202
255
  logger.error(f"Failed to set sparse checkout paths: {e}")
203
256
  if e.stderr:
@@ -207,35 +260,57 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
207
260
  # -----------------------
208
261
  # Expand include/exclude paths
209
262
  # -----------------------
263
+ # Convert directory paths to individual file paths for precise control
264
+ logger.debug("Expanding included paths to individual files")
210
265
  all_files = __expand_paths(tmp_dir, include_paths)
266
+ logger.info(f"Found {len(all_files)} file(s) in included paths")
211
267
 
268
+ # Create a set of excluded files for fast lookup
269
+ logger.debug("Expanding excluded paths to individual files")
212
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")
213
273
 
274
+ # Filter out excluded files from the list of files to copy
214
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")
215
277
 
216
278
  # -----------------------
217
279
  # Copy files into target repo
218
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...")
219
284
  for src_file in files_to_copy:
285
+ # Calculate destination path maintaining relative structure
220
286
  dst_file = target / src_file.relative_to(tmp_dir)
221
287
  relative_path = dst_file.relative_to(target)
222
288
 
289
+ # Track this file for .rhiza.history
223
290
  materialized_files.append(relative_path)
224
291
 
292
+ # Check if file already exists and handle based on force flag
225
293
  if dst_file.exists() and not force:
226
294
  logger.warning(f"{relative_path} already exists — use --force to overwrite")
227
295
  continue
228
296
 
297
+ # Create parent directories if they don't exist
229
298
  dst_file.parent.mkdir(parents=True, exist_ok=True)
299
+
300
+ # Copy file with metadata preservation
230
301
  shutil.copy2(src_file, dst_file)
231
302
  logger.success(f"[ADD] {relative_path}")
232
303
 
233
304
  finally:
305
+ # Clean up the temporary directory
306
+ logger.debug(f"Cleaning up temporary directory: {tmp_dir}")
234
307
  shutil.rmtree(tmp_dir)
235
308
 
236
309
  # -----------------------
237
310
  # Warn about workflow files
238
311
  # -----------------------
312
+ # GitHub Actions workflow files require special permissions to modify
313
+ # Check if any of the materialized files are workflow files
239
314
  workflow_files = [p for p in materialized_files if p.parts[:2] == (".github", "workflows")]
240
315
 
241
316
  if workflow_files:
@@ -243,10 +318,14 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
243
318
  "Workflow files were materialized. Updating these files requires "
244
319
  "a token with the 'workflow' permission in GitHub Actions."
245
320
  )
321
+ logger.info(f"Workflow files affected: {len(workflow_files)}")
246
322
 
247
323
  # -----------------------
248
324
  # Write .rhiza.history
249
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")
250
329
  history_file = target / ".rhiza.history"
251
330
  with history_file.open("w", encoding="utf-8") as f:
252
331
  f.write("# Rhiza Template History\n")
@@ -255,10 +334,11 @@ def materialize(target: Path, branch: str, target_branch: str | None, force: boo
255
334
  f.write(f"# Template branch: {rhiza_branch}\n")
256
335
  f.write("#\n")
257
336
  f.write("# Files under template control:\n")
337
+ # Sort files for consistent ordering
258
338
  for file_path in sorted(materialized_files):
259
339
  f.write(f"{file_path}\n")
260
340
 
261
- 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)")
262
342
 
263
343
  logger.success("Rhiza templates materialized successfully")
264
344
 
@@ -25,49 +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 one of the possible template.yml exists
39
- template_file = [target / ".github" / "rhiza" / "template.yml", target / ".github" / "template.yml"]
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"
40
45
 
41
- # Check the exists
42
- exists = [file.exists() for file in template_file]
46
+ # Check which file(s) exist
47
+ new_exists = new_location.exists()
48
+ deprecated_exists = deprecated_location.exists()
43
49
 
44
- if not any(exists):
45
- logger.error(f"No template file found: {template_file}")
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}")
46
53
  logger.info("Run 'rhiza init' to create a default template.yml")
47
54
  return False
48
55
 
49
- if exists[0]:
50
- logger.success(f"Template file exists: {template_file[0]}")
51
- template_file = template_file[0]
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
52
60
  else:
53
- logger.warning(f"Template file exists but in old location: {template_file[1]}")
54
- template_file = template_file[1]
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
55
64
 
56
- # Validate YAML syntax
65
+ # Validate YAML syntax by attempting to parse the file
66
+ logger.debug(f"Parsing YAML file: {template_file}")
57
67
  try:
58
68
  with open(template_file) as f:
59
69
  config = yaml.safe_load(f)
60
70
  except yaml.YAMLError as e:
61
71
  logger.error(f"Invalid YAML syntax in template.yml: {e}")
72
+ logger.error("Fix the YAML syntax errors and try again")
62
73
  return False
63
74
 
75
+ # Check if the file is completely empty
64
76
  if config is None:
65
77
  logger.error("template.yml is empty")
78
+ logger.error("Add configuration to template.yml or run 'rhiza init' to generate defaults")
66
79
  return False
67
80
 
68
81
  logger.success("YAML syntax is valid")
69
82
 
70
- # 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")
71
87
  required_fields = {
72
88
  "template-repository": str,
73
89
  "include": list,
@@ -75,81 +91,103 @@ def validate(target: Path) -> bool:
75
91
 
76
92
  validation_passed = True
77
93
 
94
+ # Check each required field
78
95
  for field, expected_type in required_fields.items():
79
96
  if field not in config:
80
97
  logger.error(f"Missing required field: {field}")
98
+ logger.error(f"Add '{field}' to your template.yml")
81
99
  validation_passed = False
82
100
  elif not isinstance(config[field], expected_type):
83
101
  logger.error(
84
102
  f"Field '{field}' must be of type {expected_type.__name__}, got {type(config[field]).__name__}"
85
103
  )
104
+ logger.error(f"Fix the type of '{field}' in template.yml")
86
105
  validation_passed = False
87
106
  else:
88
107
  logger.success(f"Field '{field}' is present and valid")
89
108
 
90
109
  # Validate template-repository format
110
+ # Must be in 'owner/repo' format (e.g., 'jebel-quant/rhiza')
111
+ logger.debug("Validating template-repository format")
91
112
  if "template-repository" in config:
92
113
  repo = config["template-repository"]
93
114
  if not isinstance(repo, str):
94
115
  logger.error(f"template-repository must be a string, got {type(repo).__name__}")
116
+ logger.error("Example: 'owner/repository'")
95
117
  validation_passed = False
96
118
  elif "/" not in repo:
97
119
  logger.error(f"template-repository must be in format 'owner/repo', got: {repo}")
120
+ logger.error("Example: 'jebel-quant/rhiza'")
98
121
  validation_passed = False
99
122
  else:
100
123
  logger.success(f"template-repository format is valid: {repo}")
101
124
 
102
125
  # Validate include paths
126
+ # Must be a non-empty list of strings
127
+ logger.debug("Validating include paths")
103
128
  if "include" in config:
104
129
  include = config["include"]
105
130
  if not isinstance(include, list):
106
131
  logger.error(f"include must be a list, got {type(include).__name__}")
132
+ logger.error("Example: include: ['.github', '.gitignore']")
107
133
  validation_passed = False
108
134
  elif len(include) == 0:
109
135
  logger.error("include list cannot be empty")
136
+ logger.error("Add at least one path to materialize")
110
137
  validation_passed = False
111
138
  else:
112
139
  logger.success(f"include list has {len(include)} path(s)")
140
+ # Log each included path for transparency
113
141
  for path in include:
114
142
  if not isinstance(path, str):
115
143
  logger.warning(f"include path should be a string, got {type(path).__name__}: {path}")
116
144
  else:
117
145
  logger.info(f" - {path}")
118
146
 
119
- # Validate optional fields
147
+ # Validate optional fields if present
148
+ # template-branch: Branch name in the template repository
149
+ logger.debug("Validating optional fields")
120
150
  if "template-branch" in config:
121
151
  branch = config["template-branch"]
122
152
  if not isinstance(branch, str):
123
153
  logger.warning(f"template-branch should be a string, got {type(branch).__name__}: {branch}")
154
+ logger.warning("Example: 'main' or 'develop'")
124
155
  else:
125
156
  logger.success(f"template-branch is valid: {branch}")
126
157
 
158
+ # template-host: Git hosting platform (github or gitlab)
127
159
  if "template-host" in config:
128
160
  host = config["template-host"]
129
161
  if not isinstance(host, str):
130
162
  logger.warning(f"template-host should be a string, got {type(host).__name__}: {host}")
163
+ logger.warning("Must be 'github' or 'gitlab'")
131
164
  elif host not in ("github", "gitlab"):
132
165
  logger.warning(f"template-host should be 'github' or 'gitlab', got: {host}")
166
+ logger.warning("Other hosts are not currently supported")
133
167
  else:
134
168
  logger.success(f"template-host is valid: {host}")
135
169
 
170
+ # exclude: Optional list of paths to exclude from materialization
136
171
  if "exclude" in config:
137
172
  exclude = config["exclude"]
138
173
  if not isinstance(exclude, list):
139
174
  logger.warning(f"exclude should be a list, got {type(exclude).__name__}")
175
+ logger.warning("Example: exclude: ['.github/workflows/ci.yml']")
140
176
  else:
141
177
  logger.success(f"exclude list has {len(exclude)} path(s)")
178
+ # Log each excluded path for transparency
142
179
  for path in exclude:
143
180
  if not isinstance(path, str):
144
181
  logger.warning(f"exclude path should be a string, got {type(path).__name__}: {path}")
145
182
  else:
146
183
  logger.info(f" - {path}")
147
184
 
148
- # Final verdict
185
+ # Final verdict on validation
186
+ logger.debug("Validation complete, determining final result")
149
187
  if validation_passed:
150
188
  logger.success("✓ Validation passed: template.yml is valid")
151
189
  return True
152
190
  else:
153
191
  logger.error("✗ Validation failed: template.yml has errors")
154
- # raise AssertionError("Invalid template.yml")
192
+ logger.error("Fix the errors above and run 'rhiza validate' again")
155
193
  return False
rhiza/commands/welcome.py CHANGED
@@ -15,7 +15,13 @@ 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
  │ │
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rhiza
3
- Version: 0.6.0
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
@@ -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=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=1JFvT-Y8eYcjqhM7xaaRCBmelKlQNmxaVgyCOeDvydw,2310
7
- rhiza/commands/materialize.py,sha256=aQcBp8VTBt5DPxyDC2VcgngqTdLDxqbYd5F_B2UF2_A,9534
8
- rhiza/commands/validate.py,sha256=oYCZnTxr39l20jr6DhBpFtMURv7jULSV8Ii6it8KHxs,5628
9
- rhiza/commands/welcome.py,sha256=gLgahbfBCBhmZPxWKywx7kYGBTMCVyyVVnly9uKdJv0,2007
10
- rhiza-0.6.0.dist-info/METADATA,sha256=GrFAnXe3MUq0evyRHFQDE4Yv3mUziu2LKz0wrYFN1fQ,22523
11
- rhiza-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- rhiza-0.6.0.dist-info/entry_points.txt,sha256=NAwZUpbXvfKv50a_Qq-PxMHl3lcjAyZO63IBeuUNgfY,45
13
- rhiza-0.6.0.dist-info/licenses/LICENSE,sha256=4m5X7LhqX-6D0Ks79Ys8CLpmza8cxDG34g4S9XSNAGY,1077
14
- rhiza-0.6.0.dist-info/RECORD,,
File without changes