library.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import logging
  4. from typing import Optional
  5. logger = logging.getLogger(__name__)
  6. # -----------------------
  7. # SECTION: Library Class
  8. # -----------------------
  9. class Library:
  10. """Represents a single library with a specific path."""
  11. def __init__(self, name: str, path: Path, priority: int = 0) -> None:
  12. """Initialize a library instance.
  13. Args:
  14. name: Display name for the library
  15. path: Path to the library directory
  16. priority: Priority for library lookup (higher = checked first)
  17. """
  18. self.name = name
  19. self.path = path
  20. self.priority = priority # Higher priority = checked first
  21. def _is_template_draft(self, template_path: Path) -> bool:
  22. """Check if a template is marked as draft.
  23. Args:
  24. template_path: Path to the template directory
  25. Returns:
  26. True if the template is marked as draft, False otherwise
  27. """
  28. import yaml
  29. # Find the template file
  30. template_file = None
  31. if (template_path / "template.yaml").exists():
  32. template_file = template_path / "template.yaml"
  33. elif (template_path / "template.yml").exists():
  34. template_file = template_path / "template.yml"
  35. if not template_file:
  36. return False
  37. try:
  38. with open(template_file, "r", encoding="utf-8") as f:
  39. documents = list(yaml.safe_load_all(f))
  40. valid_docs = [doc for doc in documents if doc is not None]
  41. if not valid_docs:
  42. return False
  43. template_data = valid_docs[0]
  44. metadata = template_data.get("metadata", {})
  45. return metadata.get("draft", False)
  46. except Exception as e:
  47. logger.warning(f"Error checking draft status for {template_path}: {e}")
  48. return False
  49. def find_by_id(self, module_name: str, template_id: str) -> tuple[Path, str]:
  50. """Find a template by its ID in this library.
  51. Args:
  52. module_name: The module name (e.g., 'compose', 'terraform')
  53. template_id: The template ID to find
  54. Returns:
  55. Path to the template directory if found
  56. Raises:
  57. FileNotFoundError: If the template ID is not found in this library or is marked as draft
  58. """
  59. logger.debug(f"Looking for template '{template_id}' in module '{module_name}' in library '{self.name}'")
  60. # Build the path to the specific template directory
  61. template_path = self.path / module_name / template_id
  62. # Check if the template directory and either template.yaml or template.yml exist
  63. if not (template_path.is_dir() and ((template_path / "template.yaml").exists() or (template_path / "template.yml").exists())):
  64. raise FileNotFoundError(f"Template '{template_id}' not found in module '{module_name}' in library '{self.name}'")
  65. # Check if template is marked as draft
  66. if self._is_template_draft(template_path):
  67. raise FileNotFoundError(f"Template '{template_id}' is marked as draft and cannot be used")
  68. logger.debug(f"Found template '{template_id}' at: {template_path}")
  69. return template_path, self.name
  70. def find(self, module_name: str, sort_results: bool = False) -> list[tuple[Path, str]]:
  71. """Find templates in this library for a specific module.
  72. Excludes templates marked as draft.
  73. Args:
  74. module_name: The module name (e.g., 'compose', 'terraform')
  75. sort_results: Whether to return results sorted alphabetically
  76. Returns:
  77. List of Path objects representing template directories (excluding drafts)
  78. Raises:
  79. FileNotFoundError: If the module directory is not found in this library
  80. """
  81. logger.debug(f"Looking for templates in module '{module_name}' in library '{self.name}'")
  82. # Build the path to the module directory
  83. module_path = self.path / module_name
  84. # Check if the module directory exists
  85. if not module_path.is_dir():
  86. raise FileNotFoundError(f"Module '{module_name}' not found in library '{self.name}'")
  87. # Get all directories in the module path that contain a template.yaml or template.yml file
  88. # and are not marked as draft
  89. template_dirs = []
  90. try:
  91. for item in module_path.iterdir():
  92. if item.is_dir() and ((item / "template.yaml").exists() or (item / "template.yml").exists()):
  93. # Skip draft templates
  94. if not self._is_template_draft(item):
  95. template_dirs.append((item, self.name))
  96. else:
  97. logger.debug(f"Skipping draft template: {item.name}")
  98. except PermissionError as e:
  99. raise FileNotFoundError(f"Permission denied accessing module '{module_name}' in library '{self.name}': {e}")
  100. # Sort if requested
  101. if sort_results:
  102. template_dirs.sort(key=lambda x: x[0].name.lower())
  103. logger.debug(f"Found {len(template_dirs)} templates in module '{module_name}'")
  104. return template_dirs
  105. # !SECTION
  106. # -----------------------------
  107. # SECTION: LibraryManager Class
  108. # -----------------------------
  109. class LibraryManager:
  110. """Manages multiple libraries and provides methods to find templates."""
  111. # FIXME: For now this is static and only has one library
  112. def __init__(self) -> None:
  113. # get the root path of the repository
  114. repo_root = Path(__file__).parent.parent.parent.resolve()
  115. self.libraries = [
  116. Library(name="default", path=repo_root / "library", priority=0)
  117. ]
  118. def find_by_id(self, module_name: str, template_id: str) -> Optional[tuple[Path, str]]:
  119. """Find a template by its ID across all libraries.
  120. Args:
  121. module_name: The module name (e.g., 'compose', 'terraform')
  122. template_id: The template ID to find
  123. Returns:
  124. Path to the template directory if found, None otherwise
  125. """
  126. logger.debug(f"Searching for template '{template_id}' in module '{module_name}' across all libraries")
  127. for library in sorted(self.libraries, key=lambda x: x.priority, reverse=True):
  128. try:
  129. template_path, lib_name = library.find_by_id(module_name, template_id)
  130. logger.debug(f"Found template '{template_id}' in library '{library.name}'")
  131. return template_path, lib_name
  132. except FileNotFoundError:
  133. # Continue searching in next library
  134. continue
  135. logger.debug(f"Template '{template_id}' not found in any library")
  136. return None
  137. def find(self, module_name: str, sort_results: bool = False) -> list[tuple[Path, str]]:
  138. """Find templates across all libraries for a specific module.
  139. Args:
  140. module_name: The module name (e.g., 'compose', 'terraform')
  141. sort_results: Whether to return results sorted alphabetically
  142. Returns:
  143. List of Path objects representing template directories from all libraries
  144. """
  145. logger.debug(f"Searching for templates in module '{module_name}' across all libraries")
  146. all_templates = []
  147. for library in sorted(self.libraries, key=lambda x: x.priority, reverse=True):
  148. try:
  149. templates = library.find(module_name, sort_results=False) # Sort at the end
  150. all_templates.extend(templates)
  151. logger.debug(f"Found {len(templates)} templates in library '{library.name}'")
  152. except FileNotFoundError:
  153. # Module not found in this library, continue with next
  154. logger.debug(f"Module '{module_name}' not found in library '{library.name}'")
  155. continue
  156. # Remove duplicates based on template name (directory name)
  157. seen_names = set()
  158. unique_templates = []
  159. for template in all_templates:
  160. name, library_name = template
  161. if name.name not in seen_names:
  162. unique_templates.append((name, library_name))
  163. seen_names.add(name.name)
  164. # Sort if requested
  165. if sort_results:
  166. unique_templates.sort(key=lambda x: x[0].name.lower())
  167. logger.debug(f"Found {len(unique_templates)} unique templates total")
  168. return unique_templates
  169. # !SECTION