arex-python-sdk 0.2.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.
- arex/__init__.py +137 -0
- arex/_poll.py +126 -0
- arex/_transport.py +303 -0
- arex/_validate.py +156 -0
- arex/_wire.py +126 -0
- arex/async_client.py +133 -0
- arex/client.py +197 -0
- arex/errors.py +334 -0
- arex/models.py +414 -0
- arex/py.typed +0 -0
- arex/research.py +363 -0
- arex_python_sdk-0.2.0.dist-info/METADATA +55 -0
- arex_python_sdk-0.2.0.dist-info/RECORD +15 -0
- arex_python_sdk-0.2.0.dist-info/WHEEL +4 -0
- arex_python_sdk-0.2.0.dist-info/licenses/LICENSE +203 -0
arex/models.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Public result models and SDK-owned defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, get_args
|
|
9
|
+
|
|
10
|
+
from arex import _wire
|
|
11
|
+
from arex._wire import (
|
|
12
|
+
WIRE_LIMITS,
|
|
13
|
+
BrowseTier,
|
|
14
|
+
ResearchFailureCode,
|
|
15
|
+
ResearchMode,
|
|
16
|
+
ResearchModeRequest,
|
|
17
|
+
ResearchStatus,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"API_KEY_PATTERN",
|
|
22
|
+
"API_KEY_PREFIX",
|
|
23
|
+
"BROWSE_AUTO_TIMEOUT_MS",
|
|
24
|
+
"BROWSE_FLEX_TIMEOUT_MS",
|
|
25
|
+
"BROWSE_TIERS",
|
|
26
|
+
"DEEP_POLL_TIMEOUT_MS",
|
|
27
|
+
"DEFAULT_BASE_URL",
|
|
28
|
+
"DEFAULT_BROWSE_MAX_CHARACTERS",
|
|
29
|
+
"DEFAULT_BROWSE_TIER",
|
|
30
|
+
"DEFAULT_LIVECRAWL_TIMEOUT_AUTO_MS",
|
|
31
|
+
"DEFAULT_LIVECRAWL_TIMEOUT_FLEX_MS",
|
|
32
|
+
"DEFAULT_MAX_RETRIES",
|
|
33
|
+
"DEFAULT_POLL_INTERVAL_MS",
|
|
34
|
+
"DEFAULT_RESEARCH_MODE",
|
|
35
|
+
"DEFAULT_RETRY_DELAY_MS",
|
|
36
|
+
"DEFAULT_SEARCH_LIMIT",
|
|
37
|
+
"FAST_POLL_TIMEOUT_MS",
|
|
38
|
+
"LIVECRAWL_TIMEOUT_MARGIN_MS",
|
|
39
|
+
"RESEARCH_MODES",
|
|
40
|
+
"RESEARCH_REQUEST_TIMEOUT_MS",
|
|
41
|
+
"SEARCH_TIMEOUT_MS",
|
|
42
|
+
"USAGE_TIMEOUT_MS",
|
|
43
|
+
"BrowseDocument",
|
|
44
|
+
"BrowseTier",
|
|
45
|
+
"CreditUsage",
|
|
46
|
+
"CreditWeek",
|
|
47
|
+
"CreditWindow",
|
|
48
|
+
"Credits",
|
|
49
|
+
"ResearchFailureCode",
|
|
50
|
+
"ResearchMode",
|
|
51
|
+
"ResearchModeRequest",
|
|
52
|
+
"ResearchResult",
|
|
53
|
+
"ResearchRun",
|
|
54
|
+
"ResearchRunError",
|
|
55
|
+
"ResearchSource",
|
|
56
|
+
"ResearchStatus",
|
|
57
|
+
"SearchResponse",
|
|
58
|
+
"SearchResult",
|
|
59
|
+
"is_arex_api_key",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Authentication
|
|
64
|
+
API_KEY_PREFIX = "sk_arex_"
|
|
65
|
+
API_KEY_PATTERN = re.compile(r"^sk_arex_[A-Za-z0-9_-]{32}$")
|
|
66
|
+
DEFAULT_BASE_URL = "https://api.arex-research.com"
|
|
67
|
+
|
|
68
|
+
DEFAULT_SEARCH_LIMIT = WIRE_LIMITS["SearchRequest"]["limit"]["default"]
|
|
69
|
+
BROWSE_TIERS = get_args(BrowseTier)
|
|
70
|
+
DEFAULT_BROWSE_TIER: BrowseTier = WIRE_LIMITS["BrowseRequest"]["tier"]["default"]
|
|
71
|
+
DEFAULT_LIVECRAWL_TIMEOUT_AUTO_MS = 20000
|
|
72
|
+
DEFAULT_LIVECRAWL_TIMEOUT_FLEX_MS = 90000
|
|
73
|
+
DEFAULT_BROWSE_MAX_CHARACTERS = WIRE_LIMITS["BrowseRequest"]["max_characters"][
|
|
74
|
+
"default"
|
|
75
|
+
]
|
|
76
|
+
RESEARCH_MODES = get_args(ResearchModeRequest)
|
|
77
|
+
DEFAULT_RESEARCH_MODE: ResearchModeRequest = WIRE_LIMITS["ResearchCreateRequest"][
|
|
78
|
+
"mode"
|
|
79
|
+
]["default"]
|
|
80
|
+
|
|
81
|
+
# SDK-owned timeout, polling and retry defaults
|
|
82
|
+
SEARCH_TIMEOUT_MS = 30000
|
|
83
|
+
USAGE_TIMEOUT_MS = 15000
|
|
84
|
+
BROWSE_AUTO_TIMEOUT_MS = 30000
|
|
85
|
+
BROWSE_FLEX_TIMEOUT_MS = 70000
|
|
86
|
+
LIVECRAWL_TIMEOUT_MARGIN_MS = 10000
|
|
87
|
+
RESEARCH_REQUEST_TIMEOUT_MS = 30000
|
|
88
|
+
DEFAULT_POLL_INTERVAL_MS = 2000
|
|
89
|
+
FAST_POLL_TIMEOUT_MS = 150000
|
|
90
|
+
DEEP_POLL_TIMEOUT_MS = 180000
|
|
91
|
+
DEFAULT_MAX_RETRIES = 2
|
|
92
|
+
DEFAULT_RETRY_DELAY_MS = 1000
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_arex_api_key(value: str) -> bool:
|
|
96
|
+
"""Whether `value` has the shape of an AREX API key."""
|
|
97
|
+
return API_KEY_PATTERN.match(value) is not None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _invalid(field: str) -> Exception:
|
|
101
|
+
from arex.errors import ArexNetworkError
|
|
102
|
+
|
|
103
|
+
return ArexNetworkError(
|
|
104
|
+
"invalid_response",
|
|
105
|
+
f"The AREX API response is missing or mistyped the field {field!r}.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _object(data: Any, field: str) -> Mapping[str, Any]:
|
|
110
|
+
if not isinstance(data, Mapping):
|
|
111
|
+
raise _invalid(field)
|
|
112
|
+
return data
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _str(data: Mapping[str, Any], key: str) -> str:
|
|
116
|
+
value = data.get(key)
|
|
117
|
+
if not isinstance(value, str):
|
|
118
|
+
raise _invalid(key)
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _opt_str(data: Mapping[str, Any], key: str) -> str | None:
|
|
123
|
+
value = data.get(key)
|
|
124
|
+
if value is None:
|
|
125
|
+
return None
|
|
126
|
+
if not isinstance(value, str):
|
|
127
|
+
raise _invalid(key)
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _int(data: Mapping[str, Any], key: str) -> int:
|
|
132
|
+
value = data.get(key)
|
|
133
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
134
|
+
raise _invalid(key)
|
|
135
|
+
return value
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _nullable_int(data: Mapping[str, Any], key: str) -> int | None:
|
|
139
|
+
value = data.get(key)
|
|
140
|
+
if value is None:
|
|
141
|
+
return None
|
|
142
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
143
|
+
raise _invalid(key)
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _bool(data: Mapping[str, Any], key: str) -> bool:
|
|
148
|
+
value = data.get(key)
|
|
149
|
+
if not isinstance(value, bool):
|
|
150
|
+
raise _invalid(key)
|
|
151
|
+
return value
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _literal(data: Mapping[str, Any], key: str, allowed: tuple[str, ...]) -> str:
|
|
155
|
+
value = data.get(key)
|
|
156
|
+
if not isinstance(value, str) or value not in allowed:
|
|
157
|
+
raise _invalid(key)
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True, slots=True)
|
|
162
|
+
class Credits:
|
|
163
|
+
"""Credit counters reported alongside a charged response."""
|
|
164
|
+
|
|
165
|
+
used: int
|
|
166
|
+
limit: int | None = None
|
|
167
|
+
remaining: int | None = None
|
|
168
|
+
reset_at: str | None = None
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def from_headers(cls, headers: Mapping[str, str]) -> Credits | None:
|
|
172
|
+
"""Read the `X-Arex-Credits-*` headers, or `None` when they are absent."""
|
|
173
|
+
lowered = {key.lower(): value for key, value in headers.items()}
|
|
174
|
+
used = _header_int(lowered, "x-arex-credits-used")
|
|
175
|
+
if used is None:
|
|
176
|
+
return None
|
|
177
|
+
return cls(
|
|
178
|
+
used=used,
|
|
179
|
+
limit=_header_int(lowered, "x-arex-credits-limit"),
|
|
180
|
+
remaining=_header_int(lowered, "x-arex-credits-remaining"),
|
|
181
|
+
reset_at=lowered.get("x-arex-credits-reset"),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _header_int(headers: Mapping[str, str], name: str) -> int | None:
|
|
186
|
+
raw = headers.get(name)
|
|
187
|
+
if raw is None:
|
|
188
|
+
return None
|
|
189
|
+
try:
|
|
190
|
+
return int(raw.strip())
|
|
191
|
+
except ValueError:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True, slots=True)
|
|
196
|
+
class SearchResult:
|
|
197
|
+
"""One search hit."""
|
|
198
|
+
|
|
199
|
+
title: str
|
|
200
|
+
url: str
|
|
201
|
+
snippet: str | None = None
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def from_wire(cls, data: _wire.SearchResponseResultsItem) -> SearchResult:
|
|
205
|
+
obj = _object(data, "results[]")
|
|
206
|
+
return cls(
|
|
207
|
+
title=_str(obj, "title"),
|
|
208
|
+
url=_str(obj, "url"),
|
|
209
|
+
snippet=_opt_str(obj, "snippet"),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True, slots=True)
|
|
214
|
+
class SearchResponse:
|
|
215
|
+
"""The result of `Arex.search`."""
|
|
216
|
+
|
|
217
|
+
results: tuple[SearchResult, ...]
|
|
218
|
+
credits: Credits
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def from_wire(cls, data: _wire.SearchResponse, credits: Credits) -> SearchResponse:
|
|
222
|
+
obj = _object(data, "body")
|
|
223
|
+
results = obj.get("results")
|
|
224
|
+
if not isinstance(results, list):
|
|
225
|
+
raise _invalid("results")
|
|
226
|
+
return cls(
|
|
227
|
+
results=tuple(SearchResult.from_wire(item) for item in results),
|
|
228
|
+
credits=credits,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass(frozen=True, slots=True)
|
|
233
|
+
class BrowseDocument:
|
|
234
|
+
"""The result of `Arex.browse`."""
|
|
235
|
+
|
|
236
|
+
url: str
|
|
237
|
+
title: str | None
|
|
238
|
+
published_at: str | None
|
|
239
|
+
image_url: str | None
|
|
240
|
+
image_urls: tuple[str, ...] | None
|
|
241
|
+
content: str
|
|
242
|
+
truncated: bool
|
|
243
|
+
credits: Credits
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def from_wire(cls, data: _wire.BrowseResponse, credits: Credits) -> BrowseDocument:
|
|
247
|
+
obj = _object(data, "body")
|
|
248
|
+
raw_urls = obj.get("image_urls")
|
|
249
|
+
if raw_urls is None:
|
|
250
|
+
image_urls = None
|
|
251
|
+
elif (
|
|
252
|
+
isinstance(raw_urls, list)
|
|
253
|
+
and len(raw_urls) <= WIRE_LIMITS["BrowseResponse"]["image_urls"]["maxItems"]
|
|
254
|
+
and all(isinstance(item, str) for item in raw_urls)
|
|
255
|
+
):
|
|
256
|
+
image_urls = tuple(raw_urls)
|
|
257
|
+
else:
|
|
258
|
+
raise _invalid("image_urls")
|
|
259
|
+
return cls(
|
|
260
|
+
url=_str(obj, "url"),
|
|
261
|
+
title=_opt_str(obj, "title"),
|
|
262
|
+
published_at=_opt_str(obj, "published_at"),
|
|
263
|
+
image_url=_opt_str(obj, "image_url"),
|
|
264
|
+
image_urls=image_urls,
|
|
265
|
+
content=_str(obj, "content"),
|
|
266
|
+
truncated=_bool(obj, "truncated"),
|
|
267
|
+
credits=credits,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True, slots=True)
|
|
272
|
+
class CreditWindow:
|
|
273
|
+
"""Credit consumption inside one accounting window."""
|
|
274
|
+
|
|
275
|
+
used: int
|
|
276
|
+
limit: int | None
|
|
277
|
+
remaining: int | None
|
|
278
|
+
reset_at: str
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass(frozen=True, slots=True)
|
|
282
|
+
class CreditWeek(CreditWindow):
|
|
283
|
+
"""The weekly credit window, which also reports when it started."""
|
|
284
|
+
|
|
285
|
+
start: str
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def from_wire(cls, data: _wire.UsageResponseWeek) -> CreditWeek:
|
|
289
|
+
obj = _object(data, "week")
|
|
290
|
+
return cls(
|
|
291
|
+
used=_int(obj, "used"),
|
|
292
|
+
limit=_nullable_int(obj, "limit"),
|
|
293
|
+
remaining=_nullable_int(obj, "remaining"),
|
|
294
|
+
reset_at=_str(obj, "reset_at"),
|
|
295
|
+
start=_str(obj, "start"),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass(frozen=True, slots=True)
|
|
300
|
+
class CreditUsage:
|
|
301
|
+
"""The result of `Arex.usage`: the daily window plus the weekly one."""
|
|
302
|
+
|
|
303
|
+
date: str
|
|
304
|
+
used: int
|
|
305
|
+
limit: int | None
|
|
306
|
+
remaining: int | None
|
|
307
|
+
reset_at: str
|
|
308
|
+
week: CreditWeek | None = None
|
|
309
|
+
|
|
310
|
+
@classmethod
|
|
311
|
+
def from_wire(cls, data: _wire.UsageResponse) -> CreditUsage:
|
|
312
|
+
obj = _object(data, "body")
|
|
313
|
+
week = obj.get("week")
|
|
314
|
+
return cls(
|
|
315
|
+
date=_str(obj, "date"),
|
|
316
|
+
used=_int(obj, "used"),
|
|
317
|
+
limit=_nullable_int(obj, "limit"),
|
|
318
|
+
remaining=_nullable_int(obj, "remaining"),
|
|
319
|
+
reset_at=_str(obj, "reset_at"),
|
|
320
|
+
week=None if week is None else CreditWeek.from_wire(week),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass(frozen=True, slots=True)
|
|
325
|
+
class ResearchSource:
|
|
326
|
+
"""One source cited by a research answer."""
|
|
327
|
+
|
|
328
|
+
title: str
|
|
329
|
+
url: str
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def from_wire(cls, data: _wire.ResearchRunResultSourcesItem) -> ResearchSource:
|
|
333
|
+
obj = _object(data, "result.sources[]")
|
|
334
|
+
return cls(title=_str(obj, "title"), url=_str(obj, "url"))
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@dataclass(frozen=True, slots=True)
|
|
338
|
+
class ResearchResult:
|
|
339
|
+
"""The answer of a completed research run."""
|
|
340
|
+
|
|
341
|
+
answer: str
|
|
342
|
+
sources: tuple[ResearchSource, ...]
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def from_wire(cls, data: _wire.ResearchRunResult) -> ResearchResult:
|
|
346
|
+
obj = _object(data, "result")
|
|
347
|
+
sources = obj.get("sources")
|
|
348
|
+
if not isinstance(sources, list):
|
|
349
|
+
raise _invalid("result.sources")
|
|
350
|
+
return cls(
|
|
351
|
+
answer=_str(obj, "answer"),
|
|
352
|
+
sources=tuple(ResearchSource.from_wire(item) for item in sources),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@dataclass(frozen=True, slots=True)
|
|
357
|
+
class ResearchRunError:
|
|
358
|
+
"""Why a research run failed."""
|
|
359
|
+
|
|
360
|
+
code: ResearchFailureCode
|
|
361
|
+
retryable: bool
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def from_wire(cls, data: _wire.ResearchRunError) -> ResearchRunError:
|
|
365
|
+
obj = _object(data, "error")
|
|
366
|
+
return cls(
|
|
367
|
+
code=_literal(obj, "code", get_args(ResearchFailureCode)), # type: ignore[arg-type]
|
|
368
|
+
retryable=_bool(obj, "retryable"),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@dataclass(frozen=True, slots=True)
|
|
373
|
+
class ResearchRun:
|
|
374
|
+
"""A research run at one point in its lifecycle."""
|
|
375
|
+
|
|
376
|
+
id: str
|
|
377
|
+
status: ResearchStatus
|
|
378
|
+
mode: ResearchMode
|
|
379
|
+
requested_mode: ResearchModeRequest
|
|
380
|
+
created_at: str
|
|
381
|
+
started_at: str | None = None
|
|
382
|
+
completed_at: str | None = None
|
|
383
|
+
result: ResearchResult | None = None
|
|
384
|
+
error: ResearchRunError | None = None
|
|
385
|
+
credits: Credits | None = None
|
|
386
|
+
|
|
387
|
+
@classmethod
|
|
388
|
+
def from_wire(
|
|
389
|
+
cls, data: _wire.ResearchRun, credits: Credits | None = None
|
|
390
|
+
) -> ResearchRun:
|
|
391
|
+
obj = _object(data, "body")
|
|
392
|
+
status = _literal(obj, "status", get_args(ResearchStatus))
|
|
393
|
+
mode = _literal(obj, "mode", get_args(ResearchMode))
|
|
394
|
+
# Payloads are meaningful only in their matching terminal state.
|
|
395
|
+
result = obj.get("result") if status == "completed" else None
|
|
396
|
+
error = obj.get("error") if status == "failed" else None
|
|
397
|
+
if status == "completed" and result is None:
|
|
398
|
+
raise _invalid("result")
|
|
399
|
+
if status == "failed" and error is None:
|
|
400
|
+
raise _invalid("error")
|
|
401
|
+
return cls(
|
|
402
|
+
id=_str(obj, "id"),
|
|
403
|
+
status=status, # type: ignore[arg-type]
|
|
404
|
+
mode=mode, # type: ignore[arg-type]
|
|
405
|
+
requested_mode=_literal(
|
|
406
|
+
obj, "requested_mode", get_args(ResearchModeRequest)
|
|
407
|
+
), # type: ignore[arg-type]
|
|
408
|
+
created_at=_str(obj, "created_at"),
|
|
409
|
+
started_at=_opt_str(obj, "started_at"),
|
|
410
|
+
completed_at=_opt_str(obj, "completed_at"),
|
|
411
|
+
result=None if result is None else ResearchResult.from_wire(result),
|
|
412
|
+
error=None if error is None else ResearchRunError.from_wire(error),
|
|
413
|
+
credits=credits,
|
|
414
|
+
)
|
arex/py.typed
ADDED
|
File without changes
|