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