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
authenta/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .authenta_client import *
|
|
2
|
+
from .authenta_exceptions import (
|
|
3
|
+
AuthentaError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
AuthorizationError,
|
|
6
|
+
QuotaExceededError,
|
|
7
|
+
InsufficientCreditsError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
ServerError,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AuthentaClient",
|
|
14
|
+
"AuthentaError",
|
|
15
|
+
"AuthenticationError",
|
|
16
|
+
"AuthorizationError",
|
|
17
|
+
"QuotaExceededError",
|
|
18
|
+
"InsufficientCreditsError",
|
|
19
|
+
"ValidationError",
|
|
20
|
+
"ServerError",
|
|
21
|
+
]
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
example usage:
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from authenta.async_authenta_client import AsyncAuthentaClient
|
|
6
|
+
from authenta.authenta_exceptions import AuthenticationError
|
|
7
|
+
|
|
8
|
+
async def main():
|
|
9
|
+
client = AsyncAuthentaClient(
|
|
10
|
+
base_url="https://platform.authenta.ai",
|
|
11
|
+
client_id="...",
|
|
12
|
+
client_secret="...",
|
|
13
|
+
)
|
|
14
|
+
async with client:
|
|
15
|
+
media = await client.process("file_path", model_type="AC-1/DF-1")
|
|
16
|
+
print(media.get("status"))
|
|
17
|
+
|
|
18
|
+
asyncio.run(main())
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
import mimetypes
|
|
26
|
+
from typing import Any, Dict, Optional
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
from .authenta_exceptions import (
|
|
31
|
+
AuthentaError,
|
|
32
|
+
AuthenticationError,
|
|
33
|
+
AuthorizationError,
|
|
34
|
+
QuotaExceededError,
|
|
35
|
+
InsufficientCreditsError,
|
|
36
|
+
ValidationError,
|
|
37
|
+
ServerError,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _raise_for_authenta_error_async(resp: httpx.Response) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Async variant: map an Authenta API error response to a rich SDK exception.
|
|
44
|
+
"""
|
|
45
|
+
status = resp.status_code
|
|
46
|
+
try:
|
|
47
|
+
data = resp.json()
|
|
48
|
+
except ValueError:
|
|
49
|
+
if 400 <= status < 500:
|
|
50
|
+
raise ValidationError(
|
|
51
|
+
message=resp.text or "Client error",
|
|
52
|
+
status_code=status,
|
|
53
|
+
)
|
|
54
|
+
if status >= 500:
|
|
55
|
+
raise ServerError(
|
|
56
|
+
message=resp.text or "Server error",
|
|
57
|
+
status_code=status,
|
|
58
|
+
)
|
|
59
|
+
resp.raise_for_status()
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
code = data.get("code") or "unknown"
|
|
63
|
+
message = data.get("message") or resp.reason_phrase or "Unknown error"
|
|
64
|
+
details = data
|
|
65
|
+
|
|
66
|
+
if code == "IAM001":
|
|
67
|
+
raise AuthenticationError(message, status_code=status, details=details)
|
|
68
|
+
if code == "IAM002":
|
|
69
|
+
raise AuthorizationError(message, status_code=status, details=details)
|
|
70
|
+
if code == "AA001":
|
|
71
|
+
raise QuotaExceededError(message, status_code=status, details=details)
|
|
72
|
+
if code == "U007":
|
|
73
|
+
raise InsufficientCreditsError(message, status_code=status, details=details)
|
|
74
|
+
|
|
75
|
+
if 400 <= status < 500:
|
|
76
|
+
raise ValidationError(message, code=code, status_code=status, details=details)
|
|
77
|
+
if status >= 500:
|
|
78
|
+
raise ServerError(message, code=code, status_code=status, details=details)
|
|
79
|
+
|
|
80
|
+
raise AuthentaError(message, code=code, status_code=status, details=details)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _safe_json_async(resp: httpx.Response) -> Dict[str, Any]:
|
|
84
|
+
text = resp.text or ""
|
|
85
|
+
if not text.strip():
|
|
86
|
+
return {}
|
|
87
|
+
try:
|
|
88
|
+
return resp.json()
|
|
89
|
+
except ValueError:
|
|
90
|
+
raise ValidationError(
|
|
91
|
+
message="Expected JSON response but got non-JSON payload",
|
|
92
|
+
status_code=resp.status_code,
|
|
93
|
+
details={"body": text[:200]},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AsyncAuthentaClient:
|
|
98
|
+
"""
|
|
99
|
+
Asynchronous Authenta Python SDK.
|
|
100
|
+
|
|
101
|
+
Mirrors AuthentaClient and uses httpx.AsyncClient and async/await.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
base_url: str,
|
|
107
|
+
client_id: str,
|
|
108
|
+
client_secret: str,
|
|
109
|
+
*,
|
|
110
|
+
timeout: float = 30.0,
|
|
111
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
112
|
+
):
|
|
113
|
+
self.base_url = base_url.rstrip("/")
|
|
114
|
+
self.client_id = client_id
|
|
115
|
+
self.client_secret = client_secret
|
|
116
|
+
self.timeout = timeout
|
|
117
|
+
self._external_client = client
|
|
118
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
119
|
+
|
|
120
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
121
|
+
if self._external_client is not None:
|
|
122
|
+
return self._external_client
|
|
123
|
+
if self._client is None:
|
|
124
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
125
|
+
return self._client
|
|
126
|
+
|
|
127
|
+
async def aclose(self) -> None:
|
|
128
|
+
if self._client is not None:
|
|
129
|
+
await self._client.aclose()
|
|
130
|
+
self._client = None
|
|
131
|
+
|
|
132
|
+
async def __aenter__(self) -> "AsyncAuthentaClient":
|
|
133
|
+
await self._get_client()
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
137
|
+
await self.aclose()
|
|
138
|
+
|
|
139
|
+
def _headers(self) -> Dict[str, str]:
|
|
140
|
+
return {
|
|
141
|
+
"x-client-id": self.client_id,
|
|
142
|
+
"x-client-secret": self.client_secret,
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def _content_type(self, path: str) -> str:
|
|
147
|
+
filetype, _ = mimetypes.guess_type(path)
|
|
148
|
+
return filetype or "application/octet-stream"
|
|
149
|
+
|
|
150
|
+
async def create_media(
|
|
151
|
+
self,
|
|
152
|
+
name: str,
|
|
153
|
+
content_type: str,
|
|
154
|
+
size: int,
|
|
155
|
+
model_type: str,
|
|
156
|
+
**kwargs
|
|
157
|
+
) -> Dict[str, Any]:
|
|
158
|
+
url = f"{self.base_url}/api/media"
|
|
159
|
+
payload = {
|
|
160
|
+
"name": name,
|
|
161
|
+
"contentType": content_type,
|
|
162
|
+
"size": size,
|
|
163
|
+
"modelType": model_type,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Add model-specific parameters to payload
|
|
167
|
+
if model_type == "FI":
|
|
168
|
+
# Face Integrity specific parameters
|
|
169
|
+
fi_params = {
|
|
170
|
+
"isSingleFace": kwargs.get("isSingleFace"),
|
|
171
|
+
"faceSwapCheck": kwargs.get("faceSwapCheck"),
|
|
172
|
+
"livenessCheck": kwargs.get("livenessCheck"),
|
|
173
|
+
"faceSimilarityCheck": kwargs.get("faceSimilarityCheck"),
|
|
174
|
+
}
|
|
175
|
+
# Only add parameters that were explicitly provided (not None)
|
|
176
|
+
payload.update({k: v for k, v in fi_params.items() if v is not None})
|
|
177
|
+
else:
|
|
178
|
+
# For other model types, pass through remaining kwargs
|
|
179
|
+
payload.update({k: v for k, v in kwargs.items() if v is not None})
|
|
180
|
+
|
|
181
|
+
client = await self._get_client()
|
|
182
|
+
resp = await client.post(url, json=payload, headers=self._headers())
|
|
183
|
+
if not resp.is_success:
|
|
184
|
+
_raise_for_authenta_error_async(resp)
|
|
185
|
+
return _safe_json_async(resp)
|
|
186
|
+
|
|
187
|
+
async def get_media(self, mid: str) -> Dict[str, Any]:
|
|
188
|
+
url = f"{self.base_url}/api/media/{mid}"
|
|
189
|
+
client = await self._get_client()
|
|
190
|
+
resp = await client.get(url, headers=self._headers())
|
|
191
|
+
if not resp.is_success:
|
|
192
|
+
_raise_for_authenta_error_async(resp)
|
|
193
|
+
return _safe_json_async(resp)
|
|
194
|
+
|
|
195
|
+
async def upload_file(self, path: str, model_type: str, **kwargs) -> Dict[str, Any]:
|
|
196
|
+
filename = os.path.basename(path)
|
|
197
|
+
content_type = self._content_type(path)
|
|
198
|
+
size = os.path.getsize(path)
|
|
199
|
+
|
|
200
|
+
meta = await self.create_media(
|
|
201
|
+
name=filename,
|
|
202
|
+
content_type=content_type,
|
|
203
|
+
size=size,
|
|
204
|
+
model_type=model_type,
|
|
205
|
+
**kwargs
|
|
206
|
+
)
|
|
207
|
+
upload_url = meta.get("uploadUrl")
|
|
208
|
+
if not upload_url:
|
|
209
|
+
raise RuntimeError("No uploadUrl in create_media response")
|
|
210
|
+
|
|
211
|
+
client = await self._get_client()
|
|
212
|
+
# S3 presigned PUT; usually no JSON error schema
|
|
213
|
+
with open(path, "rb") as f:
|
|
214
|
+
resp = await client.put(
|
|
215
|
+
upload_url,
|
|
216
|
+
content=f,
|
|
217
|
+
headers={"Content-Type": content_type},
|
|
218
|
+
timeout=300.0,
|
|
219
|
+
)
|
|
220
|
+
if not resp.is_success:
|
|
221
|
+
# Map 4xx/5xx to ValidationError/ServerError
|
|
222
|
+
if 400 <= resp.status_code < 500:
|
|
223
|
+
raise ValidationError(
|
|
224
|
+
message=resp.text or "Upload client error",
|
|
225
|
+
status_code=resp.status_code,
|
|
226
|
+
)
|
|
227
|
+
if resp.status_code >= 500:
|
|
228
|
+
raise ServerError(
|
|
229
|
+
message=resp.text or "Upload server error",
|
|
230
|
+
status_code=resp.status_code,
|
|
231
|
+
)
|
|
232
|
+
resp.raise_for_status()
|
|
233
|
+
return meta
|
|
234
|
+
|
|
235
|
+
async def wait_for_media(
|
|
236
|
+
self,
|
|
237
|
+
mid: str,
|
|
238
|
+
interval: float = 5.0,
|
|
239
|
+
timeout: float = 600.0,
|
|
240
|
+
) -> Dict[str, Any]:
|
|
241
|
+
"""
|
|
242
|
+
Poll GET /api/media/{mid} until it reaches a terminal status.
|
|
243
|
+
Terminal: PROCESSED, FAILED, ERROR.
|
|
244
|
+
"""
|
|
245
|
+
start = time.time()
|
|
246
|
+
while True:
|
|
247
|
+
media = await self.get_media(mid)
|
|
248
|
+
status = (media.get("status") or "").upper()
|
|
249
|
+
if status in {"PROCESSED", "FAILED", "ERROR"}:
|
|
250
|
+
return media
|
|
251
|
+
if time.time() - start > timeout:
|
|
252
|
+
raise TimeoutError(
|
|
253
|
+
f"Timed out waiting for media {mid}, last status={status!r}"
|
|
254
|
+
)
|
|
255
|
+
await asyncio.sleep(interval)
|
|
256
|
+
|
|
257
|
+
async def list_media(self, **params) -> Dict[str, Any]:
|
|
258
|
+
url = f"{self.base_url}/api/media"
|
|
259
|
+
client = await self._get_client()
|
|
260
|
+
resp = await client.get(url, headers=self._headers(), params=params)
|
|
261
|
+
if not resp.is_success:
|
|
262
|
+
_raise_for_authenta_error_async(resp)
|
|
263
|
+
return _safe_json_async(resp)
|
|
264
|
+
|
|
265
|
+
async def delete_media(self, mid: str) -> None:
|
|
266
|
+
url = f"{self.base_url}/api/media/{mid}"
|
|
267
|
+
client = await self._get_client()
|
|
268
|
+
resp = await client.delete(url, headers=self._headers())
|
|
269
|
+
if not resp.is_success:
|
|
270
|
+
_raise_for_authenta_error_async(resp)
|
|
271
|
+
|
|
272
|
+
async def process(
|
|
273
|
+
self,
|
|
274
|
+
path: str,
|
|
275
|
+
model_type: str,
|
|
276
|
+
interval: float = 5.0,
|
|
277
|
+
timeout: float = 600.0,
|
|
278
|
+
) -> Dict[str, Any]:
|
|
279
|
+
"""
|
|
280
|
+
High-level async helper:
|
|
281
|
+
1) await upload_file(path, model_type) -> get mid
|
|
282
|
+
2) await wait_for_media(mid)
|
|
283
|
+
"""
|
|
284
|
+
meta = await self.upload_file(path, model_type=model_type)
|
|
285
|
+
mid = meta.get("mid")
|
|
286
|
+
if not mid:
|
|
287
|
+
raise RuntimeError("No 'mid' in upload response")
|
|
288
|
+
return await self.wait_for_media(mid, interval=interval, timeout=timeout)
|
|
289
|
+
|
|
290
|
+
async def process_FI(
|
|
291
|
+
self,
|
|
292
|
+
path: str,
|
|
293
|
+
model_type: str,
|
|
294
|
+
isSingleFace: Optional[bool] = None,
|
|
295
|
+
faceSwapCheck: Optional[bool] = None,
|
|
296
|
+
livenessCheck: Optional[bool] = None,
|
|
297
|
+
faceSimilarityCheck: Optional[bool] = None,
|
|
298
|
+
auto_polling: bool = True,
|
|
299
|
+
interval: float = 5.0,
|
|
300
|
+
timeout: float = 600.0,
|
|
301
|
+
) -> Dict[str, Any]:
|
|
302
|
+
"""
|
|
303
|
+
High-level async helper for Face Integrity (FI) model:
|
|
304
|
+
1) await upload_file(path, model_type, **fi_params) -> get mid
|
|
305
|
+
2) await wait_for_media(mid) (only if auto_polling=True)
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
path: Local path to the media file.
|
|
309
|
+
model_type: Detection model type to use (e.g., "FI-1").
|
|
310
|
+
isSingleFace: Whether to check for a single face.
|
|
311
|
+
faceSwapCheck: Whether to check for face swapping.
|
|
312
|
+
livenessCheck: Whether to check for liveness.
|
|
313
|
+
faceSimilarityCheck: Whether to check for face similarity.
|
|
314
|
+
auto_polling: If True (default), awaits until processing completes via
|
|
315
|
+
wait_for_media(). If False, returns immediately after upload with
|
|
316
|
+
the initial media metadata (including 'mid') so you can poll manually.
|
|
317
|
+
interval: Polling interval in seconds (used only when auto_polling=True).
|
|
318
|
+
timeout: Timeout in seconds (used only when auto_polling=True).
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
If auto_polling=True: the final processed media JSON.
|
|
322
|
+
If auto_polling=False: the initial upload metadata dict (includes 'mid').
|
|
323
|
+
"""
|
|
324
|
+
# Build FI-specific parameters
|
|
325
|
+
fi_params = {
|
|
326
|
+
"isSingleFace": isSingleFace,
|
|
327
|
+
"faceSwapCheck": faceSwapCheck,
|
|
328
|
+
"livenessCheck": livenessCheck,
|
|
329
|
+
"faceSimilarityCheck": faceSimilarityCheck,
|
|
330
|
+
}
|
|
331
|
+
# Remove None values to avoid sending unnecessary params
|
|
332
|
+
fi_params = {k: v for k, v in fi_params.items() if v is not None}
|
|
333
|
+
|
|
334
|
+
meta = await self.upload_file(path, model_type=model_type, **fi_params)
|
|
335
|
+
if not auto_polling:
|
|
336
|
+
return meta
|
|
337
|
+
mid = meta.get("mid")
|
|
338
|
+
if not mid:
|
|
339
|
+
raise RuntimeError("No 'mid' in upload response")
|
|
340
|
+
return await self.wait_for_media(mid, interval=interval, timeout=timeout)
|