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

1"""Template model for Rhiza configuration.""" 

2 

3from dataclasses import dataclass, field 

4from enum import StrEnum 

5from typing import TYPE_CHECKING, Any 

6 

7from rhiza.models._base import YamlSerializable 

8from rhiza.models._git_utils import _normalize_to_list 

9 

10if TYPE_CHECKING: 

11 pass 

12 

13 

14class GitHost(StrEnum): 

15 """Supported git hosting platforms.""" 

16 

17 GITHUB = "github" 

18 GITLAB = "gitlab" 

19 

20 

21@dataclass(kw_only=True, frozen=True) 

22class RhizaTemplate(YamlSerializable): 

23 """Represents the structure of .rhiza/template.yml. 

24 

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 """ 

41 

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" 

51 

52 @classmethod 

53 def from_config(cls, config: dict[str, Any]) -> "RhizaTemplate": 

54 """Create a RhizaTemplate instance from a configuration dictionary. 

55 

56 Args: 

57 config: Dictionary containing template configuration. 

58 

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") 

65 

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") 

69 

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 ) 

81 

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 

99 

100 @property 

101 def git_url(self) -> str: 

102 """Construct the HTTPS clone URL for this template repository. 

103 

104 Returns: 

105 HTTPS clone URL derived from ``template_repository`` and 

106 ``template_host``. 

107 

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