Coverage for src / rhiza / models / template.py: 100%
49 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-06-15 18:22 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-06-15 18:22 +0000
1"""Template model for Rhiza configuration."""
3from dataclasses import dataclass, field
4from enum import StrEnum
5from typing import TYPE_CHECKING, Any
7from rhiza.models._base import YamlSerializable
8from rhiza.models._git_utils import _normalize_to_list
10if TYPE_CHECKING:
11 pass
14class GitHost(StrEnum):
15 """Supported git hosting platforms."""
17 GITHUB = "github"
18 GITLAB = "gitlab"
21@dataclass(kw_only=True, frozen=True)
22class RhizaTemplate(YamlSerializable):
23 """Represents the structure of .rhiza/template.yml.
25 Attributes:
26 template_repository: The GitHub or GitLab repository containing templates (e.g., "jebel-quant/rhiza").
27 Can be None if not specified in the template file.
28 template_branch: The branch to use from the template repository.
29 Can be None if not specified in the template file (defaults to "main" when creating).
30 template_host: The git hosting platform ("github" or "gitlab").
31 Defaults to "github" if not specified in the template file.
32 language: The programming language of the project ("python", "go", etc.).
33 Defaults to "python" if not specified in the template file.
34 include: List of paths to include from the template repository (path-based mode).
35 exclude: List of paths to exclude from the template repository (default: empty list).
36 templates: List of template names to include (template-based mode).
37 Can be used together with include to merge paths.
38 template_bundles_path: Path to the bundle definitions file inside the upstream
39 template repository. Defaults to ``.rhiza/template-bundles.yml``.
40 """
42 template_repository: str = ""
43 template_branch: str = ""
44 template_host: GitHost | str = GitHost.GITHUB
45 language: str = "python"
46 include: list[str] = field(default_factory=list)
47 exclude: list[str] = field(default_factory=list)
48 templates: list[str] = field(default_factory=list)
49 profiles: list[str] = field(default_factory=list)
50 template_bundles_path: str = ".rhiza/template-bundles.yml"
52 @classmethod
53 def from_config(cls, config: dict[str, Any]) -> "RhizaTemplate":
54 """Create a RhizaTemplate instance from a configuration dictionary.
56 Args:
57 config: Dictionary containing template configuration.
59 Returns:
60 A new RhizaTemplate instance.
61 """
62 # Support both 'repository' and 'template-repository' (repository takes precedence)
63 # Empty or None values fall back to the alternative field
64 template_repository = config.get("repository") or config.get("template-repository")
66 # Support both 'ref' and 'template-branch' (ref takes precedence)
67 # Empty or None values fall back to the alternative field
68 template_branch = config.get("ref") or config.get("template-branch")
70 return cls(
71 template_repository=template_repository or "",
72 template_branch=template_branch or "",
73 template_host=config.get("template-host", GitHost.GITHUB),
74 language=config.get("language", "python"),
75 include=_normalize_to_list(config.get("include")),
76 exclude=_normalize_to_list(config.get("exclude")),
77 templates=_normalize_to_list(config.get("templates")),
78 profiles=_normalize_to_list(config.get("profiles")),
79 template_bundles_path=config.get("template-bundles-path", ".rhiza/template-bundles.yml"),
80 )
82 @property
83 def config(self) -> dict[str, Any]:
84 """Read template configuration from the template.yml file."""
85 # Convert to dictionary with YAML-compatible keys
86 config: dict[str, Any] = {}
87 config["repository"] = self.template_repository
88 config["ref"] = self.template_branch
89 config["template-host"] = str(self.template_host)
90 config["language"] = self.language
91 config["templates"] = self.templates
92 config["include"] = self.include
93 config["exclude"] = self.exclude
94 if self.profiles:
95 config["profiles"] = self.profiles
96 if self.template_bundles_path != ".rhiza/template-bundles.yml":
97 config["template-bundles-path"] = self.template_bundles_path
98 return config
100 @property
101 def git_url(self) -> str:
102 """Construct the HTTPS clone URL for this template repository.
104 Returns:
105 HTTPS clone URL derived from ``template_repository`` and
106 ``template_host``.
108 Raises:
109 ValueError: If ``template_repository`` is not set or
110 ``template_host`` is not ``"github"`` or ``"gitlab"``.
111 """
112 if not self.template_repository:
113 raise ValueError("template_repository is not configured in template.yml") # noqa: TRY003
114 host = self.template_host or GitHost.GITHUB
115 if host == GitHost.GITHUB:
116 return f"https://github.com/{self.template_repository}.git"
117 if host == GitHost.GITLAB:
118 return f"https://gitlab.com/{self.template_repository}.git"
119 raise ValueError(f"Unsupported template-host: {host}. Must be 'github' or 'gitlab'.") # noqa: TRY003