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__,
            )