Coverage for src / rhiza / models / template.py: 100%

46 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 07:04 +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 template_bundles_path: str = ".rhiza/template-bundles.yml" 

50 

51 @classmethod 

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

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

54 

55 Args: 

56 config: Dictionary containing template configuration. 

57 

58 Returns: 

59 A new RhizaTemplate instance. 

60 """ 

61 # Support both 'repository' and 'template-repository' (repository takes precedence) 

62 # Empty or None values fall back to the alternative field 

63 template_repository = config.get("repository") or config.get("template-repository") 

64 

65 # Support both 'ref' and 'template-branch' (ref takes precedence) 

66 # Empty or None values fall back to the alternative field 

67 template_branch = config.get("ref") or config.get("template-branch") 

68 

69 return cls( 

70 template_repository=template_repository or "", 

71 template_branch=template_branch or "", 

72 template_host=config.get("template-host", GitHost.GITHUB), 

73 language=config.get("language", "python"), 

74 include=_normalize_to_list(config.get("include")), 

75 exclude=_normalize_to_list(config.get("exclude")), 

76 templates=_normalize_to_list(config.get("templates")), 

77 template_bundles_path=config.get("template-bundles-path", ".rhiza/template-bundles.yml"), 

78 ) 

79 

80 @property 

81 def config(self) -> dict[str, Any]: 

82 """Read template configuration from the template.yml file.""" 

83 # Convert to dictionary with YAML-compatible keys 

84 config: dict[str, Any] = {} 

85 config["repository"] = self.template_repository 

86 config["ref"] = self.template_branch 

87 config["template-host"] = str(self.template_host) 

88 config["language"] = self.language 

89 config["templates"] = self.templates 

90 config["include"] = self.include 

91 config["exclude"] = self.exclude 

92 if self.template_bundles_path != ".rhiza/template-bundles.yml": 

93 config["template-bundles-path"] = self.template_bundles_path 

94 return config 

95 

96 @property 

97 def git_url(self) -> str: 

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

99 

100 Returns: 

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

102 ``template_host``. 

103 

104 Raises: 

105 ValueError: If ``template_repository`` is not set or 

106 ``template_host`` is not ``"github"`` or ``"gitlab"``. 

107 """ 

108 if not self.template_repository: 

109 raise ValueError("template_repository is not configured in template.yml") # noqa: TRY003 

110 host = self.template_host or GitHost.GITHUB 

111 if host == GitHost.GITHUB: 

112 return f"https://github.com/{self.template_repository}.git" 

113 if host == GitHost.GITLAB: 

114 return f"https://gitlab.com/{self.template_repository}.git" 

115 raise ValueError(f"Unsupported template-host: {host}. Must be 'github' or 'gitlab'.") # noqa: TRY003