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.
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .env
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
+ ```
@@ -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