Files
Medios-Macina/Store/registry.py

169 lines
5.9 KiB
Python
Raw Normal View History

2025-12-11 19:04:02 -08:00
"""Store registry.
Concrete store implementations live in the `Store/` package.
This module is the single source of truth for store discovery.
2025-12-13 00:18:30 -08:00
This registry is config-driven:
- Each store subtype (e.g. `hydrusnetwork`) maps to a concrete store class.
- Each store class advertises its required config keys via `StoreClass.__new__.keys`.
- Instances are created from config using those keys (case-insensitive lookup).
2025-12-11 19:04:02 -08:00
"""
from __future__ import annotations
2025-12-13 00:18:30 -08:00
import importlib
import inspect
import pkgutil
2025-12-11 19:04:02 -08:00
from pathlib import Path
2025-12-13 00:18:30 -08:00
from typing import Any, Dict, Iterable, Optional, Type
2025-12-11 19:04:02 -08:00
from SYS.logger import debug
2025-12-11 23:21:45 -08:00
from Store._base import Store as BaseStore
2025-12-13 00:18:30 -08:00
def _normalize_store_type(value: str) -> str:
return "".join(ch.lower() for ch in str(value or "") if ch.isalnum())
def _normalize_config_key(value: str) -> str:
return str(value or "").strip().upper()
def _get_case_insensitive(mapping: Dict[str, Any], key: str) -> Any:
if key in mapping:
return mapping[key]
desired = _normalize_config_key(key)
for k, v in mapping.items():
if _normalize_config_key(k) == desired:
return v
return None
def _discover_store_classes() -> Dict[str, Type[BaseStore]]:
"""Discover store classes from the Store package.
Convention:
- The store type key is the normalized class name (e.g. HydrusNetwork -> hydrusnetwork).
"""
import Store as store_pkg
discovered: Dict[str, Type[BaseStore]] = {}
for module_info in pkgutil.iter_modules(store_pkg.__path__):
module_name = module_info.name
if module_name in {"__init__", "_base", "registry"}:
continue
module = importlib.import_module(f"Store.{module_name}")
for _, obj in vars(module).items():
if not inspect.isclass(obj):
continue
if obj is BaseStore:
continue
if not issubclass(obj, BaseStore):
continue
discovered[_normalize_store_type(obj.__name__)] = obj
return discovered
def _required_keys_for(store_cls: Type[BaseStore]) -> list[str]:
keys = getattr(store_cls.__new__, "keys", None)
if keys is None:
return []
if isinstance(keys, dict):
return [str(k) for k in keys.keys()]
if isinstance(keys, (list, tuple, set, frozenset)):
return [str(k) for k in keys]
if isinstance(keys, str):
return [keys]
raise TypeError(f"Unsupported __new__.keys type for {store_cls.__name__}: {type(keys)}")
def _build_kwargs(store_cls: Type[BaseStore], instance_name: str, instance_config: Any) -> Dict[str, Any]:
if isinstance(instance_config, dict):
cfg_dict = dict(instance_config)
else:
cfg_dict = {}
required = _required_keys_for(store_cls)
# If NAME is required but not present, allow the instance key to provide it.
if any(_normalize_config_key(k) == "NAME" for k in required) and _get_case_insensitive(cfg_dict, "NAME") is None:
cfg_dict["NAME"] = str(instance_name)
kwargs: Dict[str, Any] = {}
missing: list[str] = []
for key in required:
value = _get_case_insensitive(cfg_dict, key)
if value is None or value == "":
missing.append(str(key))
continue
kwargs[str(key)] = value
if missing:
raise ValueError(f"Missing required keys for {store_cls.__name__}: {', '.join(missing)}")
return kwargs
2025-12-11 19:04:02 -08:00
class Store:
def __init__(self, config: Optional[Dict[str, Any]] = None, suppress_debug: bool = False) -> None:
self._config = config or {}
self._suppress_debug = suppress_debug
2025-12-11 23:21:45 -08:00
self._backends: Dict[str, BaseStore] = {}
2025-12-11 19:04:02 -08:00
self._load_backends()
def _load_backends(self) -> None:
store_cfg = self._config.get("store")
if not isinstance(store_cfg, dict):
store_cfg = {}
2025-12-13 00:18:30 -08:00
classes_by_type = _discover_store_classes()
for raw_store_type, instances in store_cfg.items():
if not isinstance(instances, dict):
continue
store_type = _normalize_store_type(str(raw_store_type))
store_cls = classes_by_type.get(store_type)
if store_cls is None:
if not self._suppress_debug:
debug(f"[Store] Unknown store type '{raw_store_type}'")
continue
2025-12-11 19:04:02 -08:00
2025-12-13 00:18:30 -08:00
for instance_name, instance_config in instances.items():
2025-12-11 19:04:02 -08:00
try:
2025-12-13 00:18:30 -08:00
kwargs = _build_kwargs(store_cls, str(instance_name), instance_config)
# Convenience normalization for filesystem-like paths.
for key in list(kwargs.keys()):
if _normalize_config_key(key) in {"PATH", "LOCATION"}:
kwargs[key] = str(Path(str(kwargs[key])).expanduser())
backend = store_cls(**kwargs)
backend_name = str(kwargs.get("NAME") or instance_name)
self._backends[backend_name] = backend
2025-12-11 19:04:02 -08:00
except Exception as exc:
if not self._suppress_debug:
2025-12-13 00:18:30 -08:00
debug(
f"[Store] Failed to register {store_cls.__name__} instance '{instance_name}': {exc}"
)
2025-12-11 19:04:02 -08:00
def list_backends(self) -> list[str]:
return sorted(self._backends.keys())
def list_searchable_backends(self) -> list[str]:
searchable: list[str] = []
for name, backend in self._backends.items():
2025-12-11 23:21:45 -08:00
if type(backend).search is not BaseStore.search:
2025-12-11 19:04:02 -08:00
searchable.append(name)
return sorted(searchable)
2025-12-11 23:21:45 -08:00
def __getitem__(self, backend_name: str) -> BaseStore:
2025-12-11 19:04:02 -08:00
if backend_name not in self._backends:
raise KeyError(f"Unknown store backend: {backend_name}. Available: {list(self._backends.keys())}")
return self._backends[backend_name]
def is_available(self, backend_name: str) -> bool:
return backend_name in self._backends