● Zion Boggan
repos/Sightline/backend/app/services/connectors.py
zionboggan.com ↗
78 lines · python
History for this file →
1
"""API connector framework. A connector pulls reads from an external ALPR system on demand
2
(or on a schedule) and pushes them through the same ingest pipeline as file uploads. New
3
connectors implement `fetch()` -> list[read dict] and register in CONNECTORS."""
4
from __future__ import annotations
5
 
6
from abc import ABC, abstractmethod
7
 
8
try:
9
    import httpx
10
except Exception:
11
    httpx = None
12
 
13
 
14
class BaseConnector(ABC):
15
    name: str = "base"
16
    description: str = ""
17
 
18
    @abstractmethod
19
    def fetch(self, config: dict) -> list[dict]:
20
        """Return a list of raw read dicts (same shape a JSON export row would have)."""
21
        ...
22
 
23
 
24
class GenericRestConnector(BaseConnector):
25
    """Polls a JSON HTTP endpoint and extracts an array of read objects. Config:
26
        { "url": "...", "auth_header": "Bearer ...", "records_path": "reads" }
27
    `records_path` is the key holding the list (supports a top-level list too)."""
28
    name = "generic_rest"
29
    description = "Poll a JSON HTTP endpoint returning ALPR reads."
30
 
31
    def fetch(self, config: dict) -> list[dict]:
32
        if httpx is None:
33
            raise RuntimeError("httpx not installed")
34
        url = config.get("url")
35
        if not url:
36
            raise ValueError("connector config requires 'url'")
37
        headers = {}
38
        if config.get("auth_header"):
39
            headers["Authorization"] = config["auth_header"]
40
        resp = httpx.get(url, headers=headers, timeout=config.get("timeout", 15.0),
41
                         follow_redirects=True)
42
        resp.raise_for_status()
43
        doc = resp.json()
44
        if isinstance(doc, list):
45
            return doc
46
        path = config.get("records_path")
47
        if path and isinstance(doc.get(path), list):
48
            return doc[path]
49
        for k in ("reads", "data", "results", "records", "items"):
50
            if isinstance(doc.get(k), list):
51
                return doc[k]
52
        raise ValueError("could not locate a list of reads in the response")
53
 
54
 
55
class WebhookConnector(BaseConnector):
56
    """Accepts a pre-fetched payload pushed by an external system (config['payload'] is the
57
    list of reads). Lets a vendor webhook hand reads straight to the pipeline."""
58
    name = "webhook"
59
    description = "Accept a pushed batch of reads (vendor webhook)."
60
 
61
    def fetch(self, config: dict) -> list[dict]:
62
        payload = config.get("payload")
63
        if not isinstance(payload, list):
64
            raise ValueError("webhook connector requires config['payload'] as a list")
65
        return payload
66
 
67
 
68
CONNECTORS: dict[str, BaseConnector] = {
69
    c.name: c for c in [GenericRestConnector(), WebhookConnector()]
70
}
71
 
72
 
73
def list_connectors() -> list[dict]:
74
    return [{"name": c.name, "description": c.description} for c in CONNECTORS.values()]
75
 
76
 
77
def get_connector(name: str) -> BaseConnector | None:
78
    return CONNECTORS.get(name)