"""Registry and factory for daily derived-data plugins.""" import importlib import importlib.util import inspect from pathlib import Path from typing import Optional, Type from pipeline.derived.base import BaseDerivedData _REGISTRY: dict[str, Type[BaseDerivedData]] = {} _builtins_loaded = False def register_derived(cls: Type[BaseDerivedData]) -> Type[BaseDerivedData]: """Class decorator that registers derived data under ``BaseDerivedData.name``.""" if not (isinstance(cls, type) and issubclass(cls, BaseDerivedData)): raise TypeError(f"{cls!r} is not a BaseDerivedData subclass") key = getattr(cls, "name", "") if not key: raise ValueError(f"{cls.__name__} must set a non-empty class attribute `name`") existing = _REGISTRY.get(key) if existing is not None and existing is not cls: raise ValueError( f"Derived data name '{key}' already registered by {existing.__name__}" ) _REGISTRY[key] = cls return cls def available_derived() -> list[str]: """Sorted names of all registered derived-data plugins.""" _ensure_builtins() return sorted(_REGISTRY) def get_derived(name: str, **params) -> BaseDerivedData: """Instantiate a registered derived-data plugin by name. Only parameters accepted by the plugin class's ``__init__`` are forwarded. """ _ensure_builtins() if name not in _REGISTRY: raise KeyError(f"Unknown derived data '{name}'. Available: {sorted(_REGISTRY)}") cls = _REGISTRY[name] accepted = _accepted_params(cls) kwargs = params if accepted is None else {k: v for k, v in params.items() if k in accepted} return cls(**kwargs) def load_derived_module(spec: str) -> None: """Import an external module so its ``@register_derived`` classes register.""" looks_like_file = spec.endswith(".py") or Path(spec).expanduser().exists() if looks_like_file: path = Path(spec).expanduser().resolve() if not path.exists(): raise FileNotFoundError(f"Derived data module not found: {path}") module_spec = importlib.util.spec_from_file_location(path.stem, path) if module_spec is None or module_spec.loader is None: raise ImportError(f"Cannot load derived data module from {path}") module = importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module) else: importlib.import_module(spec) def _accepted_params(cls: Type[BaseDerivedData]) -> Optional[set[str]]: """Param names ``cls.__init__`` accepts, or None if it takes ``**kwargs``.""" sig = inspect.signature(cls.__init__) if any(p.kind is p.VAR_KEYWORD for p in sig.parameters.values()): return None return {name for name in sig.parameters if name != "self"} def _ensure_builtins() -> None: global _builtins_loaded if not _builtins_loaded: import pipeline.derived.library # noqa: F401 _builtins_loaded = True