realitydefender 0.1.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.
- realitydefender/__init__.py +370 -0
- realitydefender/__version__.py +5 -0
- realitydefender/client/__init__.py +7 -0
- realitydefender/client/http_client.py +178 -0
- realitydefender/core/constants.py +21 -0
- realitydefender/core/events.py +124 -0
- realitydefender/detection/__init__.py +8 -0
- realitydefender/detection/results.py +191 -0
- realitydefender/detection/upload.py +140 -0
- realitydefender/errors.py +37 -0
- realitydefender/py.typed +1 -0
- realitydefender/reality_defender.py +317 -0
- realitydefender/types/__init__.py +48 -0
- realitydefender/types/events.py +27 -0
- realitydefender/types.py +78 -0
- realitydefender/utils/__init__.py +8 -0
- realitydefender/utils/async_utils.py +42 -0
- realitydefender/utils/file_utils.py +43 -0
- realitydefender-0.1.0.dist-info/METADATA +236 -0
- realitydefender-0.1.0.dist-info/RECORD +23 -0
- realitydefender-0.1.0.dist-info/WHEEL +5 -0
- realitydefender-0.1.0.dist-info/licenses/LICENSE +201 -0
- realitydefender-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reality Defender SDK
|
|
3
|
+
Client library for deepfake detection using the Reality Defender API
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Callable, Optional, cast
|
|
9
|
+
|
|
10
|
+
from .client import create_http_client
|
|
11
|
+
from .core.constants import DEFAULT_POLLING_INTERVAL, DEFAULT_TIMEOUT
|
|
12
|
+
from .core.events import EventEmitter
|
|
13
|
+
from .detection.results import get_detection_result
|
|
14
|
+
from .detection.upload import upload_file
|
|
15
|
+
from .errors import ErrorCode, RealityDefenderError
|
|
16
|
+
from .types import (
|
|
17
|
+
DetectionOptions,
|
|
18
|
+
DetectionResult,
|
|
19
|
+
GetResultOptions,
|
|
20
|
+
RealityDefenderConfig,
|
|
21
|
+
UploadOptions,
|
|
22
|
+
UploadResult,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"RealityDefender",
|
|
27
|
+
"upload_file",
|
|
28
|
+
"get_detection_result",
|
|
29
|
+
"RealityDefenderError",
|
|
30
|
+
"ErrorCode",
|
|
31
|
+
"RealityDefenderConfig",
|
|
32
|
+
"UploadOptions",
|
|
33
|
+
"UploadResult",
|
|
34
|
+
"DetectionResult",
|
|
35
|
+
"GetResultOptions",
|
|
36
|
+
"DetectionOptions",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RealityDefender(EventEmitter):
|
|
41
|
+
"""
|
|
42
|
+
Main SDK class for interacting with the Reality Defender API
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, config: RealityDefenderConfig):
|
|
46
|
+
"""
|
|
47
|
+
Creates a new Reality Defender SDK instance
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config: Configuration options including API key
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
RealityDefenderError: If API key is missing
|
|
54
|
+
"""
|
|
55
|
+
super().__init__()
|
|
56
|
+
|
|
57
|
+
if not config.get("api_key"):
|
|
58
|
+
raise RealityDefenderError("API key is required", "unauthorized")
|
|
59
|
+
|
|
60
|
+
self.api_key = config["api_key"]
|
|
61
|
+
self.client = create_http_client(
|
|
62
|
+
{"api_key": self.api_key, "base_url": config.get("base_url")}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def upload(self, options: UploadOptions) -> UploadResult:
|
|
66
|
+
"""
|
|
67
|
+
Upload a file to Reality Defender for analysis (async version)
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
options: Upload options including file path
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with request_id and media_id
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
RealityDefenderError: If upload fails
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
result = await upload_file(self.client, options)
|
|
80
|
+
return result
|
|
81
|
+
except RealityDefenderError:
|
|
82
|
+
raise
|
|
83
|
+
except Exception as error:
|
|
84
|
+
raise RealityDefenderError(f"Upload failed: {str(error)}", "upload_failed")
|
|
85
|
+
|
|
86
|
+
def upload_sync(self, options: UploadOptions) -> UploadResult:
|
|
87
|
+
"""
|
|
88
|
+
Upload a file to Reality Defender for analysis (synchronous version)
|
|
89
|
+
|
|
90
|
+
This is a convenience wrapper around the async upload method.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
options: Upload options including file path
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dictionary with request_id and media_id
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
RealityDefenderError: If upload fails
|
|
100
|
+
"""
|
|
101
|
+
return self._run_async(self.upload(options))
|
|
102
|
+
|
|
103
|
+
def upload_file(self, file_path: str) -> UploadResult:
|
|
104
|
+
"""
|
|
105
|
+
Upload a file to Reality Defender for analysis (simplified version)
|
|
106
|
+
|
|
107
|
+
This is a more Pythonic convenience method that takes a direct file path.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
file_path: Path to the file to analyze
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary with request_id and media_id
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
RealityDefenderError: If upload fails
|
|
117
|
+
"""
|
|
118
|
+
if not os.path.exists(file_path):
|
|
119
|
+
raise RealityDefenderError(f"File not found: {file_path}", "invalid_file")
|
|
120
|
+
|
|
121
|
+
return self.upload_sync({"file_path": file_path})
|
|
122
|
+
|
|
123
|
+
async def get_result(
|
|
124
|
+
self, request_id: str, options: Optional[GetResultOptions] = None
|
|
125
|
+
) -> DetectionResult:
|
|
126
|
+
"""
|
|
127
|
+
Get the detection result for a specific request ID (async version)
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
request_id: The request ID to get results for
|
|
131
|
+
options: Optional parameters for polling
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Detection result with status and scores
|
|
135
|
+
"""
|
|
136
|
+
if options is None:
|
|
137
|
+
options = {}
|
|
138
|
+
return await get_detection_result(self.client, request_id, options)
|
|
139
|
+
|
|
140
|
+
def get_result_sync(
|
|
141
|
+
self, request_id: str, options: Optional[GetResultOptions] = None
|
|
142
|
+
) -> DetectionResult:
|
|
143
|
+
"""
|
|
144
|
+
Get the detection result for a specific request ID (synchronous version)
|
|
145
|
+
|
|
146
|
+
This is a convenience wrapper around the async get_result method.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
request_id: The request ID to get results for
|
|
150
|
+
options: Optional parameters for polling
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Detection result with status and scores
|
|
154
|
+
"""
|
|
155
|
+
return self._run_async(self.get_result(request_id, options))
|
|
156
|
+
|
|
157
|
+
def detect_file(self, file_path: str) -> DetectionResult:
|
|
158
|
+
"""
|
|
159
|
+
Convenience method to upload and detect a file in one step
|
|
160
|
+
|
|
161
|
+
This is a fully synchronous method that handles all async operations internally.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
file_path: Path to the file to analyze
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Detection result with status and scores
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
RealityDefenderError: If upload or detection fails
|
|
171
|
+
"""
|
|
172
|
+
# Validation - more Pythonic to check path early
|
|
173
|
+
if not os.path.exists(file_path):
|
|
174
|
+
raise RealityDefenderError(f"File not found: {file_path}", "invalid_file")
|
|
175
|
+
|
|
176
|
+
# Upload the file
|
|
177
|
+
upload_result = self.upload_sync({"file_path": file_path})
|
|
178
|
+
request_id = upload_result["request_id"]
|
|
179
|
+
|
|
180
|
+
# Get the result
|
|
181
|
+
return self.get_result_sync(request_id)
|
|
182
|
+
|
|
183
|
+
def poll_for_results(
|
|
184
|
+
self,
|
|
185
|
+
request_id: str,
|
|
186
|
+
polling_interval: Optional[int] = None,
|
|
187
|
+
timeout: Optional[int] = None,
|
|
188
|
+
) -> asyncio.Task:
|
|
189
|
+
"""
|
|
190
|
+
Start polling for results with event-based callback (async version)
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
request_id: The request ID to poll for
|
|
194
|
+
polling_interval: Interval in milliseconds between polls (default: 2000)
|
|
195
|
+
timeout: Maximum time to poll in milliseconds (default: 60000)
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Asyncio task that can be awaited
|
|
199
|
+
"""
|
|
200
|
+
polling_interval = polling_interval or DEFAULT_POLLING_INTERVAL
|
|
201
|
+
timeout = timeout or DEFAULT_TIMEOUT
|
|
202
|
+
|
|
203
|
+
return asyncio.create_task(
|
|
204
|
+
self._poll_for_results(request_id, polling_interval, timeout)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def poll_for_results_sync(
|
|
208
|
+
self,
|
|
209
|
+
request_id: str,
|
|
210
|
+
*, # Force keyword arguments for better readability
|
|
211
|
+
polling_interval: Optional[int] = None,
|
|
212
|
+
timeout: Optional[int] = None,
|
|
213
|
+
on_result: Optional[Callable[[DetectionResult], None]] = None,
|
|
214
|
+
on_error: Optional[Callable[[RealityDefenderError], None]] = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Start polling for results with synchronous callbacks
|
|
218
|
+
|
|
219
|
+
This is a convenience wrapper around the async poll_for_results method.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
request_id: The request ID to poll for
|
|
223
|
+
polling_interval: Interval in milliseconds between polls (default: 2000)
|
|
224
|
+
timeout: Maximum time to poll in milliseconds (default: 60000)
|
|
225
|
+
on_result: Callback function when result is received
|
|
226
|
+
on_error: Callback function when error occurs
|
|
227
|
+
"""
|
|
228
|
+
# Add event handlers if provided
|
|
229
|
+
if on_result:
|
|
230
|
+
# Cast to ResultHandler to satisfy type checker
|
|
231
|
+
from .types.events import ResultHandler
|
|
232
|
+
self.on("result", cast(ResultHandler, on_result))
|
|
233
|
+
if on_error:
|
|
234
|
+
# Cast to ErrorHandler to satisfy type checker
|
|
235
|
+
from .types.events import ErrorHandler
|
|
236
|
+
self.on("error", cast(ErrorHandler, on_error))
|
|
237
|
+
|
|
238
|
+
# Create and run the polling task
|
|
239
|
+
polling_task = self.poll_for_results(request_id, polling_interval, timeout)
|
|
240
|
+
self._run_async(polling_task) # Return value not used in this case
|
|
241
|
+
|
|
242
|
+
async def _poll_for_results(
|
|
243
|
+
self, request_id: str, polling_interval: int, timeout: int
|
|
244
|
+
) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Internal implementation of polling for results
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
request_id: The request ID to poll for
|
|
250
|
+
polling_interval: Interval in milliseconds between polls
|
|
251
|
+
timeout: Maximum time to poll in milliseconds
|
|
252
|
+
"""
|
|
253
|
+
elapsed = 0
|
|
254
|
+
max_wait_time = timeout
|
|
255
|
+
is_completed = False
|
|
256
|
+
|
|
257
|
+
# Check if timeout is already zero/expired before starting
|
|
258
|
+
if timeout <= 0:
|
|
259
|
+
self.emit(
|
|
260
|
+
"error", RealityDefenderError("Polling timeout exceeded", "timeout")
|
|
261
|
+
)
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
while not is_completed and elapsed < max_wait_time:
|
|
265
|
+
try:
|
|
266
|
+
result = await self.get_result(request_id)
|
|
267
|
+
|
|
268
|
+
if result["status"] == "ANALYZING":
|
|
269
|
+
elapsed += polling_interval
|
|
270
|
+
await asyncio.sleep(polling_interval / 1000) # Convert to seconds
|
|
271
|
+
else:
|
|
272
|
+
# We have a final result
|
|
273
|
+
is_completed = True
|
|
274
|
+
self.emit("result", result)
|
|
275
|
+
except RealityDefenderError as error:
|
|
276
|
+
if error.code == "not_found":
|
|
277
|
+
# Result not ready yet, continue polling
|
|
278
|
+
elapsed += polling_interval
|
|
279
|
+
await asyncio.sleep(polling_interval / 1000) # Convert to seconds
|
|
280
|
+
else:
|
|
281
|
+
# Any other error is emitted and polling stops
|
|
282
|
+
is_completed = True
|
|
283
|
+
self.emit("error", error)
|
|
284
|
+
except Exception as error:
|
|
285
|
+
is_completed = True
|
|
286
|
+
self.emit("error", RealityDefenderError(str(error), "unknown_error"))
|
|
287
|
+
|
|
288
|
+
# Check if we timed out
|
|
289
|
+
if not is_completed and elapsed >= max_wait_time:
|
|
290
|
+
self.emit(
|
|
291
|
+
"error", RealityDefenderError("Polling timeout exceeded", "timeout")
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def _run_async(self, coro: Any) -> Any:
|
|
295
|
+
"""
|
|
296
|
+
Run an async coroutine in a new event loop
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
coro: Coroutine to run
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
The result of the coroutine
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
RealityDefenderError: If the async operation fails
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
# Get the current event loop, or create a new one if needed
|
|
309
|
+
try:
|
|
310
|
+
loop = asyncio.get_event_loop()
|
|
311
|
+
if loop.is_closed():
|
|
312
|
+
loop = asyncio.new_event_loop()
|
|
313
|
+
asyncio.set_event_loop(loop)
|
|
314
|
+
except RuntimeError:
|
|
315
|
+
# If there's no event loop in this thread, create one
|
|
316
|
+
loop = asyncio.new_event_loop()
|
|
317
|
+
asyncio.set_event_loop(loop)
|
|
318
|
+
|
|
319
|
+
if loop.is_running():
|
|
320
|
+
# If the loop is already running, use run_coroutine_threadsafe
|
|
321
|
+
# This will likely happen in a GUI application or web server
|
|
322
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
323
|
+
return future.result()
|
|
324
|
+
else:
|
|
325
|
+
# If not running, we can use the loop directly
|
|
326
|
+
return loop.run_until_complete(coro)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
# Convert any asyncio errors to our own error format
|
|
329
|
+
if isinstance(e, RealityDefenderError):
|
|
330
|
+
raise e
|
|
331
|
+
raise RealityDefenderError(
|
|
332
|
+
f"Async operation failed: {str(e)}", "unknown_error"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
async def cleanup(self) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Clean up resources used by the SDK
|
|
338
|
+
|
|
339
|
+
This should be called when you're done using the SDK to ensure all resources
|
|
340
|
+
are properly released.
|
|
341
|
+
"""
|
|
342
|
+
if hasattr(self, "client") and self.client:
|
|
343
|
+
await self.client.close()
|
|
344
|
+
|
|
345
|
+
def cleanup_sync(self) -> None:
|
|
346
|
+
"""
|
|
347
|
+
Synchronous version of cleanup
|
|
348
|
+
|
|
349
|
+
This should be called when you're done using the SDK to ensure all resources
|
|
350
|
+
are properly released.
|
|
351
|
+
"""
|
|
352
|
+
self._run_async(self.cleanup()) # Discard the return value
|
|
353
|
+
|
|
354
|
+
def __del__(self) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Destructor to ensure resources are cleaned up
|
|
357
|
+
|
|
358
|
+
This will attempt to close any open sessions when the object is garbage collected.
|
|
359
|
+
It's still recommended to explicitly call cleanup() or cleanup_sync() when done.
|
|
360
|
+
"""
|
|
361
|
+
try:
|
|
362
|
+
if hasattr(self, "client") and self.client:
|
|
363
|
+
# We can't use asyncio directly in __del__, so we'll try our best
|
|
364
|
+
# to clean up without relying on async operations
|
|
365
|
+
if hasattr(self.client, "session") and self.client.session:
|
|
366
|
+
# Mark session for closing on GC - it's not perfect but better than nothing
|
|
367
|
+
self.client.session._closed = True
|
|
368
|
+
except Exception:
|
|
369
|
+
# Suppress any errors during cleanup
|
|
370
|
+
pass
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP client for making requests to the Reality Defender API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple, TypedDict
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
|
|
10
|
+
from ..core.constants import DEFAULT_API_ENDPOINT
|
|
11
|
+
from ..errors import RealityDefenderError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClientConfig(TypedDict, total=False):
|
|
15
|
+
"""Configuration for HTTP client"""
|
|
16
|
+
|
|
17
|
+
api_key: str
|
|
18
|
+
base_url: Optional[str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HttpClient:
|
|
22
|
+
"""
|
|
23
|
+
HTTP client for Reality Defender API
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: ClientConfig):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the HTTP client
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config: Configuration including API key and base URL
|
|
32
|
+
"""
|
|
33
|
+
self.api_key = config["api_key"]
|
|
34
|
+
self.base_url = config.get("base_url") or DEFAULT_API_ENDPOINT
|
|
35
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
36
|
+
|
|
37
|
+
async def ensure_session(self) -> aiohttp.ClientSession:
|
|
38
|
+
"""
|
|
39
|
+
Ensure an HTTP session exists or create one
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Active aiohttp.ClientSession
|
|
43
|
+
"""
|
|
44
|
+
if self.session is None or self.session.closed:
|
|
45
|
+
self.session = aiohttp.ClientSession(
|
|
46
|
+
headers={
|
|
47
|
+
"X-API-KEY": self.api_key,
|
|
48
|
+
"Accept": "application/json",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
return self.session
|
|
52
|
+
|
|
53
|
+
async def get(
|
|
54
|
+
self, path: str, params: Optional[Dict[str, Any]] = None
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
Make a GET request to the API
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: API endpoint path
|
|
61
|
+
params: Query parameters
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Response data as dictionary
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
RealityDefenderError: If the request fails
|
|
68
|
+
"""
|
|
69
|
+
session = await self.ensure_session()
|
|
70
|
+
url = f"{self.base_url}{path}"
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
async with session.get(url, params=params) as response:
|
|
74
|
+
return await self._handle_response(response)
|
|
75
|
+
except aiohttp.ClientError as e:
|
|
76
|
+
raise RealityDefenderError(f"HTTP request failed: {str(e)}", "server_error")
|
|
77
|
+
|
|
78
|
+
async def post(
|
|
79
|
+
self,
|
|
80
|
+
path: str,
|
|
81
|
+
data: Optional[Dict[str, Any]] = None,
|
|
82
|
+
files: Optional[Dict[str, Tuple[str, bytes, str]]] = None,
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Make a POST request to the API
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
path: API endpoint path
|
|
89
|
+
data: Request body data
|
|
90
|
+
files: Files to upload
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Response data as dictionary
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
RealityDefenderError: If the request fails
|
|
97
|
+
"""
|
|
98
|
+
session = await self.ensure_session()
|
|
99
|
+
url = f"{self.base_url}{path}"
|
|
100
|
+
|
|
101
|
+
form_data = aiohttp.FormData()
|
|
102
|
+
|
|
103
|
+
# Add regular data
|
|
104
|
+
if data:
|
|
105
|
+
for key, value in data.items():
|
|
106
|
+
form_data.add_field(key, str(value))
|
|
107
|
+
|
|
108
|
+
# Add files
|
|
109
|
+
if files:
|
|
110
|
+
for field_name, (filename, content, content_type) in files.items():
|
|
111
|
+
form_data.add_field(
|
|
112
|
+
field_name, content, filename=filename, content_type=content_type
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
async with session.post(url, data=form_data) as response:
|
|
117
|
+
return await self._handle_response(response)
|
|
118
|
+
except aiohttp.ClientError as e:
|
|
119
|
+
raise RealityDefenderError(f"HTTP request failed: {str(e)}", "server_error")
|
|
120
|
+
|
|
121
|
+
async def _handle_response(
|
|
122
|
+
self, response: aiohttp.ClientResponse
|
|
123
|
+
) -> Dict[str, Any]:
|
|
124
|
+
"""
|
|
125
|
+
Handle HTTP response and check for errors
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
response: HTTP response from aiohttp
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Parsed JSON response
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
RealityDefenderError: If the response contains an error
|
|
135
|
+
"""
|
|
136
|
+
if response.status == 404:
|
|
137
|
+
raise RealityDefenderError("Resource not found", "not_found")
|
|
138
|
+
|
|
139
|
+
if response.status == 401:
|
|
140
|
+
raise RealityDefenderError(
|
|
141
|
+
"Unauthorized - check your API key", "unauthorized"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if response.status >= 400:
|
|
145
|
+
try:
|
|
146
|
+
error_data = await response.json()
|
|
147
|
+
message = error_data.get("error", {}).get("message", "Unknown error")
|
|
148
|
+
raise RealityDefenderError(f"API error: {message}", "server_error")
|
|
149
|
+
except json.JSONDecodeError:
|
|
150
|
+
error_text = await response.text()
|
|
151
|
+
raise RealityDefenderError(f"API error: {error_text}", "server_error")
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
result: Dict[str, Any] = await response.json()
|
|
155
|
+
return result
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
content = await response.text()
|
|
158
|
+
raise RealityDefenderError(
|
|
159
|
+
f"Invalid JSON response: {content}", "server_error"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
async def close(self) -> None:
|
|
163
|
+
"""Close the HTTP session if it exists"""
|
|
164
|
+
if self.session and not self.session.closed:
|
|
165
|
+
await self.session.close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def create_http_client(config: ClientConfig) -> HttpClient:
|
|
169
|
+
"""
|
|
170
|
+
Create a new HTTP client for the Reality Defender API
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
config: Client configuration including API key
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Configured HTTP client
|
|
177
|
+
"""
|
|
178
|
+
return HttpClient(config)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants used across the Reality Defender SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Default API endpoint
|
|
6
|
+
DEFAULT_API_ENDPOINT = "https://api.prd.realitydefender.xyz"
|
|
7
|
+
|
|
8
|
+
# API paths
|
|
9
|
+
API_PATHS = {
|
|
10
|
+
"SIGNED_URL": "/api/files/aws-presigned",
|
|
11
|
+
"MEDIA_RESULT": "/api/media/users",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Default polling interval in milliseconds
|
|
15
|
+
DEFAULT_POLLING_INTERVAL = 2000
|
|
16
|
+
|
|
17
|
+
# Default timeout in milliseconds (1 minute)
|
|
18
|
+
DEFAULT_TIMEOUT = 60000
|
|
19
|
+
|
|
20
|
+
# Default maximum polling attempts
|
|
21
|
+
DEFAULT_MAX_ATTEMPTS = 30
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event handling for asynchronous operations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, overload
|
|
6
|
+
|
|
7
|
+
from ..types.events import ErrorHandler, EventName, ResultHandler
|
|
8
|
+
|
|
9
|
+
# Create a generic type for event names that can be either the specific EventName type
|
|
10
|
+
# or any string (for testing purposes)
|
|
11
|
+
E = TypeVar('E', EventName, str)
|
|
12
|
+
CallbackT = TypeVar('CallbackT', bound=Callable[..., Any])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventEmitter:
|
|
16
|
+
"""
|
|
17
|
+
Simple event emitter for handling callbacks and events
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
"""Initialize the event emitter with empty event handlers"""
|
|
22
|
+
self._events: Dict[str, List[Callable]] = {}
|
|
23
|
+
|
|
24
|
+
@overload
|
|
25
|
+
def on(self, event: EventName, callback: Union[ResultHandler, ErrorHandler]) -> None: ...
|
|
26
|
+
|
|
27
|
+
@overload
|
|
28
|
+
def on(self, event: str, callback: Callable[..., Any]) -> None: ...
|
|
29
|
+
|
|
30
|
+
def on(self, event: str, callback: Callable[..., Any]) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Register an event handler
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
event: Event name to listen for
|
|
36
|
+
callback: Function to call when event occurs
|
|
37
|
+
"""
|
|
38
|
+
if event not in self._events:
|
|
39
|
+
self._events[event] = []
|
|
40
|
+
self._events[event].append(callback)
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def once(self, event: EventName, callback: Union[ResultHandler, ErrorHandler]) -> None: ...
|
|
44
|
+
|
|
45
|
+
@overload
|
|
46
|
+
def once(self, event: str, callback: Callable[..., Any]) -> None: ...
|
|
47
|
+
|
|
48
|
+
def once(self, event: str, callback: Callable[..., Any]) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Register an event handler that will be removed after being called once
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
event: Event name to listen for
|
|
54
|
+
callback: Function to call when event occurs
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def one_time_handler(*args: Any, **kwargs: Any) -> Any:
|
|
58
|
+
self.remove_listener(event, one_time_handler)
|
|
59
|
+
return callback(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
self.on(event, one_time_handler)
|
|
62
|
+
|
|
63
|
+
@overload
|
|
64
|
+
def emit(self, event: EventName, *args: Any, **kwargs: Any) -> bool: ...
|
|
65
|
+
|
|
66
|
+
@overload
|
|
67
|
+
def emit(self, event: str, *args: Any, **kwargs: Any) -> bool: ...
|
|
68
|
+
|
|
69
|
+
def emit(self, event: str, *args: Any, **kwargs: Any) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Emit an event with arguments
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
event: Event name to emit
|
|
75
|
+
*args: Arguments to pass to event handlers
|
|
76
|
+
**kwargs: Keyword arguments to pass to event handlers
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if the event had listeners, False otherwise
|
|
80
|
+
"""
|
|
81
|
+
if event not in self._events:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
for callback in self._events[event]:
|
|
85
|
+
callback(*args, **kwargs)
|
|
86
|
+
|
|
87
|
+
return len(self._events[event]) > 0
|
|
88
|
+
|
|
89
|
+
@overload
|
|
90
|
+
def remove_listener(self, event: EventName, callback: Callable[..., Any]) -> None: ...
|
|
91
|
+
|
|
92
|
+
@overload
|
|
93
|
+
def remove_listener(self, event: str, callback: Callable[..., Any]) -> None: ...
|
|
94
|
+
|
|
95
|
+
def remove_listener(self, event: str, callback: Callable[..., Any]) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Remove a specific event listener
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
event: Event name
|
|
101
|
+
callback: Function to remove
|
|
102
|
+
"""
|
|
103
|
+
if event not in self._events:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
self._events[event] = [cb for cb in self._events[event] if cb != callback]
|
|
107
|
+
|
|
108
|
+
@overload
|
|
109
|
+
def remove_all_listeners(self, event: Optional[EventName] = None) -> None: ...
|
|
110
|
+
|
|
111
|
+
@overload
|
|
112
|
+
def remove_all_listeners(self, event: Optional[str] = None) -> None: ...
|
|
113
|
+
|
|
114
|
+
def remove_all_listeners(self, event: Optional[str] = None) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Remove all listeners for an event or all events
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
event: Event name, or None to remove all events
|
|
120
|
+
"""
|
|
121
|
+
if event is not None:
|
|
122
|
+
self._events[event] = []
|
|
123
|
+
else:
|
|
124
|
+
self._events = {}
|