from __future__ import annotations import importlib from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, Iterable, List import yaml REQUIRED_FIELDS = {"name", "type", "version", "description", "capabilities", "entrypoint"} @dataclass class CatalogEntry: name: str type: str version: str description: str capabilities: List[str] entrypoint: str provider: str | None = None def load(self) -> Any: module_name, attr = self.entrypoint.rsplit(".", 1) module = importlib.import_module(module_name) return getattr(module, attr) class CatalogRegistry: def __init__(self, entries: List[CatalogEntry]) -> None: self.entries = entries @classmethod def _load_file(cls, path: Path) -> List[CatalogEntry]: data = yaml.safe_load(path.read_text()) if path.exists() else [] entries: List[CatalogEntry] = [] for raw in data or []: missing = REQUIRED_FIELDS - set(raw) if missing: raise ValueError(f"Catalog entry missing fields {missing} in {path}") entries.append(CatalogEntry(**raw)) return entries @classmethod def from_default(cls) -> "CatalogRegistry": base = Path(__file__).parent.parent / "catalogs" entries: List[CatalogEntry] = [] for name in ["models.yaml", "tools.yaml", "plugins.yaml"]: entries.extend(cls._load_file(base / name)) return cls(entries) def find(self, *, type: str | None = None, capability: str | None = None) -> Iterable[CatalogEntry]: for entry in self.entries: if type and entry.type != type: continue if capability and capability not in entry.capabilities: continue yield entry def list_all(self) -> List[Dict[str, str]]: return [ {"type": e.type, "name": e.name, "description": e.description, "version": e.version} for e in self.entries ]