raidxai 0.1.0__tar.gz
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.
- raidxai-0.1.0/.gitignore +9 -0
- raidxai-0.1.0/LICENSE +7 -0
- raidxai-0.1.0/PKG-INFO +167 -0
- raidxai-0.1.0/README.md +143 -0
- raidxai-0.1.0/pyproject.toml +64 -0
- raidxai-0.1.0/src/raidxai/__init__.py +78 -0
- raidxai-0.1.0/src/raidxai/_core.py +125 -0
- raidxai-0.1.0/src/raidxai/_generated/__init__.py +0 -0
- raidxai-0.1.0/src/raidxai/_generated/models.py +460 -0
- raidxai-0.1.0/src/raidxai/_poll.py +69 -0
- raidxai-0.1.0/src/raidxai/_specs.py +186 -0
- raidxai-0.1.0/src/raidxai/aio.py +108 -0
- raidxai-0.1.0/src/raidxai/client.py +115 -0
- raidxai-0.1.0/src/raidxai/errors.py +88 -0
- raidxai-0.1.0/src/raidxai/files.py +37 -0
- raidxai-0.1.0/src/raidxai/models.py +63 -0
- raidxai-0.1.0/src/raidxai/resources/__init__.py +0 -0
- raidxai-0.1.0/src/raidxai/resources/audio.py +80 -0
- raidxai-0.1.0/src/raidxai/resources/fact_checking.py +135 -0
- raidxai-0.1.0/src/raidxai/resources/images.py +61 -0
- raidxai-0.1.0/src/raidxai/resources/video.py +141 -0
raidxai-0.1.0/.gitignore
ADDED
raidxai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) Raid AI. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Use of the Raid AI SDK is governed by the Raid AI Terms of Service:
|
|
4
|
+
https://raidxai.com
|
|
5
|
+
|
|
6
|
+
This software is provided to interact with the Raid AI API. It is distributed
|
|
7
|
+
"as is", without warranty of any kind, express or implied.
|
raidxai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: raidxai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Raid AI detection API — detect AI-generated and manipulated media, and fact-check claims.
|
|
5
|
+
Project-URL: Homepage, https://docs.raidxai.com
|
|
6
|
+
Project-URL: Documentation, https://docs.raidxai.com
|
|
7
|
+
Project-URL: Repository, https://github.com/Raid-AI-Corporation/Raid-AI-SDK
|
|
8
|
+
Author-email: Raid AI <info@raidxai.com>
|
|
9
|
+
License-Expression: LicenseRef-Raid-AI-Terms
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-detection,deepfake,fact-checking,forensics,raid,raidxai,sdk
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: httpx>=0.27
|
|
17
|
+
Requires-Dist: pydantic>=2.7
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: datamodel-code-generator>=0.25; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# raidxai
|
|
26
|
+
|
|
27
|
+
Official Python SDK for the **Raid AI** detection API — detect AI-generated and manipulated media
|
|
28
|
+
(images, audio, video) and fact-check media against the public record.
|
|
29
|
+
|
|
30
|
+
Sync and async clients, Python 3.10+. Full API reference: **https://docs.raidxai.com**.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install raidxai
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Authentication
|
|
39
|
+
|
|
40
|
+
Every request uses a developer token. Create one in the Raid AI dashboard
|
|
41
|
+
(**Settings → API keys**) and keep it server-side — never ship it in client code.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from raidxai import RaidClient, FileInput
|
|
45
|
+
|
|
46
|
+
raid = RaidClient(
|
|
47
|
+
api_key="<your-api-key>", # or os.environ["RAID_API_KEY"]
|
|
48
|
+
base_url="<raid-ai-api-url>", # required — or os.environ["RAID_API_BASE_URL"]
|
|
49
|
+
# timeout=60.0,
|
|
50
|
+
# max_retries=2,
|
|
51
|
+
# auth_header="bearer", # or "x-api-key"
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Each API key carries scopes (`image`, `audio`, `video`, `fact-check`) — a call to a modality
|
|
56
|
+
your key isn't scoped for raises `RaidApiError` with a `403` (`api_key.scope_missing`).
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Images (synchronous)
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
res = raid.images.process(FileInput.from_path("suspect.jpg"))
|
|
64
|
+
print(res.images[0].verdict, res.images[0].confidence)
|
|
65
|
+
|
|
66
|
+
# …or from a URL:
|
|
67
|
+
raid.images.process_from_url("https://example.com/photo.jpg")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Audio (synchronous)
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from raidxai import VoiceWorkflow
|
|
74
|
+
|
|
75
|
+
res = raid.audio.process(
|
|
76
|
+
FileInput.from_path("clip.mp3"),
|
|
77
|
+
workflow_type=VoiceWorkflow.AI_DETECTION_ONLY,
|
|
78
|
+
)
|
|
79
|
+
print(res.is_ai_detected, res.detection_confidence)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Video (asynchronous — submit then poll)
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# One call: submit and wait for the terminal verdict.
|
|
86
|
+
job = raid.video.submit_and_wait(
|
|
87
|
+
FileInput.from_path("clip.mp4"),
|
|
88
|
+
client_duration_seconds=42,
|
|
89
|
+
interval_seconds=3,
|
|
90
|
+
timeout_seconds=300,
|
|
91
|
+
)
|
|
92
|
+
print(job.status, job.result.verdict if job.result else None)
|
|
93
|
+
|
|
94
|
+
# …or drive it yourself:
|
|
95
|
+
submitted = raid.video.submit(FileInput.from_path("clip.mp4"), client_duration_seconds=42)
|
|
96
|
+
state = raid.video.get_job(submitted.job_id)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Fact-checking (asynchronous)
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
job = raid.fact_checking.submit_and_wait(
|
|
103
|
+
FileInput.from_path("photo.jpg"),
|
|
104
|
+
"image",
|
|
105
|
+
user_context="Claimed to be from the 2024 election.",
|
|
106
|
+
)
|
|
107
|
+
print(job.result.summary if job.result else None)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Async
|
|
111
|
+
|
|
112
|
+
Every resource has an async twin on `AsyncRaidClient` with the same method names:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import asyncio
|
|
116
|
+
from raidxai import AsyncRaidClient, FileInput
|
|
117
|
+
|
|
118
|
+
async def main():
|
|
119
|
+
async with AsyncRaidClient(api_key="<your-api-key>") as raid:
|
|
120
|
+
res = await raid.images.process(FileInput.from_path("photo.jpg"))
|
|
121
|
+
print(res.images[0].verdict)
|
|
122
|
+
|
|
123
|
+
asyncio.run(main())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Errors
|
|
127
|
+
|
|
128
|
+
Non-2xx responses raise `RaidApiError` (`.status`, `.code`, `.message`, `.body`, plus `.is_auth` /
|
|
129
|
+
`.is_payment_required` / `.is_rate_limited`). Transient `429`/`5xx` responses are retried automatically
|
|
130
|
+
with exponential backoff (`max_retries`). A `submit_and_wait` that never finishes raises `RaidTimeoutError`.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from raidxai import RaidApiError, RaidTimeoutError
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
raid.images.process(file)
|
|
137
|
+
except RaidApiError as err:
|
|
138
|
+
if err.is_auth:
|
|
139
|
+
print("bad or unscoped token:", err.code)
|
|
140
|
+
elif err.is_payment_required:
|
|
141
|
+
print("out of credits:", err.code)
|
|
142
|
+
else:
|
|
143
|
+
print(err.status, err.code, err.message)
|
|
144
|
+
except RaidTimeoutError as err:
|
|
145
|
+
print("job did not finish in time; last status:", err.last_status)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Types
|
|
149
|
+
|
|
150
|
+
Response models are Pydantic v2 classes generated from the API's OpenAPI spec (`ImageForensicsResponse`,
|
|
151
|
+
`VideoJob`, `Verdict`, `JobStatus`, …), so attributes are snake_case and tracked against the server contract.
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
uv venv --python 3.12 && source .venv/bin/activate
|
|
157
|
+
uv pip install -e ".[dev]"
|
|
158
|
+
./scripts/generate.sh # regenerate src/raidxai/_generated/models.py from ../spec/openapi.yaml
|
|
159
|
+
ruff check .
|
|
160
|
+
pytest
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Run the live smoke test against a real tier (auto-skips without the key):
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
RAID_API_KEY=<your-api-key> RAID_API_BASE_URL=<raid-ai-api-url> pytest tests/test_live_smoke.py
|
|
167
|
+
```
|
raidxai-0.1.0/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# raidxai
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the **Raid AI** detection API — detect AI-generated and manipulated media
|
|
4
|
+
(images, audio, video) and fact-check media against the public record.
|
|
5
|
+
|
|
6
|
+
Sync and async clients, Python 3.10+. Full API reference: **https://docs.raidxai.com**.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install raidxai
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Authentication
|
|
15
|
+
|
|
16
|
+
Every request uses a developer token. Create one in the Raid AI dashboard
|
|
17
|
+
(**Settings → API keys**) and keep it server-side — never ship it in client code.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from raidxai import RaidClient, FileInput
|
|
21
|
+
|
|
22
|
+
raid = RaidClient(
|
|
23
|
+
api_key="<your-api-key>", # or os.environ["RAID_API_KEY"]
|
|
24
|
+
base_url="<raid-ai-api-url>", # required — or os.environ["RAID_API_BASE_URL"]
|
|
25
|
+
# timeout=60.0,
|
|
26
|
+
# max_retries=2,
|
|
27
|
+
# auth_header="bearer", # or "x-api-key"
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Each API key carries scopes (`image`, `audio`, `video`, `fact-check`) — a call to a modality
|
|
32
|
+
your key isn't scoped for raises `RaidApiError` with a `403` (`api_key.scope_missing`).
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Images (synchronous)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
res = raid.images.process(FileInput.from_path("suspect.jpg"))
|
|
40
|
+
print(res.images[0].verdict, res.images[0].confidence)
|
|
41
|
+
|
|
42
|
+
# …or from a URL:
|
|
43
|
+
raid.images.process_from_url("https://example.com/photo.jpg")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Audio (synchronous)
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from raidxai import VoiceWorkflow
|
|
50
|
+
|
|
51
|
+
res = raid.audio.process(
|
|
52
|
+
FileInput.from_path("clip.mp3"),
|
|
53
|
+
workflow_type=VoiceWorkflow.AI_DETECTION_ONLY,
|
|
54
|
+
)
|
|
55
|
+
print(res.is_ai_detected, res.detection_confidence)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Video (asynchronous — submit then poll)
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# One call: submit and wait for the terminal verdict.
|
|
62
|
+
job = raid.video.submit_and_wait(
|
|
63
|
+
FileInput.from_path("clip.mp4"),
|
|
64
|
+
client_duration_seconds=42,
|
|
65
|
+
interval_seconds=3,
|
|
66
|
+
timeout_seconds=300,
|
|
67
|
+
)
|
|
68
|
+
print(job.status, job.result.verdict if job.result else None)
|
|
69
|
+
|
|
70
|
+
# …or drive it yourself:
|
|
71
|
+
submitted = raid.video.submit(FileInput.from_path("clip.mp4"), client_duration_seconds=42)
|
|
72
|
+
state = raid.video.get_job(submitted.job_id)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Fact-checking (asynchronous)
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
job = raid.fact_checking.submit_and_wait(
|
|
79
|
+
FileInput.from_path("photo.jpg"),
|
|
80
|
+
"image",
|
|
81
|
+
user_context="Claimed to be from the 2024 election.",
|
|
82
|
+
)
|
|
83
|
+
print(job.result.summary if job.result else None)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Async
|
|
87
|
+
|
|
88
|
+
Every resource has an async twin on `AsyncRaidClient` with the same method names:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import asyncio
|
|
92
|
+
from raidxai import AsyncRaidClient, FileInput
|
|
93
|
+
|
|
94
|
+
async def main():
|
|
95
|
+
async with AsyncRaidClient(api_key="<your-api-key>") as raid:
|
|
96
|
+
res = await raid.images.process(FileInput.from_path("photo.jpg"))
|
|
97
|
+
print(res.images[0].verdict)
|
|
98
|
+
|
|
99
|
+
asyncio.run(main())
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Errors
|
|
103
|
+
|
|
104
|
+
Non-2xx responses raise `RaidApiError` (`.status`, `.code`, `.message`, `.body`, plus `.is_auth` /
|
|
105
|
+
`.is_payment_required` / `.is_rate_limited`). Transient `429`/`5xx` responses are retried automatically
|
|
106
|
+
with exponential backoff (`max_retries`). A `submit_and_wait` that never finishes raises `RaidTimeoutError`.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from raidxai import RaidApiError, RaidTimeoutError
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
raid.images.process(file)
|
|
113
|
+
except RaidApiError as err:
|
|
114
|
+
if err.is_auth:
|
|
115
|
+
print("bad or unscoped token:", err.code)
|
|
116
|
+
elif err.is_payment_required:
|
|
117
|
+
print("out of credits:", err.code)
|
|
118
|
+
else:
|
|
119
|
+
print(err.status, err.code, err.message)
|
|
120
|
+
except RaidTimeoutError as err:
|
|
121
|
+
print("job did not finish in time; last status:", err.last_status)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Types
|
|
125
|
+
|
|
126
|
+
Response models are Pydantic v2 classes generated from the API's OpenAPI spec (`ImageForensicsResponse`,
|
|
127
|
+
`VideoJob`, `Verdict`, `JobStatus`, …), so attributes are snake_case and tracked against the server contract.
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
uv venv --python 3.12 && source .venv/bin/activate
|
|
133
|
+
uv pip install -e ".[dev]"
|
|
134
|
+
./scripts/generate.sh # regenerate src/raidxai/_generated/models.py from ../spec/openapi.yaml
|
|
135
|
+
ruff check .
|
|
136
|
+
pytest
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Run the live smoke test against a real tier (auto-skips without the key):
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
RAID_API_KEY=<your-api-key> RAID_API_BASE_URL=<raid-ai-api-url> pytest tests/test_live_smoke.py
|
|
143
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "raidxai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the Raid AI detection API — detect AI-generated and manipulated media, and fact-check claims."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "LicenseRef-Raid-AI-Terms"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Raid AI", email = "info@raidxai.com" }]
|
|
14
|
+
keywords = ["raid", "raidxai", "deepfake", "ai-detection", "forensics", "fact-checking", "sdk"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"httpx>=0.27",
|
|
22
|
+
"pydantic>=2.7",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://docs.raidxai.com"
|
|
27
|
+
Documentation = "https://docs.raidxai.com"
|
|
28
|
+
Repository = "https://github.com/Raid-AI-Corporation/Raid-AI-SDK"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.0",
|
|
33
|
+
"pytest-asyncio>=0.23",
|
|
34
|
+
"ruff>=0.6",
|
|
35
|
+
"datamodel-code-generator>=0.25",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/raidxai"]
|
|
40
|
+
|
|
41
|
+
# Ship only the public SDK in the source distribution — no tests, internal dev
|
|
42
|
+
# scripts, or dotfiles. (The wheel already ships only the `raidxai` package.)
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
ignore-vcs = true
|
|
45
|
+
only-include = ["src/raidxai", "README.md", "LICENSE"]
|
|
46
|
+
exclude = ["**/.gitignore", "**/__pycache__"]
|
|
47
|
+
|
|
48
|
+
# target-version matches the package's requires-python floor (3.10), so the
|
|
49
|
+
# linter never suggests 3.11/3.12-only syntax (StrEnum, PEP 695 generics) that
|
|
50
|
+
# would break older supported interpreters. line-length 100 follows the workspace.
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
target-version = "py310"
|
|
53
|
+
line-length = 100
|
|
54
|
+
src = ["src", "tests"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
"src/raidxai/_generated/*" = ["E501", "UP", "B", "I"]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
asyncio_mode = "auto"
|
|
64
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Official Python SDK for the Raid AI detection API.
|
|
2
|
+
|
|
3
|
+
Detect AI-generated and manipulated media (images, audio, video) and fact-check
|
|
4
|
+
media against the public record.
|
|
5
|
+
|
|
6
|
+
from raidxai import RaidClient, FileInput
|
|
7
|
+
|
|
8
|
+
raid = RaidClient(api_key="<your-api-key>")
|
|
9
|
+
res = raid.images.process(FileInput.from_path("photo.jpg"))
|
|
10
|
+
print(res.images[0].verdict)
|
|
11
|
+
|
|
12
|
+
Full reference: https://docs.raidxai.com
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from ._specs import PagedResult
|
|
18
|
+
from .aio import AsyncRaidClient
|
|
19
|
+
from .client import RaidClient
|
|
20
|
+
from .errors import RaidApiError, RaidTimeoutError
|
|
21
|
+
from .files import FileInput
|
|
22
|
+
from .models import (
|
|
23
|
+
Claim,
|
|
24
|
+
FactCheckJob,
|
|
25
|
+
FactCheckResult,
|
|
26
|
+
Generators,
|
|
27
|
+
ImageFace,
|
|
28
|
+
ImageFaceAnalysis,
|
|
29
|
+
ImageForensicsResponse,
|
|
30
|
+
ImageResult,
|
|
31
|
+
JobStatus,
|
|
32
|
+
JobSubmitResponse,
|
|
33
|
+
Modality,
|
|
34
|
+
ProvenanceItem,
|
|
35
|
+
ProvenanceUrl,
|
|
36
|
+
Verdict,
|
|
37
|
+
VideoJob,
|
|
38
|
+
VideoProvider,
|
|
39
|
+
VideoResult,
|
|
40
|
+
VideoSubmitResponse,
|
|
41
|
+
VoiceAnalysisResponse,
|
|
42
|
+
VoiceWorkflowType,
|
|
43
|
+
)
|
|
44
|
+
from .resources.audio import VoiceWorkflow
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"RaidClient",
|
|
50
|
+
"AsyncRaidClient",
|
|
51
|
+
"FileInput",
|
|
52
|
+
"PagedResult",
|
|
53
|
+
"RaidApiError",
|
|
54
|
+
"RaidTimeoutError",
|
|
55
|
+
"VoiceWorkflow",
|
|
56
|
+
# models
|
|
57
|
+
"Claim",
|
|
58
|
+
"FactCheckJob",
|
|
59
|
+
"FactCheckResult",
|
|
60
|
+
"Generators",
|
|
61
|
+
"ImageFace",
|
|
62
|
+
"ImageFaceAnalysis",
|
|
63
|
+
"ImageForensicsResponse",
|
|
64
|
+
"ImageResult",
|
|
65
|
+
"JobStatus",
|
|
66
|
+
"JobSubmitResponse",
|
|
67
|
+
"Modality",
|
|
68
|
+
"ProvenanceItem",
|
|
69
|
+
"ProvenanceUrl",
|
|
70
|
+
"Verdict",
|
|
71
|
+
"VideoJob",
|
|
72
|
+
"VideoProvider",
|
|
73
|
+
"VideoResult",
|
|
74
|
+
"VideoSubmitResponse",
|
|
75
|
+
"VoiceAnalysisResponse",
|
|
76
|
+
"VoiceWorkflowType",
|
|
77
|
+
"__version__",
|
|
78
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Transport-agnostic request plumbing shared by the sync and async clients.
|
|
2
|
+
|
|
3
|
+
A :class:`RequestSpec` is a pure description of one HTTP call (built by the
|
|
4
|
+
functions in ``_specs.py``). The sync and async clients each know how to execute
|
|
5
|
+
a spec with retries; everything else — header/URL building, response parsing,
|
|
6
|
+
error mapping, backoff math — lives here so both paths behave identically.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json as _json
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from email.utils import parsedate_to_datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from .errors import RaidApiError, extract_error
|
|
20
|
+
|
|
21
|
+
# A multipart file part: (field_name, (filename, data, content_type)).
|
|
22
|
+
FilePart = tuple[str, tuple[str, bytes, str]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RequestSpec:
|
|
27
|
+
method: str
|
|
28
|
+
path: str
|
|
29
|
+
json: Any = None
|
|
30
|
+
data: dict[str, str] = field(default_factory=dict)
|
|
31
|
+
files: list[FilePart] = field(default_factory=list)
|
|
32
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
def httpx_kwargs(self) -> dict[str, Any]:
|
|
35
|
+
kwargs: dict[str, Any] = {}
|
|
36
|
+
if self.files:
|
|
37
|
+
kwargs["files"] = self.files
|
|
38
|
+
if self.data:
|
|
39
|
+
kwargs["data"] = self.data
|
|
40
|
+
elif self.json is not None:
|
|
41
|
+
kwargs["json"] = self.json
|
|
42
|
+
if self.params:
|
|
43
|
+
kwargs["params"] = {k: v for k, v in self.params.items() if v is not None}
|
|
44
|
+
return kwargs
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_headers(api_key: str, auth_header: str) -> dict[str, str]:
|
|
48
|
+
headers = {"Accept": "application/json"}
|
|
49
|
+
if auth_header == "x-api-key":
|
|
50
|
+
headers["X-Api-Key"] = api_key
|
|
51
|
+
else:
|
|
52
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
53
|
+
return headers
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_retryable_status(status: int) -> bool:
|
|
57
|
+
return status == 429 or status >= 500
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def backoff_seconds(attempt: int) -> float:
|
|
61
|
+
"""0.5s, 1s, 2s, … (attempt is 1-based)."""
|
|
62
|
+
return 0.5 * (2 ** (attempt - 1))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def retry_after_seconds(response: httpx.Response) -> float | None:
|
|
66
|
+
"""Parse a ``Retry-After`` header — either delta-seconds or an HTTP-date.
|
|
67
|
+
|
|
68
|
+
Returns ``None`` when absent/unparseable; ``0.0`` is a valid "retry now" value
|
|
69
|
+
and is preserved (callers must not treat it as falsy).
|
|
70
|
+
"""
|
|
71
|
+
header = response.headers.get("retry-after")
|
|
72
|
+
if not header:
|
|
73
|
+
return None
|
|
74
|
+
try:
|
|
75
|
+
return max(0.0, float(header))
|
|
76
|
+
except ValueError:
|
|
77
|
+
pass
|
|
78
|
+
# HTTP-date form, e.g. "Wed, 16 Jul 2026 12:00:05 GMT" (sent by many proxies/CDNs).
|
|
79
|
+
try:
|
|
80
|
+
when = parsedate_to_datetime(header)
|
|
81
|
+
except (TypeError, ValueError):
|
|
82
|
+
return None
|
|
83
|
+
if when is None:
|
|
84
|
+
return None
|
|
85
|
+
if when.tzinfo is None:
|
|
86
|
+
when = when.replace(tzinfo=timezone.utc)
|
|
87
|
+
return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def retry_delay(response: httpx.Response, attempt: int) -> float:
|
|
91
|
+
"""The delay before a retry: the server's ``Retry-After`` if given, else backoff.
|
|
92
|
+
|
|
93
|
+
Uses an explicit ``None`` check so a ``Retry-After: 0`` (retry immediately) is honored
|
|
94
|
+
rather than falling through to exponential backoff.
|
|
95
|
+
"""
|
|
96
|
+
after = retry_after_seconds(response)
|
|
97
|
+
return after if after is not None else backoff_seconds(attempt)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_response(response: httpx.Response, path: str) -> Any:
|
|
101
|
+
"""Return the parsed JSON body, or raise :class:`RaidApiError` on a non-2xx."""
|
|
102
|
+
request_id = response.headers.get("request-id") or response.headers.get("x-request-id")
|
|
103
|
+
|
|
104
|
+
if response.status_code == 204:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
text = response.text
|
|
108
|
+
parsed: Any = text
|
|
109
|
+
if "application/json" in response.headers.get("content-type", "") and text:
|
|
110
|
+
try:
|
|
111
|
+
parsed = _json.loads(text)
|
|
112
|
+
except ValueError:
|
|
113
|
+
parsed = text
|
|
114
|
+
|
|
115
|
+
if not response.is_success:
|
|
116
|
+
message, code = extract_error(parsed)
|
|
117
|
+
raise RaidApiError(
|
|
118
|
+
response.status_code,
|
|
119
|
+
message or f"API error {response.status_code}: {response.reason_phrase}",
|
|
120
|
+
code=code,
|
|
121
|
+
body=parsed,
|
|
122
|
+
request_id=request_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return parsed
|
|
File without changes
|