This commit is contained in:
2026-01-19 21:25:44 -08:00
parent 37e2ff6651
commit fcab85455d
13 changed files with 820 additions and 393 deletions

View File

@@ -1934,7 +1934,7 @@ class HydrusNetwork(Store):
try:
if service_key:
# Mutate tags for many hashes in a single request
client.mutate_tags_by_key(hash=hashes, service_key=service_key, add_tags=list(tag_tuple))
client.mutate_tags_by_key(hashes=hashes, service_key=service_key, add_tags=list(tag_tuple))
any_success = True
continue
except Exception as exc:

View File

@@ -28,9 +28,7 @@ _DISCOVERED_CLASSES_CACHE: Optional[Dict[str, Type[BaseStore]]] = None
# Backends that failed to initialize earlier in the current process.
# Keyed by (store_type, instance_key) where instance_key is the name used under config.store.<type>.<instance_key>.
_FAILED_BACKEND_CACHE: Dict[tuple[str,
str],
str] = {}
_FAILED_BACKEND_CACHE: Dict[tuple[str, str], str] = {}
def _normalize_store_type(value: str) -> str:
@@ -63,13 +61,10 @@ def _discover_store_classes() -> Dict[str, Type[BaseStore]]:
import Store as store_pkg
discovered: Dict[str,
Type[BaseStore]] = {}
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"}:
if module_name in {"__init__", "_base", "registry"}:
continue
try:
@@ -122,10 +117,7 @@ def _required_keys_for(store_cls: Type[BaseStore]) -> list[str]:
)
def _build_kwargs(store_cls: Type[BaseStore],
instance_name: str,
instance_config: Any) -> Dict[str,
Any]:
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:
@@ -134,13 +126,10 @@ def _build_kwargs(store_cls: Type[BaseStore],
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):
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] = {}
kwargs: Dict[str, Any] = {}
missing: list[str] = []
for key in required:
value = _get_case_insensitive(cfg_dict, key)
@@ -257,8 +246,7 @@ class Store:
# Convenience normalization for filesystem-like paths.
for key in list(kwargs.keys()):
if _normalize_config_key(key) in {"PATH",
"LOCATION"}:
if _normalize_config_key(key) in {"PATH", "LOCATION"}:
kwargs[key] = str(expand_path(kwargs[key]))
backend = store_cls(**kwargs)
@@ -283,8 +271,50 @@ class Store:
f"[Store] Failed to register {store_cls.__name__} instance '{instance_name}': {exc}"
)
def _resolve_backend_name(self,
backend_name: str) -> tuple[Optional[str], Optional[str]]:
def _resolve_backend_name(self, backend_name: str) -> tuple[Optional[str], Optional[str]]:
requested = str(backend_name or "")
if requested in self._backends:
return requested, None
requested_norm = _normalize_store_type(requested)
ci_matches = [
name for name in self._backends
if _normalize_store_type(name) == requested_norm
]
if len(ci_matches) == 1:
return ci_matches[0], None
if len(ci_matches) > 1:
return None, f"Ambiguous store alias '{backend_name}' matches {ci_matches}"
type_matches = [
name for name, store_type in self._backend_types.items()
if store_type == requested_norm
]
if len(type_matches) == 1:
return type_matches[0], None
if len(type_matches) > 1:
return None, (
f"Ambiguous store alias '{backend_name}' matches type '{requested_norm}': {type_matches}"
)
prefix_matches = [
name for name, store_type in self._backend_types.items()
if store_type.startswith(requested_norm)
]
if len(prefix_matches) == 1:
return prefix_matches[0], None
if len(prefix_matches) > 1:
return None, (
f"Ambiguous store alias '{backend_name}' matches type prefix '{requested_norm}': {prefix_matches}"
)
return None, None
# get_backend_instance implementation moved to the bottom of this file to avoid
# instantiating all backends during startup (see function `get_backend_instance`).
def _resolve_backend_name(self, backend_name: str) -> tuple[Optional[str], Optional[str]]:
requested = str(backend_name or "")
if requested in self._backends:
return requested, None
@@ -461,3 +491,85 @@ def list_configured_backend_names(config: Optional[Dict[str, Any]]) -> list[str]
return sorted(set(names))
except Exception:
return []
def get_backend_instance(config: Optional[Dict[str, Any]], backend_name: str, *, suppress_debug: bool = False) -> Optional[BaseStore]:
"""Instantiate and return a single store backend by configured name.
This avoids creating all configured backends (and opening their DBs)
when only a single backend is needed (common in `get-file`/`get-metadata`).
The function first tries a lightweight match against raw config NAME/value to
avoid calling `_build_kwargs` (which can raise if keys are missing).
Returns None when no matching backend is found or instantiation fails.
"""
if not backend_name:
return None
store_cfg = (config or {}).get("store") or {}
if not isinstance(store_cfg, dict):
return None
classes_by_type = _discover_store_classes()
desired = str(backend_name or "").strip().lower()
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:
continue
# Fast path: match using raw 'NAME' or 'name' in config without building full kwargs
for instance_name, instance_cfg in instances.items():
candidate_alias = None
if isinstance(instance_cfg, dict):
candidate_alias = (
instance_cfg.get("NAME") or instance_cfg.get("name")
)
candidate_alias = str(candidate_alias or instance_name).strip()
if candidate_alias.lower() == desired:
try:
kwargs = _build_kwargs(store_cls, str(instance_name), instance_cfg)
except Exception as exc:
if not suppress_debug:
debug(f"[Store] Can't build kwargs for '{instance_name}' ({store_type}): {exc}")
return None
try:
for key in list(kwargs.keys()):
if _normalize_config_key(key) in {"PATH", "LOCATION"}:
kwargs[key] = str(expand_path(kwargs[key]))
except Exception:
pass
try:
backend = store_cls(**kwargs)
return backend
except Exception as exc:
if not suppress_debug:
debug(f"[Store] Failed to instantiate backend '{candidate_alias}': {exc}")
return None
# Fallback: build kwargs for each instance and compare resolved NAME
for instance_name, instance_cfg in instances.items():
try:
kwargs = _build_kwargs(store_cls, str(instance_name), instance_cfg)
except Exception:
continue
alias = str(kwargs.get("NAME") or instance_name).strip()
if alias.lower() != desired:
continue
try:
for key in list(kwargs.keys()):
if _normalize_config_key(key) in {"PATH", "LOCATION"}:
kwargs[key] = str(expand_path(kwargs[key]))
except Exception:
pass
try:
backend = store_cls(**kwargs)
return backend
except Exception as exc:
if not suppress_debug:
debug(f"[Store] Failed to instantiate backend '{alias}': {exc}")
return None
if not suppress_debug:
debug(f"[Store] Backend '{backend_name}' not found in config")
return None