| """ |
| 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 |
|
|
|
|
| |
| |
| |
| @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, |
| } |
|
|
|
|
| |
| |
| |
| @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: ... |
|
|
|
|
| |
| |
| |
| 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: |
| |
| self._settings = settings |
|
|
| def is_available(self) -> bool: |
| return True |
|
|
| def execute(self, input_data: Any) -> ProviderResult: |
| return self._safe_execute(input_data) |
|
|
| |
| |
| |
| 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__, |
| ) |
|
|