primate-intelligence 0.1.0__tar.gz → 0.2.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.
- primate_intelligence-0.2.0/CHANGELOG.md +23 -0
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/PKG-INFO +6 -3
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/README.md +5 -2
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/primate_intelligence/__init__.py +5 -1
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/primate_intelligence/_client.py +2 -2
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/primate_intelligence/_error_registry.json +35 -0
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/primate_intelligence/_resources.py +129 -3
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/pyproject.toml +1 -1
- primate_intelligence-0.2.0/tests/test_parity.py +156 -0
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/.gitignore +0 -0
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/primate_intelligence/_errors.py +0 -0
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/primate_intelligence/webhooks.py +0 -0
- {primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/tests/test_sdk.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog — primate-intelligence (Python SDK)
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-07-28
|
|
4
|
+
|
|
5
|
+
Parity release with the 2026-07-22 → 2026-07-28 API changes (PRI-480/482 rounds + streaming DX). Behavior verified against the live OpenAPI document and recorded live responses. Dict-based responses mean no breaking type changes — this release is additive.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `client.credits.retrieve(limit=, before=)` — `GET /v1/credits`: balance in billable seconds + paginated transaction ledger (each debit carries its analysis/stream `source_id`). Check before submitting work to avoid a 402.
|
|
10
|
+
- `client.credit_pricing.retrieve()` — `GET /v1/credit-pricing` (public): `price_per_second_cents`, grants, purchase presets.
|
|
11
|
+
- `client.analyses.validate_only(video_id=, prompt=)` — dry-run (`validate_only: true`): returns an `analysis_preview` with `assessable`, `estimated_seconds`, `estimated_cost_usd`. Costs nothing.
|
|
12
|
+
- `client.analyses.create_batch(video_id=, prompts=[...])` — `POST /v1/analyses/batch`: 2–10 prompts, first full price, each additional at 50%. Returns `analysis_batch` with all analyses + pricing summary.
|
|
13
|
+
- `client.analyses.validate_batch(...)` — batch dry-run returning `analysis_batch_preview` with per-prompt `discount_pct` (0 or 50) and estimates.
|
|
14
|
+
- Auto `Idempotency-Key` now also covers `POST /v1/analyses/batch`.
|
|
15
|
+
|
|
16
|
+
### Documentation (docstrings — dict responses, no code change needed)
|
|
17
|
+
|
|
18
|
+
- `create_and_wait` / `create`: documents `result.detected_count` (count-intent queries; 0 = nothing found), `result.indeterminate_reason` (`low_confidence | nothing_detected | unsupported_query_form | duration_mismatch`), nullable `result.video_duration_s`, and the real `usage` shape `{billed_seconds, credit_balance_after}` (immutable post-settlement snapshot).
|
|
19
|
+
- `streams.end`: documents the honest `end_reason` vocabulary (`completed | canceled | insufficient_credits | error | timeout | ice_failed | media_timeout`), `results_summary.result_frames` (the `frames` alias is removed ~2026-08-28), `failure_diagnostic`, and the metering-tick field `session_remaining_s` (the `balance_s` alias is removed ~2026-08-28).
|
|
20
|
+
|
|
21
|
+
## 0.1.0
|
|
22
|
+
|
|
23
|
+
Initial release.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: primate-intelligence
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Official Python SDK for the Primate Vision API (Primate Intelligence). Video scene understanding: upload, analyze, stream.
|
|
5
5
|
Project-URL: Homepage, https://primateintelligence.ai/docs
|
|
6
6
|
Project-URL: Repository, https://github.com/Primate-Intelligence/primate-intelligence-api
|
|
@@ -57,12 +57,15 @@ curl -X POST https://api.primateintelligence.ai/v1/sandbox
|
|
|
57
57
|
|
|
58
58
|
## Features
|
|
59
59
|
|
|
60
|
-
- **Typed resource namespaces** for the full `/v1` surface: `videos`, `analyses`, `streams`, `client_tokens`, `models`, `usage`, `errors`, `webhook_endpoints`.
|
|
60
|
+
- **Typed resource namespaces** for the full `/v1` surface: `videos`, `analyses`, `streams`, `client_tokens`, `models`, `usage`, `credits`, `credit_pricing`, `errors`, `webhook_endpoints`.
|
|
61
61
|
- **Automatic retries** driven by the [error registry](https://primateintelligence.ai/docs/errors) `retryable` flags + `Retry-After`.
|
|
62
62
|
- **Auto idempotency keys** (UUIDv4) on all create calls.
|
|
63
63
|
- **`create_and_wait()`** — create an analysis and block until a terminal state (uses `Prefer: wait` server-side, then polls).
|
|
64
64
|
- **`videos.upload()`** — create + presigned S3 PUT + complete, in one call.
|
|
65
65
|
- **Pagination iterators** — `for video in client.videos.iter(): …`.
|
|
66
|
+
- **`analyses.validate_only()`** — dry-run a prompt: compiled query + `assessable` + cost estimate, without creating an analysis or spending credits.
|
|
67
|
+
- **`analyses.create_batch()` / `validate_batch()`** — 2–10 prompts against one video; first full price, each additional at 50%.
|
|
68
|
+
- **`credits.retrieve()`** — balance + per-analysis transaction ledger (`GET /v1/credits`); **`credit_pricing.retrieve()`** — public pricing config.
|
|
66
69
|
- **Webhook verification** — `verify_webhook()` implements [Standard Webhooks](https://www.standardwebhooks.com) with constant-time compare + replay-window checks.
|
|
67
70
|
|
|
68
71
|
## Error handling
|
|
@@ -76,7 +79,7 @@ try:
|
|
|
76
79
|
except PrimateError as err:
|
|
77
80
|
print(err.code, err.status, err.request_id, err.docs_url)
|
|
78
81
|
if err.code == "insufficient_credits":
|
|
79
|
-
|
|
82
|
+
credits = client.credits.retrieve() # balance_seconds + transaction ledger
|
|
80
83
|
# → prompt the account owner to top up or enable auto-refill
|
|
81
84
|
```
|
|
82
85
|
|
|
@@ -35,12 +35,15 @@ curl -X POST https://api.primateintelligence.ai/v1/sandbox
|
|
|
35
35
|
|
|
36
36
|
## Features
|
|
37
37
|
|
|
38
|
-
- **Typed resource namespaces** for the full `/v1` surface: `videos`, `analyses`, `streams`, `client_tokens`, `models`, `usage`, `errors`, `webhook_endpoints`.
|
|
38
|
+
- **Typed resource namespaces** for the full `/v1` surface: `videos`, `analyses`, `streams`, `client_tokens`, `models`, `usage`, `credits`, `credit_pricing`, `errors`, `webhook_endpoints`.
|
|
39
39
|
- **Automatic retries** driven by the [error registry](https://primateintelligence.ai/docs/errors) `retryable` flags + `Retry-After`.
|
|
40
40
|
- **Auto idempotency keys** (UUIDv4) on all create calls.
|
|
41
41
|
- **`create_and_wait()`** — create an analysis and block until a terminal state (uses `Prefer: wait` server-side, then polls).
|
|
42
42
|
- **`videos.upload()`** — create + presigned S3 PUT + complete, in one call.
|
|
43
43
|
- **Pagination iterators** — `for video in client.videos.iter(): …`.
|
|
44
|
+
- **`analyses.validate_only()`** — dry-run a prompt: compiled query + `assessable` + cost estimate, without creating an analysis or spending credits.
|
|
45
|
+
- **`analyses.create_batch()` / `validate_batch()`** — 2–10 prompts against one video; first full price, each additional at 50%.
|
|
46
|
+
- **`credits.retrieve()`** — balance + per-analysis transaction ledger (`GET /v1/credits`); **`credit_pricing.retrieve()`** — public pricing config.
|
|
44
47
|
- **Webhook verification** — `verify_webhook()` implements [Standard Webhooks](https://www.standardwebhooks.com) with constant-time compare + replay-window checks.
|
|
45
48
|
|
|
46
49
|
## Error handling
|
|
@@ -54,7 +57,7 @@ try:
|
|
|
54
57
|
except PrimateError as err:
|
|
55
58
|
print(err.code, err.status, err.request_id, err.docs_url)
|
|
56
59
|
if err.code == "insufficient_credits":
|
|
57
|
-
|
|
60
|
+
credits = client.credits.retrieve() # balance_seconds + transaction ledger
|
|
58
61
|
# → prompt the account owner to top up or enable auto-refill
|
|
59
62
|
```
|
|
60
63
|
|
|
@@ -23,6 +23,8 @@ from ._errors import PrimateError, PrimateTimeoutError, registry_flags
|
|
|
23
23
|
from ._resources import (
|
|
24
24
|
Analyses,
|
|
25
25
|
ClientTokens,
|
|
26
|
+
CreditPricing,
|
|
27
|
+
Credits,
|
|
26
28
|
Errors,
|
|
27
29
|
Models,
|
|
28
30
|
Streams,
|
|
@@ -33,7 +35,7 @@ from ._resources import (
|
|
|
33
35
|
)
|
|
34
36
|
from .webhooks import verify_webhook, verify_webhook_request
|
|
35
37
|
|
|
36
|
-
__version__ = "0.
|
|
38
|
+
__version__ = "0.2.0"
|
|
37
39
|
__all__ = [
|
|
38
40
|
"Primate",
|
|
39
41
|
"PrimateError",
|
|
@@ -68,6 +70,8 @@ class Primate:
|
|
|
68
70
|
self.client_tokens = ClientTokens(self.http)
|
|
69
71
|
self.models = Models(self.http)
|
|
70
72
|
self.usage = Usage(self.http)
|
|
73
|
+
self.credits = Credits(self.http)
|
|
74
|
+
self.credit_pricing = CreditPricing(self.http)
|
|
71
75
|
self.errors = Errors(self.http)
|
|
72
76
|
self.webhook_endpoints = WebhookEndpoints(self.http)
|
|
73
77
|
self.test_fixtures = TestFixtures(self.http)
|
|
@@ -17,8 +17,8 @@ import httpx
|
|
|
17
17
|
from ._errors import PrimateError
|
|
18
18
|
|
|
19
19
|
DEFAULT_BASE_URL = "https://api.primateintelligence.ai"
|
|
20
|
-
_IDEMPOTENT_CREATE_PATHS = ("/v1/videos", "/v1/analyses", "/v1/webhook_endpoints")
|
|
21
|
-
_USER_AGENT = "primate-intelligence-sdk-py/0.
|
|
20
|
+
_IDEMPOTENT_CREATE_PATHS = ("/v1/videos", "/v1/analyses", "/v1/analyses/batch", "/v1/webhook_endpoints")
|
|
21
|
+
_USER_AGENT = "primate-intelligence-sdk-py/0.2.0"
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class HttpClient:
|
{primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/primate_intelligence/_error_registry.json
RENAMED
|
@@ -50,6 +50,13 @@
|
|
|
50
50
|
"idem": false,
|
|
51
51
|
"docs_url": "https://primateintelligence.ai/docs/errors#test_mode_only"
|
|
52
52
|
},
|
|
53
|
+
"test_key_fixture_only": {
|
|
54
|
+
"kind": "http",
|
|
55
|
+
"status": 403,
|
|
56
|
+
"retryable": false,
|
|
57
|
+
"idem": false,
|
|
58
|
+
"docs_url": "https://primateintelligence.ai/docs/errors#test_key_fixture_only"
|
|
59
|
+
},
|
|
53
60
|
"validation_failed": {
|
|
54
61
|
"kind": "http",
|
|
55
62
|
"status": 400,
|
|
@@ -225,6 +232,13 @@
|
|
|
225
232
|
"idem": false,
|
|
226
233
|
"docs_url": "https://primateintelligence.ai/docs/errors#insufficient_credits"
|
|
227
234
|
},
|
|
235
|
+
"grant_exhausted": {
|
|
236
|
+
"kind": "http",
|
|
237
|
+
"status": 402,
|
|
238
|
+
"retryable": false,
|
|
239
|
+
"idem": false,
|
|
240
|
+
"docs_url": "https://primateintelligence.ai/docs/errors#grant_exhausted"
|
|
241
|
+
},
|
|
228
242
|
"capacity_exhausted": {
|
|
229
243
|
"kind": "http",
|
|
230
244
|
"status": 503,
|
|
@@ -239,6 +253,20 @@
|
|
|
239
253
|
"idem": true,
|
|
240
254
|
"docs_url": "https://primateintelligence.ai/docs/errors#sandbox_limit_exceeded"
|
|
241
255
|
},
|
|
256
|
+
"upgrade_limit_exceeded": {
|
|
257
|
+
"kind": "http",
|
|
258
|
+
"status": 429,
|
|
259
|
+
"retryable": false,
|
|
260
|
+
"idem": false,
|
|
261
|
+
"docs_url": "https://primateintelligence.ai/docs/errors#upgrade_limit_exceeded"
|
|
262
|
+
},
|
|
263
|
+
"device_code_expired": {
|
|
264
|
+
"kind": "http",
|
|
265
|
+
"status": 410,
|
|
266
|
+
"retryable": false,
|
|
267
|
+
"idem": false,
|
|
268
|
+
"docs_url": "https://primateintelligence.ai/docs/errors#device_code_expired"
|
|
269
|
+
},
|
|
242
270
|
"inference_unavailable": {
|
|
243
271
|
"kind": "http",
|
|
244
272
|
"status": 503,
|
|
@@ -246,6 +274,13 @@
|
|
|
246
274
|
"idem": true,
|
|
247
275
|
"docs_url": "https://primateintelligence.ai/docs/errors#inference_unavailable"
|
|
248
276
|
},
|
|
277
|
+
"upstream_error": {
|
|
278
|
+
"kind": "http",
|
|
279
|
+
"status": 502,
|
|
280
|
+
"retryable": false,
|
|
281
|
+
"idem": false,
|
|
282
|
+
"docs_url": "https://primateintelligence.ai/docs/errors#upstream_error"
|
|
283
|
+
},
|
|
249
284
|
"db_unavailable": {
|
|
250
285
|
"kind": "http",
|
|
251
286
|
"status": 503,
|
{primate_intelligence-0.1.0 → primate_intelligence-0.2.0}/primate_intelligence/_resources.py
RENAMED
|
@@ -70,9 +70,59 @@ class Analyses:
|
|
|
70
70
|
self._http = http
|
|
71
71
|
|
|
72
72
|
def create(self, **params: Any) -> dict[str, Any]:
|
|
73
|
-
"""POST /v1/analyses.
|
|
73
|
+
"""POST /v1/analyses.
|
|
74
|
+
|
|
75
|
+
Pass ``validate_only=True`` for a dry run: compiles the prompt, reports
|
|
76
|
+
whether the query is assessable, and estimates duration/cost — WITHOUT
|
|
77
|
+
creating an analysis or spending credits. The response is then an
|
|
78
|
+
``analysis_preview`` object (``{object, query, parse_mode, assessable,
|
|
79
|
+
video_duration_s, estimated_seconds, estimated_cost_usd}``), not an
|
|
80
|
+
analysis. Prefer :meth:`validate_only` which reads more clearly.
|
|
81
|
+
"""
|
|
74
82
|
return self._http.post("/v1/analyses", json_body=params)
|
|
75
83
|
|
|
84
|
+
def validate_only(self, **params: Any) -> dict[str, Any]:
|
|
85
|
+
"""Dry-run a prompt (POST /v1/analyses with ``validate_only: true``).
|
|
86
|
+
|
|
87
|
+
Costs nothing: no analysis is created, no credits spent. Returns an
|
|
88
|
+
``analysis_preview`` dict::
|
|
89
|
+
|
|
90
|
+
{object: "analysis_preview", query, parse_mode: "llm"|"heuristic"|"client",
|
|
91
|
+
assessable: bool, video_duration_s: float|None,
|
|
92
|
+
estimated_seconds: int|None, estimated_cost_usd: float|None}
|
|
93
|
+
|
|
94
|
+
Estimates are None when the video duration is not yet known.
|
|
95
|
+
"""
|
|
96
|
+
params["validate_only"] = True
|
|
97
|
+
return self._http.post("/v1/analyses", json_body=params)
|
|
98
|
+
|
|
99
|
+
def create_batch(self, **params: Any) -> dict[str, Any]:
|
|
100
|
+
"""POST /v1/analyses/batch — 2–10 prompts against the same video.
|
|
101
|
+
|
|
102
|
+
First prompt is billed at full price; each additional prompt at 50%.
|
|
103
|
+
Required: ``video_id`` and ``prompts`` (list of 2–10 strings).
|
|
104
|
+
Returns an ``analysis_batch``::
|
|
105
|
+
|
|
106
|
+
{object: "analysis_batch", id, video_id, analyses: […], pricing:
|
|
107
|
+
{full_price_prompts, discounted_prompts, discount_pct,
|
|
108
|
+
estimated_total_seconds, estimated_total_cost_usd}}
|
|
109
|
+
|
|
110
|
+
Poll each returned analysis id individually. Pass ``validate_only=True``
|
|
111
|
+
(or use :meth:`validate_batch`) for a cost-estimate dry-run that returns
|
|
112
|
+
an ``analysis_batch_preview`` with per-prompt ``discount_pct`` (0 or 50)
|
|
113
|
+
and estimates — no credits reserved, no jobs created.
|
|
114
|
+
"""
|
|
115
|
+
return self._http.post("/v1/analyses/batch", json_body=params)
|
|
116
|
+
|
|
117
|
+
def validate_batch(self, **params: Any) -> dict[str, Any]:
|
|
118
|
+
"""Dry-run a batch (POST /v1/analyses/batch with ``validate_only: true``).
|
|
119
|
+
|
|
120
|
+
Parses all prompts and returns an ``analysis_batch_preview`` with
|
|
121
|
+
per-prompt estimates. No credits reserved, no jobs created.
|
|
122
|
+
"""
|
|
123
|
+
params["validate_only"] = True
|
|
124
|
+
return self._http.post("/v1/analyses/batch", json_body=params)
|
|
125
|
+
|
|
76
126
|
def retrieve(self, analysis_id: str) -> dict[str, Any]:
|
|
77
127
|
return self._http.get(f"/v1/analyses/{analysis_id}")
|
|
78
128
|
|
|
@@ -96,6 +146,15 @@ class Analyses:
|
|
|
96
146
|
|
|
97
147
|
Uses ``Prefer: wait`` server-side sync sugar for the first leg
|
|
98
148
|
(cap 120s), then polls with the given interval until ``timeout_s``.
|
|
149
|
+
|
|
150
|
+
On completion, ``result`` carries ``answer`` (yes|no|indeterminate),
|
|
151
|
+
``confidence``, ``clips``, ``video_duration_s`` (may be None), and —
|
|
152
|
+
when relevant — ``detected_count`` (count-intent queries; 0 = nothing
|
|
153
|
+
found) and ``indeterminate_reason`` (low_confidence |
|
|
154
|
+
nothing_detected | unsupported_query_form | duration_mismatch).
|
|
155
|
+
``usage`` (terminal only) is ``{billed_seconds, credit_balance_after}``
|
|
156
|
+
— an immutable snapshot of the balance after THIS analysis settled.
|
|
157
|
+
Do not pass ``validate_only`` here — use :meth:`validate_only`.
|
|
99
158
|
"""
|
|
100
159
|
prefer_wait = min(int(timeout_s), 120)
|
|
101
160
|
deadline = time.monotonic() + timeout_s
|
|
@@ -130,7 +189,27 @@ class Streams:
|
|
|
130
189
|
return self._http.paginate("/v1/streams", params=params)
|
|
131
190
|
|
|
132
191
|
def end(self, stream_id: str) -> dict[str, Any]:
|
|
133
|
-
"""POST /v1/streams/{id}/end (idempotent).
|
|
192
|
+
"""POST /v1/streams/{id}/end (idempotent).
|
|
193
|
+
|
|
194
|
+
The terminal stream resource carries:
|
|
195
|
+
|
|
196
|
+
- ``end_reason``: completed | canceled | insufficient_credits | error |
|
|
197
|
+
timeout | ice_failed | media_timeout. A stream that never went live
|
|
198
|
+
is never "completed"; ice_failed/media_timeout/canceled bill 0.
|
|
199
|
+
- ``results_summary``: ``{result_frames, frames (deprecated alias,
|
|
200
|
+
removed ~2026-08-28 — read result_frames), last_detections}``.
|
|
201
|
+
result_frames counts result EVENTS emitted (sampled, not per-frame).
|
|
202
|
+
- ``failure_diagnostic``: on ice_failed (and some media_timeout cases),
|
|
203
|
+
``{local_candidates, hint}`` — the server-side ICE candidate summary.
|
|
204
|
+
- ``usage``: ``{billed_seconds, credit_balance_after}`` — live-clock
|
|
205
|
+
seconds; join/negotiation free.
|
|
206
|
+
|
|
207
|
+
While live, the signaling WS sends 5s metering ticks
|
|
208
|
+
``{elapsed_s, billed_s, session_remaining_s}`` (``balance_s`` is a
|
|
209
|
+
deprecated alias of session_remaining_s, removed ~2026-08-28) and may
|
|
210
|
+
push a ``warning`` with ``code: "no_media_frames"`` when transport
|
|
211
|
+
connected but no decodable frames arrived.
|
|
212
|
+
"""
|
|
134
213
|
return self._http.post(f"/v1/streams/{stream_id}/end")
|
|
135
214
|
|
|
136
215
|
|
|
@@ -156,10 +235,57 @@ class Usage:
|
|
|
156
235
|
self._http = http
|
|
157
236
|
|
|
158
237
|
def retrieve(self) -> dict[str, Any]:
|
|
159
|
-
"""GET /v1/usage — credit balance + period meters.
|
|
238
|
+
"""GET /v1/usage — credit balance + period meters.
|
|
239
|
+
|
|
240
|
+
For the per-analysis transaction ledger, use :class:`Credits`
|
|
241
|
+
(``client.credits.retrieve()``).
|
|
242
|
+
"""
|
|
160
243
|
return self._http.get("/v1/usage")
|
|
161
244
|
|
|
162
245
|
|
|
246
|
+
class Credits:
|
|
247
|
+
def __init__(self, http: HttpClient) -> None:
|
|
248
|
+
self._http = http
|
|
249
|
+
|
|
250
|
+
def retrieve(self, *, limit: Optional[int] = None, before: Optional[str] = None) -> dict[str, Any]:
|
|
251
|
+
"""GET /v1/credits — balance + paginated transaction ledger.
|
|
252
|
+
|
|
253
|
+
Agent-facing billing truth: check this before submitting work to avoid
|
|
254
|
+
a 402. Returns::
|
|
255
|
+
|
|
256
|
+
{object: "credits", balance_seconds, grant_seconds, used_seconds,
|
|
257
|
+
transactions: [{id, kind, seconds_delta, balance_after_seconds,
|
|
258
|
+
source_type, source_id, created_at}],
|
|
259
|
+
has_more: bool}
|
|
260
|
+
|
|
261
|
+
Each debit carries the analysis/stream id it came from in
|
|
262
|
+
``source_id`` (``source_type: "analysis"``). ``limit`` is 1–100
|
|
263
|
+
(default 20); ``before`` is a cursor returning transactions older
|
|
264
|
+
than that id.
|
|
265
|
+
"""
|
|
266
|
+
params: dict[str, Any] = {}
|
|
267
|
+
if limit is not None:
|
|
268
|
+
params["limit"] = limit
|
|
269
|
+
if before is not None:
|
|
270
|
+
params["before"] = before
|
|
271
|
+
return self._http.get("/v1/credits", params=params or None)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class CreditPricing:
|
|
275
|
+
def __init__(self, http: HttpClient) -> None:
|
|
276
|
+
self._http = http
|
|
277
|
+
|
|
278
|
+
def retrieve(self) -> dict[str, Any]:
|
|
279
|
+
"""GET /v1/credit-pricing — current pricing config (public, no auth).
|
|
280
|
+
|
|
281
|
+
Returns ``{price_per_second_cents, signup_grant_seconds,
|
|
282
|
+
card_grant_seconds, auto_refill_threshold_seconds,
|
|
283
|
+
allowed_purchase_cents, custom_purchase_enabled, min_purchase_cents}``.
|
|
284
|
+
Cost of an analysis = ``usage.billed_seconds × price_per_second_cents``.
|
|
285
|
+
"""
|
|
286
|
+
return self._http.get("/v1/credit-pricing")
|
|
287
|
+
|
|
288
|
+
|
|
163
289
|
class Errors:
|
|
164
290
|
def __init__(self, http: HttpClient) -> None:
|
|
165
291
|
self._http = http
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "primate-intelligence"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Official Python SDK for the Primate Vision API (Primate Intelligence). Video scene understanding: upload, analyze, stream."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""PRI-480 parity tests — new resources exercised against RECORDED LIVE API
|
|
2
|
+
responses (captured 2026-07-28 from https://api.primateintelligence.ai with a
|
|
3
|
+
sandbox key), never invented shapes. Offline via httpx MockTransport."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from primate_intelligence import Primate
|
|
11
|
+
|
|
12
|
+
# ── Recorded live fixtures (2026-07-28) ──────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
LIVE_CREDITS = {
|
|
15
|
+
"object": "credits",
|
|
16
|
+
"balance_seconds": 6000,
|
|
17
|
+
"grant_seconds": 6000,
|
|
18
|
+
"used_seconds": 0,
|
|
19
|
+
"transactions": [
|
|
20
|
+
{
|
|
21
|
+
"id": "09b1463b-a461-4eba-864b-1a7fd46c6844",
|
|
22
|
+
"kind": "signup_grant",
|
|
23
|
+
"seconds_delta": 6000,
|
|
24
|
+
"balance_after_seconds": 6000,
|
|
25
|
+
"source_type": None,
|
|
26
|
+
"source_id": None,
|
|
27
|
+
"created_at": "2026-07-28T19:59:13.406703+00:00",
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"has_more": False,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
LIVE_PRICING = {
|
|
34
|
+
"price_per_second_cents": 1,
|
|
35
|
+
"signup_grant_seconds": 6000,
|
|
36
|
+
"card_grant_seconds": 6000,
|
|
37
|
+
"auto_refill_threshold_seconds": 600,
|
|
38
|
+
"allowed_purchase_cents": [1000, 2500, 5000, 10000],
|
|
39
|
+
"custom_purchase_enabled": True,
|
|
40
|
+
"min_purchase_cents": 1000,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
LIVE_PREVIEW = {
|
|
44
|
+
"object": "analysis_preview",
|
|
45
|
+
"query": {"query_type": "object", "search_terms": ["forklift"]},
|
|
46
|
+
"parse_mode": "llm",
|
|
47
|
+
"assessable": True,
|
|
48
|
+
"video_duration_s": 22.967,
|
|
49
|
+
"estimated_seconds": 23,
|
|
50
|
+
"estimated_cost_usd": 0.23,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
LIVE_BATCH_PREVIEW = {
|
|
54
|
+
"object": "analysis_batch_preview",
|
|
55
|
+
"video_id": "video_01KYN50SNEJMXBAY56VP0RVYR4",
|
|
56
|
+
"prompts": [
|
|
57
|
+
{"index": 0, "query": {}, "parse_mode": "llm", "assessable": True,
|
|
58
|
+
"estimated_seconds": None, "estimated_cost_usd": None, "discount_pct": 0},
|
|
59
|
+
{"index": 1, "query": {}, "parse_mode": "llm", "assessable": True,
|
|
60
|
+
"estimated_seconds": None, "estimated_cost_usd": None, "discount_pct": 50},
|
|
61
|
+
],
|
|
62
|
+
"pricing": {
|
|
63
|
+
"full_price_prompts": 1,
|
|
64
|
+
"discounted_prompts": 1,
|
|
65
|
+
"discount_pct": 50,
|
|
66
|
+
"estimated_total_seconds": None,
|
|
67
|
+
"estimated_total_cost_usd": None,
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _client(handler) -> Primate:
|
|
73
|
+
return Primate(api_key="pv_test_k", transport=httpx.MockTransport(handler))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_credits_retrieve_hits_v1_credits():
|
|
77
|
+
seen = {}
|
|
78
|
+
|
|
79
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
80
|
+
seen["path"] = request.url.path
|
|
81
|
+
seen["query"] = dict(request.url.params)
|
|
82
|
+
return httpx.Response(200, json=LIVE_CREDITS)
|
|
83
|
+
|
|
84
|
+
out = _client(handler).credits.retrieve()
|
|
85
|
+
assert seen["path"] == "/v1/credits"
|
|
86
|
+
assert seen["query"] == {}
|
|
87
|
+
assert out["object"] == "credits"
|
|
88
|
+
assert out["balance_seconds"] == 6000
|
|
89
|
+
assert out["transactions"][0]["kind"] == "signup_grant"
|
|
90
|
+
assert out["transactions"][0]["source_id"] is None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_credits_retrieve_pagination_params():
|
|
94
|
+
seen = {}
|
|
95
|
+
|
|
96
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
97
|
+
seen["query"] = dict(request.url.params)
|
|
98
|
+
return httpx.Response(200, json=LIVE_CREDITS)
|
|
99
|
+
|
|
100
|
+
_client(handler).credits.retrieve(limit=5, before="txn_x")
|
|
101
|
+
assert seen["query"] == {"limit": "5", "before": "txn_x"}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_credit_pricing_retrieve():
|
|
105
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
106
|
+
assert request.url.path == "/v1/credit-pricing"
|
|
107
|
+
return httpx.Response(200, json=LIVE_PRICING)
|
|
108
|
+
|
|
109
|
+
out = _client(handler).credit_pricing.retrieve()
|
|
110
|
+
assert out["price_per_second_cents"] == 1
|
|
111
|
+
assert out["allowed_purchase_cents"] == [1000, 2500, 5000, 10000]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_validate_only_sends_flag_and_returns_preview():
|
|
115
|
+
seen = {}
|
|
116
|
+
|
|
117
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
118
|
+
seen["body"] = json.loads(request.content)
|
|
119
|
+
return httpx.Response(200, json=LIVE_PREVIEW)
|
|
120
|
+
|
|
121
|
+
out = _client(handler).analyses.validate_only(video_id="video_x", prompt="Is there a forklift?")
|
|
122
|
+
assert seen["body"]["validate_only"] is True
|
|
123
|
+
assert out["object"] == "analysis_preview"
|
|
124
|
+
assert out["assessable"] is True
|
|
125
|
+
assert out["estimated_seconds"] == 23
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_create_batch_posts_and_auto_idempotency_key():
|
|
129
|
+
seen = {}
|
|
130
|
+
|
|
131
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
132
|
+
seen["path"] = request.url.path
|
|
133
|
+
seen["idem"] = request.headers.get("Idempotency-Key")
|
|
134
|
+
seen["body"] = json.loads(request.content)
|
|
135
|
+
return httpx.Response(202, json={"object": "analysis_batch", "id": "ab_x",
|
|
136
|
+
"video_id": "video_x", "analyses": [],
|
|
137
|
+
"pricing": LIVE_BATCH_PREVIEW["pricing"]})
|
|
138
|
+
|
|
139
|
+
out = _client(handler).analyses.create_batch(video_id="video_x", prompts=["a?", "b?"])
|
|
140
|
+
assert seen["path"] == "/v1/analyses/batch"
|
|
141
|
+
assert seen["idem"] and len(seen["idem"]) == 36
|
|
142
|
+
assert "validate_only" not in seen["body"]
|
|
143
|
+
assert out["object"] == "analysis_batch"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_validate_batch_returns_live_preview_shape():
|
|
147
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
148
|
+
body = json.loads(request.content)
|
|
149
|
+
assert body["validate_only"] is True
|
|
150
|
+
return httpx.Response(200, json=LIVE_BATCH_PREVIEW)
|
|
151
|
+
|
|
152
|
+
out = _client(handler).analyses.validate_batch(video_id="video_x", prompts=["a?", "b?"])
|
|
153
|
+
assert out["object"] == "analysis_batch_preview"
|
|
154
|
+
assert out["prompts"][0]["discount_pct"] == 0
|
|
155
|
+
assert out["prompts"][1]["discount_pct"] == 50
|
|
156
|
+
assert out["pricing"]["discount_pct"] == 50
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|