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 +1 -1
- rhiza/cli.py +5 -5
- rhiza/commands/__init__.py +2 -2
- rhiza/commands/init.py +108 -18
- rhiza/commands/materialize.py +94 -10
- rhiza/commands/validate.py +60 -13
- rhiza/commands/welcome.py +9 -2
- rhiza/models.py +1 -1
- {rhiza-0.5.6.dist-info → rhiza-0.6.1.dist-info}/METADATA +64 -24
- rhiza-0.6.1.dist-info/RECORD +14 -0
- rhiza-0.5.6.dist-info/RECORD +0 -14
- {rhiza-0.5.6.dist-info → rhiza-0.6.1.dist-info}/WHEEL +0 -0
- {rhiza-0.5.6.dist-info → rhiza-0.6.1.dist-info}/entry_points.txt +0 -0
- {rhiza-0.5.6.dist-info → rhiza-0.6.1.dist-info}/licenses/LICENSE +0 -0
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
|
rhiza/commands/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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)
|
rhiza/commands/materialize.py
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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)}
|
|
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
|
|
rhiza/commands/validate.py
CHANGED
|
@@ -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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__:<
|
|
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.
|
|
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,,
|
rhiza-0.5.6.dist-info/RECORD
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|