""" Provider Framework — the contract every provider must satisfy. After the refactor, this module contains ONLY the Protocol, BaseProvider helper, and ProviderResult dataclass. The registry itself has moved to `providers/registry.py` so it can be instantiated and injected. Key design decisions: 1. **Provider Protocol, not ABC** — structural typing, no inheritance ceremony. A provider is any object with the right methods. 2. **Completely isolated** — a provider only imports its own deps, never imports from siblings. A failing optional dep (e.g. `insightface` missing) must not affect other providers. 3. **ProviderResult** carries the raw response (preserved verbatim), the normalized fields, the latency, and any error. Nothing is discarded. 4. **Capability flags** declare what a provider can do so the orchestrator can route queries correctly. """ from __future__ import annotations import time from dataclasses import dataclass, field from typing import Any, Optional, Protocol, runtime_checkable from models.providers import ProviderCapability, ProviderStatus # --------------------------------------------------------------------------- # # Result object # --------------------------------------------------------------------------- # @dataclass class ProviderResult: """ The contract returned by every provider. `raw` is preserved verbatim for evidence/audit. `normalized` is provider-specific cleaned data (boxes, encodings, image URLs, etc.). """ provider: str capability: ProviderCapability success: bool elapsed_ms: float raw: Any = None normalized: dict = field(default_factory=dict) error: Optional[str] = None error_type: Optional[str] = None metadata: dict = field(default_factory=dict) retry_count: int = 0 def to_dict(self) -> dict: return { "provider": self.provider, "capability": self.capability.value, "success": self.success, "elapsed_ms": self.elapsed_ms, "raw": self.raw, "normalized": self.normalized, "error": self.error, "error_type": self.error_type, "metadata": self.metadata, "retry_count": self.retry_count, } # --------------------------------------------------------------------------- # # Provider Protocol # --------------------------------------------------------------------------- # @runtime_checkable class Provider(Protocol): """ Structural contract for every provider. A provider class must define: - name: str - capability: ProviderCapability - is_available() -> bool - execute(input_data: Any) -> ProviderResult """ name: str capability: ProviderCapability def is_available(self) -> bool: ... def execute(self, input_data: Any) -> ProviderResult: ... # --------------------------------------------------------------------------- # # BaseProvider — optional convenience class # --------------------------------------------------------------------------- # class BaseProvider: """Convenience base with timing + error handling boilerplate. Subclasses implement `_run(pipeline_output) -> (raw, normalized)`. The orchestrator ALWAYS passes a `pipeline.feature_extraction.PipelineOutput` object. Providers extract what they need: - detection providers → pipeline_output.image - recognition providers → pipeline_output.image + pipeline_output.face_crops + pipeline_output.gallery - scraper providers → pipeline_output.scrape_url (or .image for reverse search) - reverse-search provs → pipeline_output.image """ name: str = "base" capability: ProviderCapability = ProviderCapability.DETECTION def __init__(self, settings=None) -> None: # settings is optional; providers that need tuning knobs store it. self._settings = settings def is_available(self) -> bool: return True def execute(self, input_data: Any) -> ProviderResult: return self._safe_execute(input_data) # ------------------------------------------------------------------ # # Subclass hook # ------------------------------------------------------------------ # def _run(self, input_data: Any) -> tuple[Any, dict]: raise NotImplementedError def _safe_execute(self, input_data: Any) -> ProviderResult: t0 = time.perf_counter() try: if not self.is_available(): return ProviderResult( provider=self.name, capability=self.capability, success=False, elapsed_ms=0.0, error="Provider not available (disabled or missing dependencies)", error_type="NotConfigured", ) raw, normalized = self._run(input_data) elapsed = (time.perf_counter() - t0) * 1000.0 return ProviderResult( provider=self.name, capability=self.capability, success=True, elapsed_ms=round(elapsed, 3), raw=raw, normalized=normalized, ) except Exception as e: elapsed = (time.perf_counter() - t0) * 1000.0 return ProviderResult( provider=self.name, capability=self.capability, success=False, elapsed_ms=round(elapsed, 3), error=str(e), error_type=type(e).__name__, )