rhiza 0.3.0__py3-none-any.whl → 0.5.0__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 +9 -0
- rhiza/__main__.py +10 -0
- rhiza/cli.py +115 -0
- rhiza/commands/__init__.py +5 -0
- rhiza/commands/init.py +66 -0
- rhiza/commands/materialize.py +140 -0
- rhiza/commands/validate.py +140 -0
- rhiza/models.py +103 -0
- rhiza-0.5.0.dist-info/METADATA +773 -0
- rhiza-0.5.0.dist-info/RECORD +13 -0
- rhiza-0.5.0.dist-info/entry_points.txt +2 -0
- rhiza/.gitkeep +0 -0
- rhiza-0.3.0.dist-info/METADATA +0 -709
- rhiza-0.3.0.dist-info/RECORD +0 -5
- {rhiza-0.3.0.dist-info → rhiza-0.5.0.dist-info}/WHEEL +0 -0
- {rhiza-0.3.0.dist-info → rhiza-0.5.0.dist-info}/licenses/LICENSE +0 -0
rhiza/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Utility tools and command-line helpers for the Rhiza project.
|
|
2
|
+
|
|
3
|
+
This package groups small, user-facing utilities that can be invoked from
|
|
4
|
+
the command line or other automation scripts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rhiza.models import RhizaTemplate
|
|
8
|
+
|
|
9
|
+
__all__ = ["RhizaTemplate"]
|
rhiza/__main__.py
ADDED
rhiza/cli.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Rhiza command-line interface (CLI).
|
|
2
|
+
|
|
3
|
+
This module defines the Typer application entry points exposed by Rhiza.
|
|
4
|
+
Commands are thin wrappers around implementations in `rhiza.commands.*`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from rhiza.commands.init import init as init_cmd
|
|
12
|
+
from rhiza.commands.materialize import materialize as materialize_cmd
|
|
13
|
+
from rhiza.commands.validate import validate as validate_cmd
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
help="Rhiza - Manage reusable configuration templates for Python projects",
|
|
17
|
+
add_completion=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def init(
|
|
23
|
+
target: Path = typer.Argument(
|
|
24
|
+
default=Path("."), # default to current directory
|
|
25
|
+
exists=True,
|
|
26
|
+
file_okay=False,
|
|
27
|
+
dir_okay=True,
|
|
28
|
+
help="Target directory (defaults to current directory)",
|
|
29
|
+
),
|
|
30
|
+
):
|
|
31
|
+
"""Initialize or validate .github/template.yml.
|
|
32
|
+
|
|
33
|
+
Creates a default .github/template.yml configuration file if one doesn't
|
|
34
|
+
exist, or validates the existing configuration.
|
|
35
|
+
|
|
36
|
+
The default template includes common Python project files:
|
|
37
|
+
- .github (workflows, actions, etc.)
|
|
38
|
+
- .editorconfig
|
|
39
|
+
- .gitignore
|
|
40
|
+
- .pre-commit-config.yaml
|
|
41
|
+
- Makefile
|
|
42
|
+
- pytest.ini
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
rhiza init
|
|
46
|
+
rhiza init /path/to/project
|
|
47
|
+
rhiza init ..
|
|
48
|
+
"""
|
|
49
|
+
init_cmd(target)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def materialize(
|
|
54
|
+
target: Path = typer.Argument(
|
|
55
|
+
default=Path("."), # default to current directory
|
|
56
|
+
exists=True,
|
|
57
|
+
file_okay=False,
|
|
58
|
+
dir_okay=True,
|
|
59
|
+
help="Target git repository (defaults to current directory)",
|
|
60
|
+
),
|
|
61
|
+
branch: str = typer.Option("main", "--branch", "-b", help="Rhiza branch to use"),
|
|
62
|
+
force: bool = typer.Option(False, "--force", "-y", help="Overwrite existing files"),
|
|
63
|
+
):
|
|
64
|
+
"""Inject Rhiza configuration templates into a target repository.
|
|
65
|
+
|
|
66
|
+
Materializes configuration files from the template repository specified
|
|
67
|
+
in .github/template.yml into your project. This command:
|
|
68
|
+
|
|
69
|
+
1. Reads .github/template.yml configuration
|
|
70
|
+
2. Performs a sparse clone of the template repository
|
|
71
|
+
3. Copies specified files/directories to your project
|
|
72
|
+
4. Respects exclusion patterns defined in the configuration
|
|
73
|
+
|
|
74
|
+
Files that already exist will NOT be overwritten unless --force is used.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
rhiza materialize
|
|
78
|
+
rhiza materialize --branch develop
|
|
79
|
+
rhiza materialize --force
|
|
80
|
+
rhiza materialize /path/to/project -b v2.0 -y
|
|
81
|
+
"""
|
|
82
|
+
materialize_cmd(target, branch, force)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command()
|
|
86
|
+
def validate(
|
|
87
|
+
target: Path = typer.Argument(
|
|
88
|
+
default=Path("."), # default to current directory
|
|
89
|
+
exists=True,
|
|
90
|
+
file_okay=False,
|
|
91
|
+
dir_okay=True,
|
|
92
|
+
help="Target git repository (defaults to current directory)",
|
|
93
|
+
),
|
|
94
|
+
):
|
|
95
|
+
"""Validate Rhiza template configuration.
|
|
96
|
+
|
|
97
|
+
Validates the .github/template.yml file to ensure it is syntactically
|
|
98
|
+
correct and semantically valid. Performs comprehensive validation:
|
|
99
|
+
|
|
100
|
+
- Checks if template.yml exists
|
|
101
|
+
- Validates YAML syntax
|
|
102
|
+
- Verifies required fields are present (template-repository, include)
|
|
103
|
+
- Validates field types and formats
|
|
104
|
+
- Ensures repository name follows owner/repo format
|
|
105
|
+
- Confirms include paths are not empty
|
|
106
|
+
|
|
107
|
+
Returns exit code 0 on success, 1 on validation failure.
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
rhiza validate
|
|
111
|
+
rhiza validate /path/to/project
|
|
112
|
+
rhiza validate ..
|
|
113
|
+
"""
|
|
114
|
+
if not validate_cmd(target):
|
|
115
|
+
raise typer.Exit(code=1)
|
rhiza/commands/init.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Command to initialize or validate .github/template.yml.
|
|
2
|
+
|
|
3
|
+
This module provides the init command that creates or validates the
|
|
4
|
+
.github/template.yml file, which defines where templates come from
|
|
5
|
+
and what paths are governed by Rhiza.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from rhiza.commands.validate import validate
|
|
13
|
+
from rhiza.models import RhizaTemplate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def init(target: Path):
|
|
17
|
+
"""Initialize or validate .github/template.yml in the target repository.
|
|
18
|
+
|
|
19
|
+
Creates a default .github/template.yml file if it doesn't exist,
|
|
20
|
+
or validates an existing one.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
target:
|
|
25
|
+
Path to the target directory. Defaults to the current working directory.
|
|
26
|
+
"""
|
|
27
|
+
# Convert to absolute path to avoid surprises
|
|
28
|
+
target = target.resolve()
|
|
29
|
+
|
|
30
|
+
logger.info(f"Initializing Rhiza configuration in: {target}")
|
|
31
|
+
|
|
32
|
+
# Create .github directory if it doesn't exist
|
|
33
|
+
github_dir = target / ".github"
|
|
34
|
+
github_dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
# Define the template file path
|
|
37
|
+
template_file = github_dir / "template.yml"
|
|
38
|
+
|
|
39
|
+
if not template_file.exists():
|
|
40
|
+
# Create default template.yml
|
|
41
|
+
logger.info("Creating default .github/template.yml")
|
|
42
|
+
|
|
43
|
+
default_template = RhizaTemplate(
|
|
44
|
+
template_repository="jebel-quant/rhiza",
|
|
45
|
+
template_branch="main",
|
|
46
|
+
include=[
|
|
47
|
+
".github",
|
|
48
|
+
".editorconfig",
|
|
49
|
+
".gitignore",
|
|
50
|
+
".pre-commit-config.yaml",
|
|
51
|
+
"Makefile",
|
|
52
|
+
"pytest.ini",
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
default_template.to_yaml(template_file)
|
|
57
|
+
|
|
58
|
+
logger.success("✓ Created .github/template.yml")
|
|
59
|
+
logger.info("""
|
|
60
|
+
Next steps:
|
|
61
|
+
1. Review and customize .github/template.yml to match your project needs
|
|
62
|
+
2. Run 'rhiza materialize' to inject templates into your repository
|
|
63
|
+
""")
|
|
64
|
+
|
|
65
|
+
# the template file exists, so validate it
|
|
66
|
+
validate(target)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Command-line helpers for working with Rhiza templates.
|
|
2
|
+
|
|
3
|
+
This module currently exposes a thin wrapper that shells out to the
|
|
4
|
+
`tools/inject_rhiza.sh` script. It exists so the functionality can be
|
|
5
|
+
invoked via a Python entry point while delegating the heavy lifting to
|
|
6
|
+
the maintained shell script.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from rhiza.commands.init import init
|
|
18
|
+
from rhiza.models import RhizaTemplate
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def expand_paths(base_dir: Path, paths: list[str]) -> list[Path]:
|
|
22
|
+
"""Expand files/directories relative to base_dir into a flat list of files.
|
|
23
|
+
|
|
24
|
+
Given a list of paths relative to ``base_dir``, return a flat list of all
|
|
25
|
+
individual files.
|
|
26
|
+
"""
|
|
27
|
+
all_files = []
|
|
28
|
+
for p in paths:
|
|
29
|
+
full_path = base_dir / p
|
|
30
|
+
if full_path.is_file():
|
|
31
|
+
all_files.append(full_path)
|
|
32
|
+
elif full_path.is_dir():
|
|
33
|
+
all_files.extend([f for f in full_path.rglob("*") if f.is_file()])
|
|
34
|
+
else:
|
|
35
|
+
# Path does not exist — could log a warning
|
|
36
|
+
continue
|
|
37
|
+
return all_files
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def materialize(target: Path, branch: str, force: bool):
|
|
41
|
+
"""Materialize rhiza templates into TARGET repository."""
|
|
42
|
+
# Convert to absolute path to avoid surprises
|
|
43
|
+
target = target.resolve()
|
|
44
|
+
|
|
45
|
+
logger.info(f"Target repository: {target}")
|
|
46
|
+
logger.info(f"Rhiza branch: {branch}")
|
|
47
|
+
|
|
48
|
+
# -----------------------
|
|
49
|
+
# Ensure template.yml
|
|
50
|
+
# -----------------------
|
|
51
|
+
template_file = target / ".github" / "template.yml"
|
|
52
|
+
# template_file.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# Initialize rhiza if not already initialized, e.g. construct a template.yml file
|
|
55
|
+
init(target)
|
|
56
|
+
|
|
57
|
+
# -----------------------
|
|
58
|
+
# Load template.yml
|
|
59
|
+
# -----------------------
|
|
60
|
+
template = RhizaTemplate.from_yaml(template_file)
|
|
61
|
+
|
|
62
|
+
rhiza_repo = template.template_repository
|
|
63
|
+
# Use template branch if specified, otherwise fall back to CLI parameter
|
|
64
|
+
rhiza_branch = template.template_branch if template.template_branch else branch
|
|
65
|
+
include_paths = template.include
|
|
66
|
+
excluded_paths = template.exclude
|
|
67
|
+
|
|
68
|
+
if not include_paths:
|
|
69
|
+
logger.error("No include paths found in template.yml")
|
|
70
|
+
raise sys.exit(1)
|
|
71
|
+
|
|
72
|
+
logger.info("Include paths:")
|
|
73
|
+
for p in include_paths:
|
|
74
|
+
logger.info(f" - {p}")
|
|
75
|
+
|
|
76
|
+
# -----------------------
|
|
77
|
+
# Sparse clone rhiza
|
|
78
|
+
# -----------------------
|
|
79
|
+
tmp_dir = Path(tempfile.mkdtemp())
|
|
80
|
+
logger.info(f"Cloning {rhiza_repo}@{rhiza_branch} into temporary directory")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
subprocess.run(
|
|
84
|
+
[
|
|
85
|
+
"git",
|
|
86
|
+
"clone",
|
|
87
|
+
"--depth",
|
|
88
|
+
"1",
|
|
89
|
+
"--filter=blob:none",
|
|
90
|
+
"--sparse",
|
|
91
|
+
"--branch",
|
|
92
|
+
rhiza_branch,
|
|
93
|
+
f"https://github.com/{rhiza_repo}.git",
|
|
94
|
+
str(tmp_dir),
|
|
95
|
+
],
|
|
96
|
+
check=True,
|
|
97
|
+
stdout=subprocess.DEVNULL,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
subprocess.run(["git", "sparse-checkout", "init"], cwd=tmp_dir, check=True)
|
|
101
|
+
subprocess.run(["git", "sparse-checkout", "set", "--skip-checks", *include_paths], cwd=tmp_dir, check=True)
|
|
102
|
+
|
|
103
|
+
# After sparse-checkout
|
|
104
|
+
all_files = expand_paths(tmp_dir, include_paths)
|
|
105
|
+
|
|
106
|
+
# Filter out excluded files
|
|
107
|
+
# excluded_set = {tmp_dir / e for e in excluded_paths}
|
|
108
|
+
excluded_files = expand_paths(tmp_dir, excluded_paths)
|
|
109
|
+
|
|
110
|
+
files_to_copy = [f for f in all_files if f not in excluded_files]
|
|
111
|
+
# print(files_to_copy)
|
|
112
|
+
|
|
113
|
+
# Copy loop
|
|
114
|
+
for src_file in files_to_copy:
|
|
115
|
+
dst_file = target / src_file.relative_to(tmp_dir)
|
|
116
|
+
if dst_file.exists() and not force:
|
|
117
|
+
logger.warning(f"{dst_file.relative_to(target)} already exists — use force=True to overwrite")
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
shutil.copy2(src_file, dst_file)
|
|
122
|
+
logger.success(f"[ADD] {dst_file.relative_to(target)}")
|
|
123
|
+
|
|
124
|
+
finally:
|
|
125
|
+
shutil.rmtree(tmp_dir)
|
|
126
|
+
|
|
127
|
+
logger.success("Rhiza templates materialized successfully")
|
|
128
|
+
logger.info("""
|
|
129
|
+
Next steps:
|
|
130
|
+
1. Review changes:
|
|
131
|
+
git status
|
|
132
|
+
git diff
|
|
133
|
+
|
|
134
|
+
2. Commit:
|
|
135
|
+
git add .
|
|
136
|
+
git commit -m "chore: import rhiza templates"
|
|
137
|
+
|
|
138
|
+
This is a one-shot snapshot.
|
|
139
|
+
Re-run this script to update templates explicitly.
|
|
140
|
+
""")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Command for validating Rhiza template configuration.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to validate .github/template.yml files
|
|
4
|
+
to ensure they are syntactically correct and semantically valid.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate(target: Path) -> bool:
|
|
14
|
+
"""Validate template.yml configuration in the target repository.
|
|
15
|
+
|
|
16
|
+
Performs authoritative validation of the template configuration:
|
|
17
|
+
- Checks if template.yml exists
|
|
18
|
+
- Validates YAML syntax
|
|
19
|
+
- Validates required fields
|
|
20
|
+
- Validates field values are appropriate
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
target:
|
|
25
|
+
Path to the target Git repository directory.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
-------
|
|
29
|
+
bool
|
|
30
|
+
True if validation passes, False otherwise.
|
|
31
|
+
"""
|
|
32
|
+
# Convert to absolute path
|
|
33
|
+
target = target.resolve()
|
|
34
|
+
|
|
35
|
+
# Check if target is a git repository
|
|
36
|
+
if not (target / ".git").is_dir():
|
|
37
|
+
logger.error(f"Target directory is not a git repository: {target}")
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
logger.info(f"Validating template configuration in: {target}")
|
|
41
|
+
|
|
42
|
+
# Check if template.yml exists
|
|
43
|
+
template_file = target / ".github" / "template.yml"
|
|
44
|
+
if not template_file.exists():
|
|
45
|
+
logger.error(f"Template file not found: {template_file}")
|
|
46
|
+
logger.info("Run 'rhiza materialize' or 'rhiza inject' to create a default template.yml")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
logger.success(f"Found template file: {template_file}")
|
|
50
|
+
|
|
51
|
+
# Validate YAML syntax
|
|
52
|
+
try:
|
|
53
|
+
with open(template_file) as f:
|
|
54
|
+
config = yaml.safe_load(f)
|
|
55
|
+
except yaml.YAMLError as e:
|
|
56
|
+
logger.error(f"Invalid YAML syntax in template.yml: {e}")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
if config is None:
|
|
60
|
+
logger.error("template.yml is empty")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
logger.success("YAML syntax is valid")
|
|
64
|
+
|
|
65
|
+
# Validate required fields
|
|
66
|
+
required_fields = {
|
|
67
|
+
"template-repository": str,
|
|
68
|
+
"include": list,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
validation_passed = True
|
|
72
|
+
|
|
73
|
+
for field, expected_type in required_fields.items():
|
|
74
|
+
if field not in config:
|
|
75
|
+
logger.error(f"Missing required field: {field}")
|
|
76
|
+
validation_passed = False
|
|
77
|
+
elif not isinstance(config[field], expected_type):
|
|
78
|
+
logger.error(
|
|
79
|
+
f"Field '{field}' must be of type {expected_type.__name__}, got {type(config[field]).__name__}"
|
|
80
|
+
)
|
|
81
|
+
validation_passed = False
|
|
82
|
+
else:
|
|
83
|
+
logger.success(f"Field '{field}' is present and valid")
|
|
84
|
+
|
|
85
|
+
# Validate template-repository format
|
|
86
|
+
if "template-repository" in config:
|
|
87
|
+
repo = config["template-repository"]
|
|
88
|
+
if not isinstance(repo, str):
|
|
89
|
+
logger.error(f"template-repository must be a string, got {type(repo).__name__}")
|
|
90
|
+
validation_passed = False
|
|
91
|
+
elif "/" not in repo:
|
|
92
|
+
logger.error(f"template-repository must be in format 'owner/repo', got: {repo}")
|
|
93
|
+
validation_passed = False
|
|
94
|
+
else:
|
|
95
|
+
logger.success(f"template-repository format is valid: {repo}")
|
|
96
|
+
|
|
97
|
+
# Validate include paths
|
|
98
|
+
if "include" in config:
|
|
99
|
+
include = config["include"]
|
|
100
|
+
if not isinstance(include, list):
|
|
101
|
+
logger.error(f"include must be a list, got {type(include).__name__}")
|
|
102
|
+
validation_passed = False
|
|
103
|
+
elif len(include) == 0:
|
|
104
|
+
logger.error("include list cannot be empty")
|
|
105
|
+
validation_passed = False
|
|
106
|
+
else:
|
|
107
|
+
logger.success(f"include list has {len(include)} path(s)")
|
|
108
|
+
for path in include:
|
|
109
|
+
if not isinstance(path, str):
|
|
110
|
+
logger.warning(f"include path should be a string, got {type(path).__name__}: {path}")
|
|
111
|
+
else:
|
|
112
|
+
logger.info(f" - {path}")
|
|
113
|
+
|
|
114
|
+
# Validate optional fields
|
|
115
|
+
if "template-branch" in config:
|
|
116
|
+
branch = config["template-branch"]
|
|
117
|
+
if not isinstance(branch, str):
|
|
118
|
+
logger.warning(f"template-branch should be a string, got {type(branch).__name__}: {branch}")
|
|
119
|
+
else:
|
|
120
|
+
logger.success(f"template-branch is valid: {branch}")
|
|
121
|
+
|
|
122
|
+
if "exclude" in config:
|
|
123
|
+
exclude = config["exclude"]
|
|
124
|
+
if not isinstance(exclude, list):
|
|
125
|
+
logger.warning(f"exclude should be a list, got {type(exclude).__name__}")
|
|
126
|
+
else:
|
|
127
|
+
logger.success(f"exclude list has {len(exclude)} path(s)")
|
|
128
|
+
for path in exclude:
|
|
129
|
+
if not isinstance(path, str):
|
|
130
|
+
logger.warning(f"exclude path should be a string, got {type(path).__name__}: {path}")
|
|
131
|
+
else:
|
|
132
|
+
logger.info(f" - {path}")
|
|
133
|
+
|
|
134
|
+
# Final verdict
|
|
135
|
+
if validation_passed:
|
|
136
|
+
logger.success("✓ Validation passed: template.yml is valid")
|
|
137
|
+
return True
|
|
138
|
+
else:
|
|
139
|
+
logger.error("✗ Validation failed: template.yml has errors")
|
|
140
|
+
return False
|
rhiza/models.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Data models for Rhiza configuration.
|
|
2
|
+
|
|
3
|
+
This module defines dataclasses that represent the structure of Rhiza
|
|
4
|
+
configuration files, making it easier to work with them without frequent
|
|
5
|
+
YAML parsing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class RhizaTemplate:
|
|
16
|
+
"""Represents the structure of .github/template.yml.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
----------
|
|
20
|
+
template_repository : str | None
|
|
21
|
+
The GitHub repository containing templates (e.g., "jebel-quant/rhiza").
|
|
22
|
+
Can be None if not specified in the template file.
|
|
23
|
+
template_branch : str | None
|
|
24
|
+
The branch to use from the template repository.
|
|
25
|
+
Can be None if not specified in the template file (defaults to "main" when creating).
|
|
26
|
+
include : list[str]
|
|
27
|
+
List of paths to include from the template repository.
|
|
28
|
+
exclude : list[str]
|
|
29
|
+
List of paths to exclude from the template repository (default: empty list).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
template_repository: str | None = None
|
|
33
|
+
template_branch: str | None = None
|
|
34
|
+
include: list[str] = field(default_factory=list)
|
|
35
|
+
exclude: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_yaml(cls, file_path: Path) -> "RhizaTemplate":
|
|
39
|
+
"""Load RhizaTemplate from a YAML file.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
file_path : Path
|
|
44
|
+
Path to the template.yml file.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
-------
|
|
48
|
+
RhizaTemplate
|
|
49
|
+
The loaded template configuration.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
------
|
|
53
|
+
FileNotFoundError
|
|
54
|
+
If the file does not exist.
|
|
55
|
+
yaml.YAMLError
|
|
56
|
+
If the YAML is malformed.
|
|
57
|
+
ValueError
|
|
58
|
+
If the file is empty.
|
|
59
|
+
"""
|
|
60
|
+
with open(file_path) as f:
|
|
61
|
+
config = yaml.safe_load(f)
|
|
62
|
+
|
|
63
|
+
if not config:
|
|
64
|
+
raise ValueError("Template file is empty")
|
|
65
|
+
|
|
66
|
+
return cls(
|
|
67
|
+
template_repository=config.get("template-repository"),
|
|
68
|
+
template_branch=config.get("template-branch"),
|
|
69
|
+
include=config.get("include", []),
|
|
70
|
+
exclude=config.get("exclude", []),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def to_yaml(self, file_path: Path) -> None:
|
|
74
|
+
"""Save RhizaTemplate to a YAML file.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
file_path : Path
|
|
79
|
+
Path where the template.yml file should be saved.
|
|
80
|
+
"""
|
|
81
|
+
# Ensure parent directory exists
|
|
82
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
# Convert to dictionary with YAML-compatible keys
|
|
85
|
+
config = {}
|
|
86
|
+
|
|
87
|
+
# Only include template-repository if it's not None
|
|
88
|
+
if self.template_repository:
|
|
89
|
+
config["template-repository"] = self.template_repository
|
|
90
|
+
|
|
91
|
+
# Only include template-branch if it's not None
|
|
92
|
+
if self.template_branch:
|
|
93
|
+
config["template-branch"] = self.template_branch
|
|
94
|
+
|
|
95
|
+
# Include is always present as it's a required field for the config to be useful
|
|
96
|
+
config["include"] = self.include
|
|
97
|
+
|
|
98
|
+
# Only include exclude if it's not empty
|
|
99
|
+
if self.exclude:
|
|
100
|
+
config["exclude"] = self.exclude
|
|
101
|
+
|
|
102
|
+
with open(file_path, "w") as f:
|
|
103
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|