hh
This commit is contained in:
@@ -3,31 +3,107 @@
|
||||
Concrete store implementations live in the `Store/` package.
|
||||
This module is the single source of truth for store discovery.
|
||||
|
||||
Config schema (canonical):
|
||||
|
||||
{
|
||||
"store": {
|
||||
"folder": {
|
||||
"default": {"path": "C:/Media"},
|
||||
"test": {"path": "C:/Temp"}
|
||||
},
|
||||
"hydrusnetwork": {
|
||||
"home": {"Hydrus-Client-API-Access-Key": "...", "url": "http://..."}
|
||||
}
|
||||
}
|
||||
}
|
||||
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).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Iterable, Optional, Type
|
||||
|
||||
from SYS.logger import debug
|
||||
|
||||
from Store._base import Store as BaseStore
|
||||
from Store.Folder import Folder
|
||||
from Store.HydrusNetwork import HydrusNetwork
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Store:
|
||||
@@ -42,43 +118,36 @@ class Store:
|
||||
if not isinstance(store_cfg, dict):
|
||||
store_cfg = {}
|
||||
|
||||
folder_cfg = store_cfg.get("folder")
|
||||
if isinstance(folder_cfg, dict):
|
||||
for name, value in folder_cfg.items():
|
||||
path_val: Optional[str]
|
||||
if isinstance(value, dict):
|
||||
path_val = value.get("path")
|
||||
elif isinstance(value, (str, bytes)):
|
||||
path_val = str(value)
|
||||
else:
|
||||
path_val = None
|
||||
classes_by_type = _discover_store_classes()
|
||||
for raw_store_type, instances in store_cfg.items():
|
||||
if not isinstance(instances, dict):
|
||||
continue
|
||||
|
||||
if not path_val:
|
||||
continue
|
||||
|
||||
location = str(Path(str(path_val)).expanduser())
|
||||
self._backends[str(name)] = Folder(location=location, name=str(name))
|
||||
|
||||
hydrus_cfg = store_cfg.get("hydrusnetwork")
|
||||
if isinstance(hydrus_cfg, dict):
|
||||
for instance_name, instance_config in hydrus_cfg.items():
|
||||
if not isinstance(instance_config, dict):
|
||||
continue
|
||||
|
||||
api_key = instance_config.get("Hydrus-Client-API-Access-Key")
|
||||
url = instance_config.get("url")
|
||||
if not api_key or not url:
|
||||
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
|
||||
|
||||
for instance_name, instance_config in instances.items():
|
||||
try:
|
||||
self._backends[str(instance_name)] = HydrusNetwork(
|
||||
instance_name=str(instance_name),
|
||||
api_key=str(api_key),
|
||||
url=str(url),
|
||||
)
|
||||
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
|
||||
except Exception as exc:
|
||||
if not self._suppress_debug:
|
||||
debug(f"[Store] Failed to register Hydrus instance '{instance_name}': {exc}")
|
||||
debug(
|
||||
f"[Store] Failed to register {store_cls.__name__} instance '{instance_name}': {exc}"
|
||||
)
|
||||
|
||||
def list_backends(self) -> list[str]:
|
||||
return sorted(self._backends.keys())
|
||||
|
||||
Reference in New Issue
Block a user