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.
@@ -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,5 @@
1
+ """
2
+ Version information for the Reality Defender SDK
3
+ """
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """
2
+ HTTP client for Reality Defender API interaction
3
+ """
4
+
5
+ from .http_client import create_http_client
6
+
7
+ __all__ = ["create_http_client"]
@@ -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 = {}