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.
@@ -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