File size: 5,652 Bytes
bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf aac350d bbc3fdf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | """
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__,
)
|