authentasdk 0.2.3__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.
- authenta/__init__.py +21 -0
- authenta/async_authenta_client.py +340 -0
- authenta/authenta_client.py +379 -0
- authenta/authenta_exceptions.py +74 -0
- authenta/face_intelligence.py +131 -0
- authenta/visualization.py +470 -0
- authentasdk-0.2.3.dist-info/METADATA +903 -0
- authentasdk-0.2.3.dist-info/RECORD +10 -0
- authentasdk-0.2.3.dist-info/WHEEL +5 -0
- authentasdk-0.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
example usage:
|
|
3
|
+
client = AuthentaClient(
|
|
4
|
+
base_url="https://platform.authenta.ai",
|
|
5
|
+
client_id="...",
|
|
6
|
+
client_secret="...",
|
|
7
|
+
)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
import mimetypes
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
from .authenta_exceptions import (
|
|
17
|
+
AuthentaError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
AuthorizationError,
|
|
20
|
+
QuotaExceededError,
|
|
21
|
+
InsufficientCreditsError,
|
|
22
|
+
ValidationError,
|
|
23
|
+
ServerError,
|
|
24
|
+
)
|
|
25
|
+
from curl_cffi import requests as cffi_requests
|
|
26
|
+
|
|
27
|
+
def _raise_for_authenta_error(resp: requests.Response) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Map an Authenta API error response to a rich SDK exception.
|
|
30
|
+
|
|
31
|
+
Expects JSON like: {"code": "IAM001", "type": "...", "message": "..."}.
|
|
32
|
+
Falls back to HTTP-based mapping if the body is not JSON.
|
|
33
|
+
"""
|
|
34
|
+
status = resp.status_code
|
|
35
|
+
try:
|
|
36
|
+
data = resp.json()
|
|
37
|
+
except ValueError:
|
|
38
|
+
# Non-JSON error body
|
|
39
|
+
if 400 <= status < 500:
|
|
40
|
+
raise ValidationError(
|
|
41
|
+
message=resp.text or "Client error",
|
|
42
|
+
status_code=status,
|
|
43
|
+
)
|
|
44
|
+
if status >= 500:
|
|
45
|
+
raise ServerError(
|
|
46
|
+
message=resp.text or "Server error",
|
|
47
|
+
status_code=status,
|
|
48
|
+
)
|
|
49
|
+
resp.raise_for_status()
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
code = data.get("code") or "unknown"
|
|
53
|
+
message = data.get("message") or resp.reason or "Unknown error"
|
|
54
|
+
details = data
|
|
55
|
+
|
|
56
|
+
if code == "IAM001":
|
|
57
|
+
raise AuthenticationError(message, status_code=status, details=details)
|
|
58
|
+
if code == "IAM002":
|
|
59
|
+
raise AuthorizationError(message, status_code=status, details=details)
|
|
60
|
+
if code == "AA001":
|
|
61
|
+
raise QuotaExceededError(message, status_code=status, details=details)
|
|
62
|
+
if code == "U007":
|
|
63
|
+
raise InsufficientCreditsError(message, status_code=status, details=details)
|
|
64
|
+
|
|
65
|
+
if 400 <= status < 500:
|
|
66
|
+
raise ValidationError(message, code=code, status_code=status, details=details)
|
|
67
|
+
if status >= 500:
|
|
68
|
+
raise ServerError(message, code=code, status_code=status, details=details)
|
|
69
|
+
|
|
70
|
+
raise AuthentaError(message, code=code, status_code=status, details=details)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _safe_json(resp: requests.Response) -> Dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Safely parse JSON; if body is empty, return {}.
|
|
76
|
+
If body is non-JSON, raise a ValidationError with the raw body snippet.
|
|
77
|
+
"""
|
|
78
|
+
text = resp.text or ""
|
|
79
|
+
if not text.strip():
|
|
80
|
+
return {}
|
|
81
|
+
try:
|
|
82
|
+
return resp.json()
|
|
83
|
+
except ValueError:
|
|
84
|
+
raise ValidationError(
|
|
85
|
+
message="Expected JSON response but got non-JSON payload",
|
|
86
|
+
status_code=resp.status_code,
|
|
87
|
+
details={"body": text[:200]},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AuthentaClient:
|
|
92
|
+
"""
|
|
93
|
+
Authenta Python SDK.
|
|
94
|
+
|
|
95
|
+
Features:
|
|
96
|
+
- Builds Auth headers with x-client-id / x-client-secret.
|
|
97
|
+
- Wraps /api/media endpoints for create, get, list, delete.
|
|
98
|
+
- Implements two-step upload (POST /api/media -> PUT to S3).
|
|
99
|
+
- Process deepfake-detection.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, base_url: str, client_id: str, client_secret: str):
|
|
103
|
+
"""
|
|
104
|
+
Create new Authenta client.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
base_url: Authenta API base URL, e.g. "https://platform.authenta.ai".
|
|
108
|
+
client_id: Your Authenta client ID.
|
|
109
|
+
client_secret: Your Authenta client secret.
|
|
110
|
+
"""
|
|
111
|
+
self.base_url = base_url.rstrip("/")
|
|
112
|
+
self.client_id = client_id
|
|
113
|
+
self.client_secret = client_secret
|
|
114
|
+
|
|
115
|
+
def _headers(self) -> Dict[str, str]:
|
|
116
|
+
"""Return default headers for Authenta API calls."""
|
|
117
|
+
return {
|
|
118
|
+
"x-client-id": self.client_id,
|
|
119
|
+
"x-client-secret": self.client_secret,
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def _content_type(self, path: str) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Guess the MIME type for a file path.
|
|
126
|
+
|
|
127
|
+
Falls back to 'application/octet-stream' if unknown.
|
|
128
|
+
"""
|
|
129
|
+
filetype, _ = mimetypes.guess_type(path)
|
|
130
|
+
return filetype or "application/octet-stream"
|
|
131
|
+
|
|
132
|
+
def create_media(
|
|
133
|
+
self,
|
|
134
|
+
name: str,
|
|
135
|
+
content_type: str,
|
|
136
|
+
size: int,
|
|
137
|
+
model_type: str,
|
|
138
|
+
**kwargs,
|
|
139
|
+
) -> Dict[str, Any]:
|
|
140
|
+
"""
|
|
141
|
+
POST /api/media: create a media record and get an upload URL.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
name: Original file name.
|
|
145
|
+
content_type: MIME type of the file (e.g. "image/png", "video/mp4").
|
|
146
|
+
size: File size in bytes.
|
|
147
|
+
model_type: Detection model type, e.g. "AC-1" or "DF-1".
|
|
148
|
+
"AC-1" - Image, "DF-1" - Video.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Parsed JSON response containing at least 'mid' and 'uploadUrl'.
|
|
152
|
+
"""
|
|
153
|
+
url = f"{self.base_url}/api/media"
|
|
154
|
+
|
|
155
|
+
payload = {
|
|
156
|
+
"name": name,
|
|
157
|
+
"contentType": content_type,
|
|
158
|
+
"size": size,
|
|
159
|
+
"modelType": model_type,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if model_type.upper() == "FI-1":
|
|
163
|
+
fi_params = {
|
|
164
|
+
"isSingleFace": kwargs.get("isSingleFace", True),
|
|
165
|
+
"faceswapCheck": kwargs.get("faceswapCheck"),
|
|
166
|
+
"livenessCheck": kwargs.get("livenessCheck"),
|
|
167
|
+
"faceSimilarityCheck": kwargs.get("faceSimilarityCheck"),
|
|
168
|
+
}
|
|
169
|
+
payload.update({
|
|
170
|
+
"metadata": {
|
|
171
|
+
i: j for i, j in fi_params.items()
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
if self.base_url=="https://platform.authenta.ai":
|
|
175
|
+
resp = cffi_requests.post(url, json=payload, headers=self._headers(), impersonate="chrome", timeout=30)
|
|
176
|
+
else:
|
|
177
|
+
resp = requests.post(url, json=payload, headers=self._headers(), timeout=30)
|
|
178
|
+
print(f"create_media payload: {payload}")
|
|
179
|
+
print("create_media raw:", resp.status_code, repr(resp.text[:200]))
|
|
180
|
+
if not resp.ok:
|
|
181
|
+
_raise_for_authenta_error(resp)
|
|
182
|
+
return _safe_json(resp)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_media(self, mid: str) -> Dict[str, Any]:
|
|
186
|
+
"""
|
|
187
|
+
GET /api/media/{mid}: fetch a single media record.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
mid: Media ID returned by create_media / upload_file.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Parsed JSON media record.
|
|
194
|
+
"""
|
|
195
|
+
url = f"{self.base_url}/api/media/{mid}"
|
|
196
|
+
if self.base_url=="https://platform.authenta.ai":
|
|
197
|
+
resp = cffi_requests.get(url, headers=self._headers(), impersonate="chrome", timeout=30)
|
|
198
|
+
else:
|
|
199
|
+
resp = requests.get(url, headers=self._headers(), timeout=30)
|
|
200
|
+
if not resp.ok:
|
|
201
|
+
_raise_for_authenta_error(resp)
|
|
202
|
+
return _safe_json(resp)
|
|
203
|
+
|
|
204
|
+
def upload_file(self, path: str, model_type: str, **kwargs) -> Dict[str, Any]:
|
|
205
|
+
"""
|
|
206
|
+
Upload a file via the two-step Authenta media flow.
|
|
207
|
+
|
|
208
|
+
Steps:
|
|
209
|
+
1) POST /api/media to create the record and obtain 'mid' + 'uploadUrl'.
|
|
210
|
+
2) PUT the file bytes to the presigned S3 'uploadUrl'.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
path: Local path to the media file.
|
|
214
|
+
model_type: Detection model type to use, e.g. "AC-1" or "DF-1".
|
|
215
|
+
**kwargs: Additional parameters for the media creation request.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
The JSON response from POST /api/media (includes 'mid', 'status', etc.).
|
|
219
|
+
"""
|
|
220
|
+
filename = os.path.basename(path)
|
|
221
|
+
content_type = self._content_type(path)
|
|
222
|
+
size = os.path.getsize(path)
|
|
223
|
+
|
|
224
|
+
# Step 1: create media
|
|
225
|
+
meta = self.create_media(
|
|
226
|
+
name=filename,
|
|
227
|
+
content_type=content_type,
|
|
228
|
+
size=size,
|
|
229
|
+
model_type=model_type,
|
|
230
|
+
**kwargs,
|
|
231
|
+
)
|
|
232
|
+
upload_url = meta.get("uploadUrl")
|
|
233
|
+
if not upload_url:
|
|
234
|
+
raise RuntimeError("No uploadUrl in create_media response")
|
|
235
|
+
|
|
236
|
+
# Step 2: upload to S3
|
|
237
|
+
with open(path, "rb") as f:
|
|
238
|
+
put_resp = requests.put(
|
|
239
|
+
upload_url,
|
|
240
|
+
data=f,
|
|
241
|
+
headers={"Content-Type": content_type},
|
|
242
|
+
timeout=300,
|
|
243
|
+
)
|
|
244
|
+
if model_type.upper() == "FI-1":
|
|
245
|
+
reference_img_url = meta.get("referenceUploadUrl")
|
|
246
|
+
if reference_img_url:
|
|
247
|
+
with open(kwargs.get("reference_img_path"), "rb") as f:
|
|
248
|
+
ref_res = requests.put(
|
|
249
|
+
reference_img_url,
|
|
250
|
+
data=f,
|
|
251
|
+
headers={"Content-Type": self._content_type(kwargs.get("reference_img_path"))},
|
|
252
|
+
timeout=300,
|
|
253
|
+
)
|
|
254
|
+
ref_res.raise_for_status()
|
|
255
|
+
put_resp.raise_for_status()
|
|
256
|
+
return meta
|
|
257
|
+
|
|
258
|
+
def wait_for_media(
|
|
259
|
+
self,
|
|
260
|
+
mid: str,
|
|
261
|
+
interval: float = 5.0,
|
|
262
|
+
timeout: float = 600.0,
|
|
263
|
+
) -> Dict[str, Any]:
|
|
264
|
+
"""
|
|
265
|
+
Poll GET /api/media/{mid} until it reaches a terminal status.
|
|
266
|
+
|
|
267
|
+
Terminal statuses: PROCESSED, FAILED, ERROR.
|
|
268
|
+
Raises TimeoutError if 'timeout' seconds elapse without a terminal state.
|
|
269
|
+
"""
|
|
270
|
+
start = time.time()
|
|
271
|
+
while True:
|
|
272
|
+
media = self.get_media(mid)
|
|
273
|
+
status = (media.get("status") or "").upper()
|
|
274
|
+
if status in {"PROCESSED", "FAILED", "ERROR"}:
|
|
275
|
+
return media
|
|
276
|
+
if time.time() - start > timeout:
|
|
277
|
+
raise TimeoutError(
|
|
278
|
+
f"Timed out waiting for media {mid}, last status={status!r}"
|
|
279
|
+
)
|
|
280
|
+
time.sleep(interval)
|
|
281
|
+
|
|
282
|
+
def list_media(self, **params) -> Dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
GET /api/media: list media for this client.
|
|
285
|
+
|
|
286
|
+
Accepts optional query params (page, pageSize, filters) if the API supports them.
|
|
287
|
+
"""
|
|
288
|
+
url = f"{self.base_url}/api/media"
|
|
289
|
+
if self.base_url=="https://platform.authenta.ai":
|
|
290
|
+
resp = cffi_requests.get(url, headers=self._headers(), params=params, impersonate="chrome", timeout=30)
|
|
291
|
+
else:
|
|
292
|
+
resp = requests.get(url, headers=self._headers(), params=params, timeout=30)
|
|
293
|
+
if not resp.ok:
|
|
294
|
+
_raise_for_authenta_error(resp)
|
|
295
|
+
return _safe_json(resp)
|
|
296
|
+
|
|
297
|
+
def process(
|
|
298
|
+
self,
|
|
299
|
+
path: str,
|
|
300
|
+
model_type: str,
|
|
301
|
+
interval: float = 5.0,
|
|
302
|
+
timeout: float = 600.0,
|
|
303
|
+
) -> Dict[str, Any]:
|
|
304
|
+
"""
|
|
305
|
+
High-level helper:
|
|
306
|
+
1) upload_file(path, model_type) -> get mid
|
|
307
|
+
2) wait_for_media(mid)
|
|
308
|
+
"""
|
|
309
|
+
meta = self.upload_file(path, model_type=model_type)
|
|
310
|
+
mid = meta.get("mid")
|
|
311
|
+
if not mid:
|
|
312
|
+
raise RuntimeError("No 'mid' in upload response")
|
|
313
|
+
return self.wait_for_media(mid, interval=interval, timeout=timeout)
|
|
314
|
+
|
|
315
|
+
def face_intelligence(
|
|
316
|
+
self,
|
|
317
|
+
path: str,
|
|
318
|
+
model_type: str,
|
|
319
|
+
reference_img_path: Optional[str] = None,
|
|
320
|
+
isSingleFace: Optional[bool] = True,
|
|
321
|
+
faceswapCheck: Optional[bool] = False,
|
|
322
|
+
livenessCheck: Optional[bool] = False,
|
|
323
|
+
faceSimilarityCheck: Optional[bool] = False,
|
|
324
|
+
auto_polling: bool = True,
|
|
325
|
+
interval: float = 5.0,
|
|
326
|
+
timeout: float = 600.0,
|
|
327
|
+
) -> Dict[str, Any]:
|
|
328
|
+
"""
|
|
329
|
+
High-level helper for Face Integrity (FI) model:
|
|
330
|
+
1) upload_file(path, model_type) -> get mid
|
|
331
|
+
2) wait_for_media(mid) (only if auto_polling=True)
|
|
332
|
+
Args:
|
|
333
|
+
path: Local path to the media file.
|
|
334
|
+
model_type: Detection model type to use, e.g. "FI-1".
|
|
335
|
+
isSingleFace: Whether to check for a single face.
|
|
336
|
+
faceswapCheck: Whether to check for face swapping.
|
|
337
|
+
livenessCheck: Whether to check for liveness.
|
|
338
|
+
faceSimilarityCheck: Whether to check for face similarity.
|
|
339
|
+
auto_polling: If True (default), blocks until processing completes via
|
|
340
|
+
wait_for_media(). If False, returns immediately after upload with
|
|
341
|
+
the initial media metadata (including 'mid') so you can poll manually.
|
|
342
|
+
interval: Polling interval in seconds (used only when auto_polling=True).
|
|
343
|
+
timeout: Timeout in seconds (used only when auto_polling=True).
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
If auto_polling=True: the final processed media JSON.
|
|
347
|
+
If auto_polling=False: the initial upload metadata dict (includes 'mid').
|
|
348
|
+
"""
|
|
349
|
+
fi_params = {
|
|
350
|
+
"reference_img_path": reference_img_path,
|
|
351
|
+
"isSingleFace": True,
|
|
352
|
+
"faceswapCheck": faceswapCheck,
|
|
353
|
+
"livenessCheck": livenessCheck,
|
|
354
|
+
"faceSimilarityCheck": faceSimilarityCheck,
|
|
355
|
+
}
|
|
356
|
+
if self._content_type(path).startswith("image/") and faceswapCheck == True:
|
|
357
|
+
raise ValueError("faceswapCheck cannot be True for image media")
|
|
358
|
+
if self._content_type(path).startswith("video/") and faceSimilarityCheck == True:
|
|
359
|
+
raise ValueError("faceSimilarityCheck cannot be True for video media")
|
|
360
|
+
if faceSimilarityCheck == True and (not reference_img_path or reference_img_path == None):
|
|
361
|
+
raise ValueError("reference_img_path must be provided if faceSimilarityCheck is True")
|
|
362
|
+
|
|
363
|
+
meta = self.upload_file(path, model_type=model_type, **fi_params)
|
|
364
|
+
if not auto_polling:
|
|
365
|
+
return meta
|
|
366
|
+
mid = meta.get("mid")
|
|
367
|
+
if not mid:
|
|
368
|
+
raise RuntimeError("No 'mid' in upload response")
|
|
369
|
+
return self.wait_for_media(mid, interval=interval, timeout=timeout)
|
|
370
|
+
|
|
371
|
+
def delete_media(self, mid: str) -> None:
|
|
372
|
+
"""DELETE /api/media/{mid}: delete a media record."""
|
|
373
|
+
url = f"{self.base_url}/api/media/{mid}"
|
|
374
|
+
if self.base_url=="https://platform.authenta.ai":
|
|
375
|
+
resp = cffi_requests.delete(url, headers=self._headers(), impersonate="chrome", timeout=30)
|
|
376
|
+
else:
|
|
377
|
+
resp = requests.delete(url, headers=self._headers(), timeout=30)
|
|
378
|
+
if not resp.ok:
|
|
379
|
+
_raise_for_authenta_error(resp)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from typing import Literal, Optional, Dict, Any
|
|
2
|
+
|
|
3
|
+
AuthentaErrorCode = Literal[
|
|
4
|
+
"IAM001", "IAM002", # Auth
|
|
5
|
+
"AA001", "U007", # Quota/Billing
|
|
6
|
+
"bad_request", "not_found", "timeout",
|
|
7
|
+
"payload_too_large", "server_error", "unknown",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuthentaError(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Base exception for all Authenta SDK errors.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
code: AuthentaErrorCode = "unknown",
|
|
20
|
+
status_code: Optional[int] = None,
|
|
21
|
+
details: Optional[Dict[str, Any]] = None,
|
|
22
|
+
):
|
|
23
|
+
self.message = message
|
|
24
|
+
self.code = code
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
self.details = details or {}
|
|
27
|
+
|
|
28
|
+
# String repr: "[IAM001] Invalid Key (HTTP 401)"
|
|
29
|
+
text = f"[{code}] {message}"
|
|
30
|
+
if status_code:
|
|
31
|
+
text += f" (HTTP {status_code})"
|
|
32
|
+
super().__init__(text)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AuthenticationError(AuthentaError):
|
|
36
|
+
"""Raised for invalid API keys (Default: IAM001)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str, code: AuthentaErrorCode = "IAM001", **kwargs):
|
|
39
|
+
super().__init__(message, code, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AuthorizationError(AuthentaError):
|
|
43
|
+
"""Raised for permission issues (Default: IAM002)."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, code: AuthentaErrorCode = "IAM002", **kwargs):
|
|
46
|
+
super().__init__(message, code, **kwargs)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class QuotaExceededError(AuthentaError):
|
|
50
|
+
"""Raised when rate limits are hit (Default: AA001)."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str, code: AuthentaErrorCode = "AA001", **kwargs):
|
|
53
|
+
super().__init__(message, code, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class InsufficientCreditsError(AuthentaError):
|
|
57
|
+
"""Raised when account balance is empty (Default: U007)."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, message: str, code: AuthentaErrorCode = "U007", **kwargs):
|
|
60
|
+
super().__init__(message, code, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ValidationError(AuthentaError):
|
|
64
|
+
"""Raised for client-side issues (Default: bad_request)."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, message: str, code: AuthentaErrorCode = "bad_request", **kwargs):
|
|
67
|
+
super().__init__(message, code, **kwargs)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ServerError(AuthentaError):
|
|
71
|
+
"""Raised for server-side issues (Default: server_error)."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, message: str, code: AuthentaErrorCode = "server_error", **kwargs):
|
|
74
|
+
super().__init__(message, code, **kwargs)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# import os
|
|
2
|
+
# import time
|
|
3
|
+
# import mimetypes
|
|
4
|
+
# from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
# import requests
|
|
7
|
+
# from .authenta_exceptions import (
|
|
8
|
+
# AuthentaError,
|
|
9
|
+
# AuthenticationError,
|
|
10
|
+
# AuthorizationError,
|
|
11
|
+
# QuotaExceededError,
|
|
12
|
+
# InsufficientCreditsError,
|
|
13
|
+
# ValidationError,
|
|
14
|
+
# ServerError,
|
|
15
|
+
# )
|
|
16
|
+
|
|
17
|
+
# from .authenta_client import _raise_for_authenta_error,_safe_json
|
|
18
|
+
|
|
19
|
+
# class FaceIntelligence:
|
|
20
|
+
# def __init__(self, base_url: str, client_id: str, client_secret: str):
|
|
21
|
+
# """
|
|
22
|
+
# Create new Authenta client.
|
|
23
|
+
|
|
24
|
+
# Args:
|
|
25
|
+
# base_url: Authenta API base URL, e.g. "https://platform.authenta.ai".
|
|
26
|
+
# client_id: Your Authenta client ID.
|
|
27
|
+
# client_secret: Your Authenta client secret.
|
|
28
|
+
# """
|
|
29
|
+
# self.base_url = base_url.rstrip("/")
|
|
30
|
+
# self.client_id = client_id
|
|
31
|
+
# self.client_secret = client_secret
|
|
32
|
+
|
|
33
|
+
# def _headers(self) -> Dict[str, str]:
|
|
34
|
+
# """Return default headers for Authenta API calls."""
|
|
35
|
+
# return {
|
|
36
|
+
# "x-client-id": self.client_id,
|
|
37
|
+
# "x-client-secret": self.client_secret,
|
|
38
|
+
# "Content-Type": "application/json",
|
|
39
|
+
# }
|
|
40
|
+
|
|
41
|
+
# def _content_type(self, path: str) -> str:
|
|
42
|
+
# """
|
|
43
|
+
# Guess the MIME type for a file path.
|
|
44
|
+
|
|
45
|
+
# Falls back to 'application/octet-stream' if unknown.
|
|
46
|
+
# """
|
|
47
|
+
# filetype, _ = mimetypes.guess_type(path)
|
|
48
|
+
# return filetype or "application/octet-stream"
|
|
49
|
+
|
|
50
|
+
# def create_media(
|
|
51
|
+
# self,
|
|
52
|
+
# name: str,
|
|
53
|
+
# content_type: str,
|
|
54
|
+
# size: int,
|
|
55
|
+
# model_type: str,
|
|
56
|
+
# **kwargs,
|
|
57
|
+
# ) -> Dict[str, Any]:
|
|
58
|
+
# url = f"{self.base_url}/api/media"
|
|
59
|
+
|
|
60
|
+
# payload = {
|
|
61
|
+
# "name": name,
|
|
62
|
+
# "contentType": content_type,
|
|
63
|
+
# "size": size,
|
|
64
|
+
# "modelType": model_type,
|
|
65
|
+
# }
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# fi_params = {
|
|
69
|
+
# "isSingleFace": kwargs.get("isSingleFace", True),
|
|
70
|
+
# "faceSwapCheck": kwargs.get("faceSwapCheck"),
|
|
71
|
+
# "livenessCheck": kwargs.get("livenessCheck"),
|
|
72
|
+
# "faceSimilarityCheck": kwargs.get("faceSimilarityCheck"),
|
|
73
|
+
# }
|
|
74
|
+
# payload.update({
|
|
75
|
+
# "metadata": {
|
|
76
|
+
# i: (j if j is not None else False) for i, j in fi_params.items()
|
|
77
|
+
# }
|
|
78
|
+
# })
|
|
79
|
+
# resp = requests.post(url, json=payload, headers=self._headers(), timeout=30)
|
|
80
|
+
# print("create_media raw:", resp.status_code, repr(resp.text[:200]))
|
|
81
|
+
# if not resp.ok:
|
|
82
|
+
# _raise_for_authenta_error(resp)
|
|
83
|
+
# return _safe_json(resp)
|
|
84
|
+
|
|
85
|
+
# def upload_file(self, path: str, model_type: str, **kwargs) -> Dict[str, Any]:
|
|
86
|
+
# """
|
|
87
|
+
# Upload a file via the two-step Authenta media flow.
|
|
88
|
+
|
|
89
|
+
# Steps:
|
|
90
|
+
# 1) POST /api/media to create the record and obtain 'mid' + 'uploadUrl'.
|
|
91
|
+
# 2) PUT the file bytes to the presigned S3 'uploadUrl'.
|
|
92
|
+
|
|
93
|
+
# Args:
|
|
94
|
+
# path: Local path to the media file.
|
|
95
|
+
# model_type: Detection model type to use, e.g. "AC-1" or "DF-1".
|
|
96
|
+
# **kwargs: Additional parameters for the media creation request.
|
|
97
|
+
|
|
98
|
+
# Returns:
|
|
99
|
+
# The JSON response from POST /api/media (includes 'mid', 'status', etc.).
|
|
100
|
+
# """
|
|
101
|
+
# filename = os.path.basename(path)
|
|
102
|
+
# content_type = self._content_type(path)
|
|
103
|
+
# size = os.path.getsize(path)
|
|
104
|
+
|
|
105
|
+
# # Step 1: create media
|
|
106
|
+
# meta = self.create_media(
|
|
107
|
+
# name=filename,
|
|
108
|
+
# content_type=content_type,
|
|
109
|
+
# size=size,
|
|
110
|
+
# model_type=model_type,
|
|
111
|
+
# **kwargs,
|
|
112
|
+
# )
|
|
113
|
+
# upload_url = meta.get("uploadUrl")
|
|
114
|
+
# if not upload_url:
|
|
115
|
+
# raise RuntimeError("No uploadUrl in create_media response")
|
|
116
|
+
|
|
117
|
+
# # Step 2: upload to S3
|
|
118
|
+
# with open(path, "rb") as f:
|
|
119
|
+
# put_resp = requests.put(
|
|
120
|
+
# upload_url,
|
|
121
|
+
# data=f,
|
|
122
|
+
# headers={"Content-Type": content_type},
|
|
123
|
+
# timeout=300,
|
|
124
|
+
# )
|
|
125
|
+
# if model_type.upper() == "FI-1":
|
|
126
|
+
# reference_img_url = meta.get("referenceUploadUrl")
|
|
127
|
+
# if reference_img_url:
|
|
128
|
+
# with open(kwargs.get("reference_img_path"), "rb") as f:
|
|
129
|
+
# ref_put
|
|
130
|
+
# put_resp.raise_for_status()
|
|
131
|
+
# return meta
|