sightradar 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sightradar/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """SightRadar — official Python client for the face recognition API.
2
+
3
+ Quickstart
4
+ ----------
5
+ from sightradar import SightRadar
6
+
7
+ sr = SightRadar(api_key="frs_...") # or set SIGHTRADAR_API_KEY
8
+ sr.create_collection("event-2026")
9
+ sr.index("event-2026", url="https://example.com/group.jpg")
10
+ result = sr.search("event-2026", url="https://example.com/selfie.jpg")
11
+ for m in result.matches:
12
+ print(m.photo_id, m.similarity)
13
+
14
+ The API is Rekognition-compatible in shape but exposed through a small, explicit
15
+ surface here. Every call raises :class:`SightRadarError` on a non-2xx response.
16
+ """
17
+
18
+ from .client import SightRadar
19
+ from .errors import (
20
+ SightRadarError,
21
+ AuthenticationError,
22
+ InsufficientCreditsError,
23
+ NotFoundError,
24
+ RateLimitError,
25
+ )
26
+ from .models import (
27
+ Collection,
28
+ SearchResult,
29
+ Match,
30
+ IndexResult,
31
+ CompareResult,
32
+ DetectResult,
33
+ Wallet,
34
+ )
35
+
36
+ __version__ = "1.0.0"
37
+
38
+ __all__ = [
39
+ "SightRadar",
40
+ "SightRadarError",
41
+ "AuthenticationError",
42
+ "InsufficientCreditsError",
43
+ "NotFoundError",
44
+ "RateLimitError",
45
+ "Collection",
46
+ "SearchResult",
47
+ "Match",
48
+ "IndexResult",
49
+ "CompareResult",
50
+ "DetectResult",
51
+ "Wallet",
52
+ "__version__",
53
+ ]
sightradar/client.py ADDED
@@ -0,0 +1,375 @@
1
+ """The synchronous SightRadar API client.
2
+
3
+ Zero hard dependencies — built on the Python standard library (``urllib``) so it
4
+ installs clean anywhere. File uploads use multipart/form-data; URL/GCS-key inputs
5
+ use JSON.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import io
11
+ import json
12
+ import mimetypes
13
+ import os
14
+ import uuid
15
+ from typing import Any, BinaryIO, Dict, List, Optional, Union
16
+ from urllib import error as urlerror
17
+ from urllib import request as urlrequest
18
+
19
+ from .errors import SightRadarError, error_for_status
20
+ from .models import (
21
+ Collection,
22
+ CompareResult,
23
+ DetectResult,
24
+ IndexResult,
25
+ SearchResult,
26
+ Wallet,
27
+ )
28
+
29
+ __version__ = "1.0.0"
30
+
31
+ DEFAULT_BASE_URL = "https://api.sightradar.com"
32
+
33
+ # A path-or-file the SDK can read bytes from for an upload.
34
+ FileLike = Union[str, bytes, BinaryIO, io.IOBase]
35
+
36
+
37
+ class SightRadar:
38
+ """Client for the SightRadar face recognition API.
39
+
40
+ Args:
41
+ api_key: Your ``frs_<prefix>_<secret>`` key. Falls back to the
42
+ ``SIGHTRADAR_API_KEY`` environment variable.
43
+ base_url: Override the API base (defaults to the production gateway).
44
+ timeout: Per-request timeout in seconds.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ api_key: Optional[str] = None,
50
+ base_url: str = DEFAULT_BASE_URL,
51
+ timeout: float = 30.0,
52
+ ):
53
+ key = api_key or os.environ.get("SIGHTRADAR_API_KEY")
54
+ if not key:
55
+ raise SightRadarError(
56
+ "No API key. Pass api_key=... or set SIGHTRADAR_API_KEY."
57
+ )
58
+ self.api_key = key
59
+ self.base_url = base_url.rstrip("/")
60
+ self.timeout = timeout
61
+
62
+ # -- transport ----------------------------------------------------------
63
+
64
+ def _headers(self) -> Dict[str, str]:
65
+ return {
66
+ "Authorization": f"Bearer {self.api_key}",
67
+ "User-Agent": f"sightradar-python/{__version__}",
68
+ "Accept": "application/json",
69
+ }
70
+
71
+ def _request(
72
+ self,
73
+ method: str,
74
+ path: str,
75
+ *,
76
+ json_body: Optional[Dict[str, Any]] = None,
77
+ raw_body: Optional[bytes] = None,
78
+ content_type: Optional[str] = None,
79
+ query: Optional[Dict[str, Any]] = None,
80
+ ) -> Any:
81
+ url = f"{self.base_url}{path}"
82
+ if query:
83
+ from urllib.parse import urlencode
84
+
85
+ params = {k: v for k, v in query.items() if v is not None}
86
+ if params:
87
+ url = f"{url}?{urlencode(params)}"
88
+
89
+ headers = self._headers()
90
+ data: Optional[bytes] = None
91
+ if json_body is not None:
92
+ data = json.dumps(json_body).encode("utf-8")
93
+ headers["Content-Type"] = "application/json"
94
+ elif raw_body is not None:
95
+ data = raw_body
96
+ if content_type:
97
+ headers["Content-Type"] = content_type
98
+
99
+ req = urlrequest.Request(url, data=data, method=method, headers=headers)
100
+ try:
101
+ with urlrequest.urlopen(req, timeout=self.timeout) as resp:
102
+ body = resp.read()
103
+ return self._parse(resp.status, body)
104
+ except urlerror.HTTPError as e:
105
+ body = e.read()
106
+ self._raise(e.code, body)
107
+ except urlerror.URLError as e: # network / DNS / TLS
108
+ raise SightRadarError(f"request failed: {e.reason}") from e
109
+
110
+ @staticmethod
111
+ def _parse(status: int, body: bytes) -> Any:
112
+ if not body:
113
+ return {}
114
+ try:
115
+ return json.loads(body)
116
+ except json.JSONDecodeError:
117
+ raise SightRadarError(
118
+ f"non-JSON response (status {status})", status
119
+ )
120
+
121
+ @staticmethod
122
+ def _raise(status: int, body: bytes) -> None:
123
+ message = f"request failed ({status})"
124
+ try:
125
+ parsed = json.loads(body)
126
+ if isinstance(parsed, dict) and parsed.get("error"):
127
+ message = str(parsed["error"])
128
+ except (json.JSONDecodeError, ValueError):
129
+ if body:
130
+ message = body.decode("utf-8", "replace")[:300]
131
+ raise error_for_status(status, message)
132
+
133
+ # -- multipart ----------------------------------------------------------
134
+
135
+ def _multipart(
136
+ self, file: FileLike, fields: Optional[Dict[str, Any]] = None
137
+ ) -> tuple[bytes, str]:
138
+ """Build a multipart/form-data body from a file + optional fields."""
139
+ filename, content = _read_file(file)
140
+ boundary = f"----sightradar{uuid.uuid4().hex}"
141
+ ctype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
142
+ buf = io.BytesIO()
143
+
144
+ def w(s: str) -> None:
145
+ buf.write(s.encode("utf-8"))
146
+
147
+ for k, v in (fields or {}).items():
148
+ if v is None:
149
+ continue
150
+ w(f"--{boundary}\r\n")
151
+ w(f'Content-Disposition: form-data; name="{k}"\r\n\r\n')
152
+ w(f"{v}\r\n")
153
+
154
+ w(f"--{boundary}\r\n")
155
+ w(f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n')
156
+ w(f"Content-Type: {ctype}\r\n\r\n")
157
+ buf.write(content)
158
+ w(f"\r\n--{boundary}--\r\n")
159
+ return buf.getvalue(), f"multipart/form-data; boundary={boundary}"
160
+
161
+ # -- collections --------------------------------------------------------
162
+
163
+ def create_collection(self, collection_id: str) -> Collection:
164
+ """Create a collection. Idempotent-ish: a duplicate raises (409)."""
165
+ d = self._request(
166
+ "POST", "/v1/collections", json_body={"collection_id": collection_id}
167
+ )
168
+ return Collection.from_dict(d)
169
+
170
+ def list_collections(
171
+ self, *, q: Optional[str] = None, limit: int = 50, offset: int = 0
172
+ ) -> List[Collection]:
173
+ """List your collections (server-side searchable via ``q``)."""
174
+ d = self._request(
175
+ "GET",
176
+ "/v1/collections",
177
+ query={"q": q, "limit": limit, "offset": offset},
178
+ )
179
+ items = d.get("collections", d) if isinstance(d, dict) else d
180
+ return [Collection.from_dict(c) for c in (items or [])]
181
+
182
+ def describe_collection(self, collection_id: str) -> Collection:
183
+ d = self._request("GET", f"/v1/collections/{collection_id}")
184
+ return Collection.from_dict(d)
185
+
186
+ def delete_collection(self, collection_id: str) -> Dict[str, Any]:
187
+ """Delete a collection and CASCADE every stored face/selfie. Irreversible."""
188
+ return self._request("DELETE", f"/v1/collections/{collection_id}")
189
+
190
+ # -- index / search -----------------------------------------------------
191
+
192
+ def index(
193
+ self,
194
+ collection_id: str,
195
+ *,
196
+ url: Optional[str] = None,
197
+ gcs_key: Optional[str] = None,
198
+ photo_id: Optional[str] = None,
199
+ file: Optional[FileLike] = None,
200
+ ) -> IndexResult:
201
+ """Detect, embed, and store every face in a photo.
202
+
203
+ Provide exactly one image source: ``url``, ``gcs_key``, or ``file``.
204
+ """
205
+ path = f"/v1/collections/{collection_id}/index"
206
+ if file is not None:
207
+ body, ctype = self._multipart(file, {"photoId": photo_id})
208
+ d = self._request("POST", path, raw_body=body, content_type=ctype)
209
+ else:
210
+ d = self._request(
211
+ "POST", path, json_body=_image_body(url, gcs_key, photo_id)
212
+ )
213
+ return IndexResult.from_dict(d)
214
+
215
+ def search(
216
+ self,
217
+ collection_id: str,
218
+ *,
219
+ url: Optional[str] = None,
220
+ gcs_key: Optional[str] = None,
221
+ embedding: Optional[List[float]] = None,
222
+ file: Optional[FileLike] = None,
223
+ threshold: Optional[float] = None,
224
+ limit: Optional[int] = None,
225
+ ) -> SearchResult:
226
+ """Find every stored photo a person appears in, from one selfie.
227
+
228
+ Provide one of ``url``, ``gcs_key``, ``embedding``, or ``file``.
229
+ """
230
+ path = f"/v1/collections/{collection_id}/search"
231
+ if file is not None:
232
+ body, ctype = self._multipart(
233
+ file, {"threshold": threshold, "limit": limit}
234
+ )
235
+ d = self._request("POST", path, raw_body=body, content_type=ctype)
236
+ else:
237
+ payload: Dict[str, Any] = {}
238
+ if embedding is not None:
239
+ payload["embedding"] = embedding
240
+ elif url is not None:
241
+ payload["url"] = url
242
+ elif gcs_key is not None:
243
+ payload["gcsKey"] = gcs_key
244
+ else:
245
+ raise SightRadarError(
246
+ "search needs one of: url, gcs_key, embedding, or file"
247
+ )
248
+ if threshold is not None:
249
+ payload["threshold"] = threshold
250
+ if limit is not None:
251
+ payload["limit"] = limit
252
+ d = self._request("POST", path, json_body=payload)
253
+ return SearchResult.from_dict(d)
254
+
255
+ def search_by_id(
256
+ self,
257
+ collection_id: str,
258
+ point_id: str,
259
+ *,
260
+ threshold: Optional[float] = None,
261
+ limit: Optional[int] = None,
262
+ ) -> SearchResult:
263
+ """Search using a previously-stored selfie point id."""
264
+ payload: Dict[str, Any] = {"id": point_id}
265
+ if threshold is not None:
266
+ payload["threshold"] = threshold
267
+ if limit is not None:
268
+ payload["limit"] = limit
269
+ d = self._request(
270
+ "POST", f"/v1/collections/{collection_id}/search-by-id", json_body=payload
271
+ )
272
+ return SearchResult.from_dict(d)
273
+
274
+ def register_selfie(
275
+ self,
276
+ collection_id: str,
277
+ *,
278
+ url: Optional[str] = None,
279
+ gcs_key: Optional[str] = None,
280
+ file: Optional[FileLike] = None,
281
+ photo_id: Optional[str] = None,
282
+ ) -> Dict[str, Any]:
283
+ """Register a selfie point you can later search by id."""
284
+ path = f"/v1/collections/{collection_id}/selfies"
285
+ if file is not None:
286
+ body, ctype = self._multipart(file, {"photoId": photo_id})
287
+ return self._request("POST", path, raw_body=body, content_type=ctype)
288
+ return self._request("POST", path, json_body=_image_body(url, gcs_key, photo_id))
289
+
290
+ # -- stateless ops ------------------------------------------------------
291
+
292
+ def detect(
293
+ self,
294
+ *,
295
+ url: Optional[str] = None,
296
+ gcs_key: Optional[str] = None,
297
+ file: Optional[FileLike] = None,
298
+ ) -> DetectResult:
299
+ """Locate and quality-gate faces in an image. Nothing is stored."""
300
+ if file is not None:
301
+ body, ctype = self._multipart(file)
302
+ d = self._request("POST", "/v1/detect", raw_body=body, content_type=ctype)
303
+ else:
304
+ d = self._request("POST", "/v1/detect", json_body=_image_body(url, gcs_key))
305
+ return DetectResult.from_dict(d)
306
+
307
+ def compare(
308
+ self,
309
+ *,
310
+ source_url: Optional[str] = None,
311
+ target_url: Optional[str] = None,
312
+ source_gcs_key: Optional[str] = None,
313
+ target_gcs_key: Optional[str] = None,
314
+ source_embedding: Optional[List[float]] = None,
315
+ target_embedding: Optional[List[float]] = None,
316
+ ) -> CompareResult:
317
+ """1:1 similarity / verification between two faces. Nothing is stored."""
318
+ payload: Dict[str, Any] = {}
319
+ if source_url:
320
+ payload["sourceUrl"] = source_url
321
+ if target_url:
322
+ payload["targetUrl"] = target_url
323
+ if source_gcs_key:
324
+ payload["sourceGcsKey"] = source_gcs_key
325
+ if target_gcs_key:
326
+ payload["targetGcsKey"] = target_gcs_key
327
+ if source_embedding is not None:
328
+ payload["source_embedding"] = source_embedding
329
+ if target_embedding is not None:
330
+ payload["target_embedding"] = target_embedding
331
+ d = self._request("POST", "/v1/compare", json_body=payload)
332
+ return CompareResult.from_dict(d)
333
+
334
+ # -- account ------------------------------------------------------------
335
+
336
+ def wallet(self) -> Wallet:
337
+ """Get the current credit balance."""
338
+ return Wallet.from_dict(self._request("GET", "/v1/wallet"))
339
+
340
+ def usage(self, days: int = 30) -> Dict[str, Any]:
341
+ """Usage report aggregated from the ledger."""
342
+ return self._request("GET", "/v1/usage", query={"days": days})
343
+
344
+
345
+ # -- helpers ----------------------------------------------------------------
346
+
347
+
348
+ def _image_body(
349
+ url: Optional[str], gcs_key: Optional[str], photo_id: Optional[str] = None
350
+ ) -> Dict[str, Any]:
351
+ body: Dict[str, Any] = {}
352
+ if url:
353
+ body["url"] = url
354
+ elif gcs_key:
355
+ body["gcsKey"] = gcs_key
356
+ else:
357
+ raise SightRadarError("provide one of: url, gcs_key, or file")
358
+ if photo_id:
359
+ body["photoId"] = photo_id
360
+ return body
361
+
362
+
363
+ def _read_file(file: FileLike) -> tuple[str, bytes]:
364
+ """Resolve a path / bytes / file-object into (filename, content_bytes)."""
365
+ if isinstance(file, str):
366
+ with open(file, "rb") as fh:
367
+ return os.path.basename(file), fh.read()
368
+ if isinstance(file, (bytes, bytearray)):
369
+ return "upload.jpg", bytes(file)
370
+ # file-like object
371
+ name = getattr(file, "name", "upload.jpg")
372
+ content = file.read()
373
+ if isinstance(content, str):
374
+ content = content.encode("utf-8")
375
+ return os.path.basename(str(name)), content
sightradar/errors.py ADDED
@@ -0,0 +1,53 @@
1
+ """Exception types raised by the SightRadar client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class SightRadarError(Exception):
9
+ """Base error for all non-2xx API responses and transport failures.
10
+
11
+ Attributes:
12
+ message: Human-readable error message (from the API ``error`` field when present).
13
+ status_code: HTTP status code, or ``None`` for transport-level failures.
14
+ """
15
+
16
+ def __init__(self, message: str, status_code: Optional[int] = None):
17
+ super().__init__(message)
18
+ self.message = message
19
+ self.status_code = status_code
20
+
21
+ def __str__(self) -> str: # pragma: no cover - trivial
22
+ if self.status_code is not None:
23
+ return f"[{self.status_code}] {self.message}"
24
+ return self.message
25
+
26
+
27
+ class AuthenticationError(SightRadarError):
28
+ """Raised on 401 — the API key is missing, malformed, or revoked."""
29
+
30
+
31
+ class InsufficientCreditsError(SightRadarError):
32
+ """Raised on 402 — the wallet does not have enough credits for the operation."""
33
+
34
+
35
+ class NotFoundError(SightRadarError):
36
+ """Raised on 404 — the collection, key, or resource does not exist."""
37
+
38
+
39
+ class RateLimitError(SightRadarError):
40
+ """Raised on 429 — too many requests; back off and retry."""
41
+
42
+
43
+ def error_for_status(status_code: int, message: str) -> SightRadarError:
44
+ """Map an HTTP status to the most specific error subclass."""
45
+ if status_code == 401:
46
+ return AuthenticationError(message, status_code)
47
+ if status_code == 402:
48
+ return InsufficientCreditsError(message, status_code)
49
+ if status_code == 404:
50
+ return NotFoundError(message, status_code)
51
+ if status_code == 429:
52
+ return RateLimitError(message, status_code)
53
+ return SightRadarError(message, status_code)
sightradar/models.py ADDED
@@ -0,0 +1,148 @@
1
+ """Typed response models.
2
+
3
+ These are lightweight dataclasses built from the JSON the API returns. Each has
4
+ a ``from_dict`` constructor that is tolerant of unknown/extra fields (forward
5
+ compatible) and exposes the raw payload via ``.raw`` for anything not modelled.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, List, Optional
12
+
13
+
14
+ @dataclass
15
+ class Collection:
16
+ collection_id: str
17
+ status: str
18
+ photo_count: int = 0
19
+ face_count: int = 0
20
+ selfie_count: int = 0
21
+ created_at: Optional[str] = None
22
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
23
+
24
+ @classmethod
25
+ def from_dict(cls, d: Dict[str, Any]) -> "Collection":
26
+ return cls(
27
+ collection_id=d.get("collection_id", ""),
28
+ status=d.get("status", ""),
29
+ photo_count=d.get("photo_count", 0) or 0,
30
+ face_count=d.get("face_count", 0) or 0,
31
+ selfie_count=d.get("selfie_count", 0) or 0,
32
+ created_at=d.get("created_at"),
33
+ raw=d,
34
+ )
35
+
36
+
37
+ @dataclass
38
+ class Match:
39
+ photo_id: Optional[str] = None
40
+ similarity: Optional[float] = None
41
+ point_id: Optional[str] = None
42
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
43
+
44
+ @classmethod
45
+ def from_dict(cls, d: Dict[str, Any]) -> "Match":
46
+ return cls(
47
+ photo_id=d.get("photo_id") or d.get("photoId"),
48
+ similarity=d.get("similarity"),
49
+ point_id=d.get("point_id") or d.get("id"),
50
+ raw=d,
51
+ )
52
+
53
+
54
+ @dataclass
55
+ class SearchResult:
56
+ collection_id: str = ""
57
+ matches: List[Match] = field(default_factory=list)
58
+ photo_ids: List[str] = field(default_factory=list)
59
+ reason: Optional[str] = None
60
+ model_version: Optional[str] = None
61
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
62
+
63
+ @property
64
+ def found(self) -> bool:
65
+ """True when at least one match was returned."""
66
+ return len(self.matches) > 0
67
+
68
+ @classmethod
69
+ def from_dict(cls, d: Dict[str, Any]) -> "SearchResult":
70
+ return cls(
71
+ collection_id=d.get("collection_id", ""),
72
+ matches=[Match.from_dict(m) for m in d.get("matches", []) or []],
73
+ photo_ids=list(d.get("photo_ids", []) or []),
74
+ reason=d.get("reason"),
75
+ model_version=d.get("model_version"),
76
+ raw=d,
77
+ )
78
+
79
+
80
+ @dataclass
81
+ class IndexResult:
82
+ collection_id: str = ""
83
+ photo_id: Optional[str] = None
84
+ indexed: int = 0
85
+ detected_face_count: int = 0
86
+ rejected_face_count: int = 0
87
+ faces: List[Dict[str, Any]] = field(default_factory=list)
88
+ model_version: Optional[str] = None
89
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
90
+
91
+ @classmethod
92
+ def from_dict(cls, d: Dict[str, Any]) -> "IndexResult":
93
+ return cls(
94
+ collection_id=d.get("collection_id", ""),
95
+ photo_id=d.get("photo_id") or d.get("photoId"),
96
+ indexed=d.get("indexed", 0) or 0,
97
+ detected_face_count=d.get("detected_face_count", 0) or 0,
98
+ rejected_face_count=d.get("rejected_face_count", 0) or 0,
99
+ faces=list(d.get("faces", []) or []),
100
+ model_version=d.get("model_version"),
101
+ raw=d,
102
+ )
103
+
104
+
105
+ @dataclass
106
+ class CompareResult:
107
+ face_found: bool = False
108
+ similarity: Optional[float] = None
109
+ match: bool = False
110
+ threshold: Optional[float] = None
111
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
112
+
113
+ @classmethod
114
+ def from_dict(cls, d: Dict[str, Any]) -> "CompareResult":
115
+ return cls(
116
+ face_found=bool(d.get("face_found", False)),
117
+ similarity=d.get("similarity"),
118
+ match=bool(d.get("match", False)),
119
+ threshold=d.get("threshold"),
120
+ raw=d,
121
+ )
122
+
123
+
124
+ @dataclass
125
+ class DetectResult:
126
+ detected_face_count: int = 0
127
+ gated_face_count: int = 0
128
+ faces: List[Dict[str, Any]] = field(default_factory=list)
129
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
130
+
131
+ @classmethod
132
+ def from_dict(cls, d: Dict[str, Any]) -> "DetectResult":
133
+ return cls(
134
+ detected_face_count=d.get("detected_face_count", 0) or 0,
135
+ gated_face_count=d.get("gated_face_count", 0) or 0,
136
+ faces=list(d.get("faces", []) or []),
137
+ raw=d,
138
+ )
139
+
140
+
141
+ @dataclass
142
+ class Wallet:
143
+ balance_credits: int = 0
144
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False)
145
+
146
+ @classmethod
147
+ def from_dict(cls, d: Dict[str, Any]) -> "Wallet":
148
+ return cls(balance_credits=d.get("balance_credits", 0) or 0, raw=d)
sightradar/py.typed ADDED
File without changes
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: sightradar
3
+ Version: 1.0.0
4
+ Summary: Official Python client for the SightRadar face recognition API
5
+ Project-URL: Homepage, https://sightradar.com
6
+ Project-URL: Documentation, https://sightradar.com/docs
7
+ Project-URL: Source, https://github.com/sightradar-hq/sightradar-python
8
+ Author: SightRadar
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,face recognition,facial recognition,rekognition,sightradar
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+
20
+ # SightRadar — Python client
21
+
22
+ Official Python client for the [SightRadar](https://sightradar.com) face
23
+ recognition API. Zero runtime dependencies (built on the standard library).
24
+
25
+ ```bash
26
+ pip install sightradar
27
+ ```
28
+
29
+ ## Authenticate
30
+
31
+ Create an API key in the [console](https://sightradar.com/login), then pass it
32
+ directly or via the `SIGHTRADAR_API_KEY` environment variable.
33
+
34
+ ```python
35
+ from sightradar import SightRadar
36
+
37
+ sr = SightRadar(api_key="frs_...") # or: SightRadar() with SIGHTRADAR_API_KEY set
38
+ ```
39
+
40
+ ## Core workflow
41
+
42
+ ```python
43
+ # 1. Create a collection to hold faces.
44
+ sr.create_collection("event-2026")
45
+
46
+ # 2. Index faces from photos (URL, GCS key, or a local file).
47
+ sr.index("event-2026", url="https://example.com/group.jpg")
48
+ sr.index("event-2026", file="/path/to/photo.jpg", photo_id="img-42")
49
+
50
+ # 3. Search the collection with one selfie.
51
+ result = sr.search("event-2026", url="https://example.com/selfie.jpg")
52
+ if result.found:
53
+ for m in result.matches:
54
+ print(m.photo_id, round(m.similarity, 3))
55
+ else:
56
+ print("no match:", result.reason)
57
+ ```
58
+
59
+ ## Stateless operations (nothing stored)
60
+
61
+ ```python
62
+ # Detect + quality-gate faces in an image.
63
+ det = sr.detect(url="https://example.com/photo.jpg")
64
+ print(det.detected_face_count, det.gated_face_count)
65
+
66
+ # 1:1 verification between two faces.
67
+ cmp = sr.compare(
68
+ source_url="https://example.com/a.jpg",
69
+ target_url="https://example.com/b.jpg",
70
+ )
71
+ print(cmp.match, cmp.similarity)
72
+ ```
73
+
74
+ ## Account
75
+
76
+ ```python
77
+ print(sr.wallet().balance_credits)
78
+ print(sr.usage(days=30))
79
+ ```
80
+
81
+ ## Errors
82
+
83
+ Every non-2xx response raises a typed exception:
84
+
85
+ ```python
86
+ from sightradar import (
87
+ SightRadarError, # base
88
+ AuthenticationError, # 401
89
+ InsufficientCreditsError, # 402
90
+ NotFoundError, # 404
91
+ RateLimitError, # 429
92
+ )
93
+
94
+ try:
95
+ sr.describe_collection("missing")
96
+ except NotFoundError as e:
97
+ print(e.status_code, e.message)
98
+ ```
99
+
100
+ ## Image inputs
101
+
102
+ Index / search / detect / register-selfie accept exactly one image source:
103
+
104
+ - `url=` — a public image URL
105
+ - `gcs_key=` — a Google Cloud Storage object key
106
+ - `file=` — a local path, `bytes`, or a file-like object (uploaded as multipart)
107
+
108
+ `search` additionally accepts `embedding=` (a 512-d vector).
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,9 @@
1
+ sightradar/__init__.py,sha256=KOwdxOLCq5Q0KvAr325tT7g3ORsjU-OSOhoF7xaXd0s,1253
2
+ sightradar/client.py,sha256=boR_ZxY9zEQYn2YPCFQlCiSgYDUVHGlFthGH4i38PcI,13286
3
+ sightradar/errors.py,sha256=3BUj8Si_ENAXyJBbkOXVfSaCenDTVo3QkrMg9-Hcujg,1795
4
+ sightradar/models.py,sha256=v3C1ITyb4KoDjM5OHhUz-OpsTwRlzJfgoZF0MmkVeEA,4632
5
+ sightradar/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ sightradar-1.0.0.dist-info/METADATA,sha256=1mfgAVTIMaL2WpIkEEMykk-gkjukN0Buf1zrnL1ZErE,3035
7
+ sightradar-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ sightradar-1.0.0.dist-info/licenses/LICENSE,sha256=tCowXdl71UVVm09mWiV2AvMoSsB_KcR-HZ4fxDsJCCs,1099
9
+ sightradar-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SightRadar (THIRDACT LABS PRIVATE LIMITED)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.