library.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import logging
  4. from typing import Optional
  5. import yaml
  6. from .exceptions import LibraryError, TemplateNotFoundError, YAMLParseError
  7. logger = logging.getLogger(__name__)
  8. class Library:
  9. """Represents a single library with a specific path."""
  10. def __init__(self, name: str, path: Path, priority: int = 0) -> None:
  11. """Initialize a library instance.
  12. Args:
  13. name: Display name for the library
  14. path: Path to the library directory
  15. priority: Priority for library lookup (higher = checked first)
  16. """
  17. self.name = name
  18. self.path = path
  19. self.priority = priority # Higher priority = checked first
  20. def _is_template_draft(self, template_path: Path) -> bool:
  21. """Check if a template is marked as draft."""
  22. # Find the template file
  23. for filename in ("template.yaml", "template.yml"):
  24. template_file = template_path / filename
  25. if template_file.exists():
  26. break
  27. else:
  28. return False
  29. try:
  30. with open(template_file, "r", encoding="utf-8") as f:
  31. docs = [doc for doc in yaml.safe_load_all(f) if doc]
  32. return docs[0].get("metadata", {}).get("draft", False) if docs else False
  33. except (yaml.YAMLError, IOError, OSError) as e:
  34. logger.warning(f"Error checking draft status for {template_path}: {e}")
  35. return False
  36. def find_by_id(self, module_name: str, template_id: str) -> tuple[Path, str]:
  37. """Find a template by its ID in this library.
  38. Args:
  39. module_name: The module name (e.g., 'compose', 'terraform')
  40. template_id: The template ID to find
  41. Returns:
  42. Path to the template directory if found
  43. Raises:
  44. FileNotFoundError: If the template ID is not found in this library or is marked as draft
  45. """
  46. logger.debug(f"Looking for template '{template_id}' in module '{module_name}' in library '{self.name}'")
  47. # Build the path to the specific template directory
  48. template_path = self.path / module_name / template_id
  49. # Check if template directory exists with a template file
  50. has_template = template_path.is_dir() and any(
  51. (template_path / f).exists() for f in ("template.yaml", "template.yml")
  52. )
  53. if not has_template or self._is_template_draft(template_path):
  54. raise TemplateNotFoundError(template_id, module_name)
  55. logger.debug(f"Found template '{template_id}' at: {template_path}")
  56. return template_path, self.name
  57. def find(self, module_name: str, sort_results: bool = False) -> list[tuple[Path, str]]:
  58. """Find templates in this library for a specific module.
  59. Excludes templates marked as draft.
  60. Args:
  61. module_name: The module name (e.g., 'compose', 'terraform')
  62. sort_results: Whether to return results sorted alphabetically
  63. Returns:
  64. List of Path objects representing template directories (excluding drafts)
  65. Raises:
  66. FileNotFoundError: If the module directory is not found in this library
  67. """
  68. logger.debug(f"Looking for templates in module '{module_name}' in library '{self.name}'")
  69. # Build the path to the module directory
  70. module_path = self.path / module_name
  71. # Check if the module directory exists
  72. if not module_path.is_dir():
  73. raise LibraryError(f"Module '{module_name}' not found in library '{self.name}'")
  74. # Get non-draft templates
  75. template_dirs = []
  76. try:
  77. for item in module_path.iterdir():
  78. has_template = item.is_dir() and any((item / f).exists() for f in ("template.yaml", "template.yml"))
  79. if has_template and not self._is_template_draft(item):
  80. template_dirs.append((item, self.name))
  81. elif has_template:
  82. logger.debug(f"Skipping draft template: {item.name}")
  83. except PermissionError as e:
  84. raise LibraryError(f"Permission denied accessing module '{module_name}' in library '{self.name}': {e}")
  85. # Sort if requested
  86. if sort_results:
  87. template_dirs.sort(key=lambda x: x[0].name.lower())
  88. logger.debug(f"Found {len(template_dirs)} templates in module '{module_name}'")
  89. return template_dirs
  90. class LibraryManager:
  91. """Manages multiple libraries and provides methods to find templates."""
  92. def __init__(self) -> None:
  93. """Initialize LibraryManager with git-based libraries from config."""
  94. from .config import ConfigManager
  95. self.config = ConfigManager()
  96. self.libraries = self._load_libraries_from_config()
  97. def _load_libraries_from_config(self) -> list[Library]:
  98. """Load libraries from configuration.
  99. Returns:
  100. List of Library instances
  101. """
  102. libraries = []
  103. libraries_path = self.config.get_libraries_path()
  104. # Get library configurations from config
  105. library_configs = self.config.get_libraries()
  106. for i, lib_config in enumerate(library_configs):
  107. # Skip disabled libraries
  108. if not lib_config.get("enabled", True):
  109. logger.debug(f"Skipping disabled library: {lib_config.get('name')}")
  110. continue
  111. name = lib_config.get("name")
  112. directory = lib_config.get("directory", ".")
  113. # Build path to library: ~/.config/boilerplates/libraries/{name}/{directory}/
  114. # For sparse-checkout, files remain in the specified directory
  115. library_base = libraries_path / name
  116. if directory and directory != ".":
  117. library_path = library_base / directory
  118. else:
  119. library_path = library_base
  120. # Check if library path exists
  121. if not library_path.exists():
  122. logger.warning(
  123. f"Library '{name}' not found at {library_path}. "
  124. f"Run 'repo update' to sync libraries."
  125. )
  126. continue
  127. # Create Library instance with priority based on order (first = highest priority)
  128. priority = len(library_configs) - i
  129. libraries.append(Library(name=name, path=library_path, priority=priority))
  130. logger.debug(f"Loaded library '{name}' from {library_path} with priority {priority}")
  131. if not libraries:
  132. logger.warning("No libraries loaded. Run 'repo update' to sync libraries.")
  133. return libraries
  134. def find_by_id(self, module_name: str, template_id: str) -> Optional[tuple[Path, str]]:
  135. """Find a template by its ID across all libraries.
  136. Args:
  137. module_name: The module name (e.g., 'compose', 'terraform')
  138. template_id: The template ID to find
  139. Returns:
  140. Path to the template directory if found, None otherwise
  141. """
  142. logger.debug(f"Searching for template '{template_id}' in module '{module_name}' across all libraries")
  143. for library in sorted(self.libraries, key=lambda x: x.priority, reverse=True):
  144. try:
  145. template_path, lib_name = library.find_by_id(module_name, template_id)
  146. logger.debug(f"Found template '{template_id}' in library '{library.name}'")
  147. return template_path, lib_name
  148. except TemplateNotFoundError:
  149. # Continue searching in next library
  150. continue
  151. logger.debug(f"Template '{template_id}' not found in any library")
  152. return None
  153. def find(self, module_name: str, sort_results: bool = False) -> list[tuple[Path, str]]:
  154. """Find templates across all libraries for a specific module.
  155. Args:
  156. module_name: The module name (e.g., 'compose', 'terraform')
  157. sort_results: Whether to return results sorted alphabetically
  158. Returns:
  159. List of Path objects representing template directories from all libraries
  160. """
  161. logger.debug(f"Searching for templates in module '{module_name}' across all libraries")
  162. all_templates = []
  163. for library in sorted(self.libraries, key=lambda x: x.priority, reverse=True):
  164. try:
  165. templates = library.find(module_name, sort_results=False) # Sort at the end
  166. all_templates.extend(templates)
  167. logger.debug(f"Found {len(templates)} templates in library '{library.name}'")
  168. except LibraryError:
  169. # Module not found in this library, continue with next
  170. logger.debug(f"Module '{module_name}' not found in library '{library.name}'")
  171. continue
  172. # Remove duplicates based on template name (directory name)
  173. seen_names = set()
  174. unique_templates = []
  175. for template in all_templates:
  176. name, library_name = template
  177. if name.name not in seen_names:
  178. unique_templates.append((name, library_name))
  179. seen_names.add(name.name)
  180. # Sort if requested
  181. if sort_results:
  182. unique_templates.sort(key=lambda x: x[0].name.lower())
  183. logger.debug(f"Found {len(unique_templates)} unique templates total")
  184. return unique_templates