|
|
@@ -5,7 +5,7 @@ import logging
|
|
|
from typing import Optional
|
|
|
import yaml
|
|
|
|
|
|
-from .exceptions import LibraryError, TemplateNotFoundError, YAMLParseError
|
|
|
+from .exceptions import LibraryError, TemplateNotFoundError, YAMLParseError, DuplicateTemplateError
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@@ -13,17 +13,22 @@ logger = logging.getLogger(__name__)
|
|
|
class Library:
|
|
|
"""Represents a single library with a specific path."""
|
|
|
|
|
|
- def __init__(self, name: str, path: Path, priority: int = 0) -> None:
|
|
|
+ def __init__(self, name: str, path: Path, priority: int = 0, library_type: str = "git") -> None:
|
|
|
"""Initialize a library instance.
|
|
|
|
|
|
Args:
|
|
|
name: Display name for the library
|
|
|
path: Path to the library directory
|
|
|
priority: Priority for library lookup (higher = checked first)
|
|
|
+ library_type: Type of library ("git" or "static")
|
|
|
"""
|
|
|
+ if library_type not in ("git", "static"):
|
|
|
+ raise ValueError(f"Invalid library type: {library_type}. Must be 'git' or 'static'.")
|
|
|
+
|
|
|
self.name = name
|
|
|
self.path = path
|
|
|
self.priority = priority # Higher priority = checked first
|
|
|
+ self.library_type = library_type
|
|
|
|
|
|
def _is_template_draft(self, template_path: Path) -> bool:
|
|
|
"""Check if a template is marked as draft."""
|
|
|
@@ -97,12 +102,20 @@ class Library:
|
|
|
if not module_path.is_dir():
|
|
|
raise LibraryError(f"Module '{module_name}' not found in library '{self.name}'")
|
|
|
|
|
|
- # Get non-draft templates
|
|
|
+ # Track seen IDs to detect duplicates within this library
|
|
|
+ seen_ids = {}
|
|
|
template_dirs = []
|
|
|
try:
|
|
|
for item in module_path.iterdir():
|
|
|
has_template = item.is_dir() and any((item / f).exists() for f in ("template.yaml", "template.yml"))
|
|
|
if has_template and not self._is_template_draft(item):
|
|
|
+ template_id = item.name
|
|
|
+
|
|
|
+ # Check for duplicate within same library
|
|
|
+ if template_id in seen_ids:
|
|
|
+ raise DuplicateTemplateError(template_id, self.name)
|
|
|
+
|
|
|
+ seen_ids[template_id] = True
|
|
|
template_dirs.append((item, self.name))
|
|
|
elif has_template:
|
|
|
logger.debug(f"Skipping draft template: {item.name}")
|
|
|
@@ -145,28 +158,55 @@ class LibraryManager:
|
|
|
continue
|
|
|
|
|
|
name = lib_config.get("name")
|
|
|
- directory = lib_config.get("directory", ".")
|
|
|
+ lib_type = lib_config.get("type", "git") # Default to "git" for backward compat
|
|
|
+
|
|
|
+ # Handle library type-specific path resolution
|
|
|
+ if lib_type == "git":
|
|
|
+ # Existing git logic
|
|
|
+ directory = lib_config.get("directory", ".")
|
|
|
+
|
|
|
+ # Build path to library: ~/.config/boilerplates/libraries/{name}/{directory}/
|
|
|
+ # For sparse-checkout, files remain in the specified directory
|
|
|
+ library_base = libraries_path / name
|
|
|
+ if directory and directory != ".":
|
|
|
+ library_path = library_base / directory
|
|
|
+ else:
|
|
|
+ library_path = library_base
|
|
|
+
|
|
|
+ elif lib_type == "static":
|
|
|
+ # New static logic - use path directly
|
|
|
+ path_str = lib_config.get("path")
|
|
|
+ if not path_str:
|
|
|
+ logger.warning(f"Static library '{name}' has no path configured")
|
|
|
+ continue
|
|
|
+
|
|
|
+ # Expand ~ and resolve relative paths
|
|
|
+ library_path = Path(path_str).expanduser()
|
|
|
+ if not library_path.is_absolute():
|
|
|
+ # Resolve relative to config directory
|
|
|
+ library_path = (self.config.config_path.parent / library_path).resolve()
|
|
|
|
|
|
- # Build path to library: ~/.config/boilerplates/libraries/{name}/{directory}/
|
|
|
- # For sparse-checkout, files remain in the specified directory
|
|
|
- library_base = libraries_path / name
|
|
|
- if directory and directory != ".":
|
|
|
- library_path = library_base / directory
|
|
|
else:
|
|
|
- library_path = library_base
|
|
|
+ logger.warning(f"Unknown library type '{lib_type}' for library '{name}'")
|
|
|
+ continue
|
|
|
|
|
|
# Check if library path exists
|
|
|
if not library_path.exists():
|
|
|
- logger.warning(
|
|
|
- f"Library '{name}' not found at {library_path}. "
|
|
|
- f"Run 'repo update' to sync libraries."
|
|
|
- )
|
|
|
+ if lib_type == "git":
|
|
|
+ logger.warning(
|
|
|
+ f"Library '{name}' not found at {library_path}. "
|
|
|
+ f"Run 'repo update' to sync libraries."
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ logger.warning(f"Static library '{name}' not found at {library_path}")
|
|
|
continue
|
|
|
|
|
|
- # Create Library instance with priority based on order (first = highest priority)
|
|
|
+ # Create Library instance with type and priority based on order (first = highest priority)
|
|
|
priority = len(library_configs) - i
|
|
|
- libraries.append(Library(name=name, path=library_path, priority=priority))
|
|
|
- logger.debug(f"Loaded library '{name}' from {library_path} with priority {priority}")
|
|
|
+ libraries.append(
|
|
|
+ Library(name=name, path=library_path, priority=priority, library_type=lib_type)
|
|
|
+ )
|
|
|
+ logger.debug(f"Loaded {lib_type} library '{name}' from {library_path} with priority {priority}")
|
|
|
|
|
|
if not libraries:
|
|
|
logger.warning("No libraries loaded. Run 'repo update' to sync libraries.")
|
|
|
@@ -176,15 +216,39 @@ class LibraryManager:
|
|
|
def find_by_id(self, module_name: str, template_id: str) -> Optional[tuple[Path, str]]:
|
|
|
"""Find a template by its ID across all libraries.
|
|
|
|
|
|
+ Supports both simple IDs and qualified IDs (template.library format).
|
|
|
+
|
|
|
Args:
|
|
|
module_name: The module name (e.g., 'compose', 'terraform')
|
|
|
- template_id: The template ID to find
|
|
|
+ template_id: The template ID to find (simple or qualified)
|
|
|
|
|
|
Returns:
|
|
|
- Path to the template directory if found, None otherwise
|
|
|
+ Tuple of (template_path, library_name) if found, None otherwise
|
|
|
"""
|
|
|
logger.debug(f"Searching for template '{template_id}' in module '{module_name}' across all libraries")
|
|
|
|
|
|
+ # Check if this is a qualified ID (contains '.')
|
|
|
+ if '.' in template_id:
|
|
|
+ parts = template_id.rsplit('.', 1)
|
|
|
+ if len(parts) == 2:
|
|
|
+ base_id, requested_lib = parts
|
|
|
+ logger.debug(f"Parsing qualified ID: base='{base_id}', library='{requested_lib}'")
|
|
|
+
|
|
|
+ # Try to find in the specific library
|
|
|
+ for library in self.libraries:
|
|
|
+ if library.name == requested_lib:
|
|
|
+ try:
|
|
|
+ template_path, lib_name = library.find_by_id(module_name, base_id)
|
|
|
+ logger.debug(f"Found template '{base_id}' in library '{requested_lib}'")
|
|
|
+ return template_path, lib_name
|
|
|
+ except TemplateNotFoundError:
|
|
|
+ logger.debug(f"Template '{base_id}' not found in library '{requested_lib}'")
|
|
|
+ return None
|
|
|
+
|
|
|
+ logger.debug(f"Library '{requested_lib}' not found")
|
|
|
+ return None
|
|
|
+
|
|
|
+ # Simple ID - search by priority
|
|
|
for library in sorted(self.libraries, key=lambda x: x.priority, reverse=True):
|
|
|
try:
|
|
|
template_path, lib_name = library.find_by_id(module_name, template_id)
|
|
|
@@ -197,42 +261,65 @@ class LibraryManager:
|
|
|
logger.debug(f"Template '{template_id}' not found in any library")
|
|
|
return None
|
|
|
|
|
|
- def find(self, module_name: str, sort_results: bool = False) -> list[tuple[Path, str]]:
|
|
|
+ def find(self, module_name: str, sort_results: bool = False) -> list[tuple[Path, str, bool]]:
|
|
|
"""Find templates across all libraries for a specific module.
|
|
|
|
|
|
+ Handles duplicates by qualifying IDs with library names when needed.
|
|
|
+
|
|
|
Args:
|
|
|
module_name: The module name (e.g., 'compose', 'terraform')
|
|
|
sort_results: Whether to return results sorted alphabetically
|
|
|
|
|
|
Returns:
|
|
|
- List of Path objects representing template directories from all libraries
|
|
|
+ List of tuples (template_path, library_name, needs_qualification)
|
|
|
+ where needs_qualification is True if the template ID appears in multiple libraries
|
|
|
"""
|
|
|
logger.debug(f"Searching for templates in module '{module_name}' across all libraries")
|
|
|
|
|
|
all_templates = []
|
|
|
|
|
|
+ # Collect templates from all libraries
|
|
|
for library in sorted(self.libraries, key=lambda x: x.priority, reverse=True):
|
|
|
try:
|
|
|
- templates = library.find(module_name, sort_results=False) # Sort at the end
|
|
|
+ templates = library.find(module_name, sort_results=False)
|
|
|
all_templates.extend(templates)
|
|
|
logger.debug(f"Found {len(templates)} templates in library '{library.name}'")
|
|
|
- except LibraryError:
|
|
|
- # Module not found in this library, continue with next
|
|
|
+ except (LibraryError, DuplicateTemplateError) as e:
|
|
|
+ # DuplicateTemplateError from library.find() should propagate up
|
|
|
+ if isinstance(e, DuplicateTemplateError):
|
|
|
+ raise
|
|
|
logger.debug(f"Module '{module_name}' not found in library '{library.name}'")
|
|
|
continue
|
|
|
|
|
|
- # Remove duplicates based on template name (directory name)
|
|
|
- seen_names = set()
|
|
|
- unique_templates = []
|
|
|
- for template in all_templates:
|
|
|
- name, library_name = template
|
|
|
- if name.name not in seen_names:
|
|
|
- unique_templates.append((name, library_name))
|
|
|
- seen_names.add(name.name)
|
|
|
+ # Track template IDs and their libraries to detect cross-library duplicates
|
|
|
+ id_to_occurrences = {}
|
|
|
+ for template_path, library_name in all_templates:
|
|
|
+ template_id = template_path.name
|
|
|
+ if template_id not in id_to_occurrences:
|
|
|
+ id_to_occurrences[template_id] = []
|
|
|
+ id_to_occurrences[template_id].append((template_path, library_name))
|
|
|
+
|
|
|
+ # Build result with qualification markers for duplicates
|
|
|
+ result = []
|
|
|
+ for template_id, occurrences in id_to_occurrences.items():
|
|
|
+ if len(occurrences) > 1:
|
|
|
+ # Duplicate across libraries - mark for qualified IDs
|
|
|
+ lib_names = ', '.join(lib for _, lib in occurrences)
|
|
|
+ logger.info(
|
|
|
+ f"Template '{template_id}' found in multiple libraries: {lib_names}. "
|
|
|
+ f"Using qualified IDs."
|
|
|
+ )
|
|
|
+ for template_path, library_name in occurrences:
|
|
|
+ # Mark that this ID needs qualification
|
|
|
+ result.append((template_path, library_name, True))
|
|
|
+ else:
|
|
|
+ # Unique template - no qualification needed
|
|
|
+ template_path, library_name = occurrences[0]
|
|
|
+ result.append((template_path, library_name, False))
|
|
|
|
|
|
# Sort if requested
|
|
|
if sort_results:
|
|
|
- unique_templates.sort(key=lambda x: x[0].name.lower())
|
|
|
+ result.sort(key=lambda x: x[0].name.lower())
|
|
|
|
|
|
- logger.debug(f"Found {len(unique_templates)} unique templates total")
|
|
|
- return unique_templates
|
|
|
+ logger.debug(f"Found {len(result)} templates total")
|
|
|
+ return result
|