arex-python-sdk 0.2.1__py3-none-any.whl → 0.3.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/_transport.py CHANGED
@@ -38,14 +38,11 @@ def _prepare(
38
38
  base_url: str,
39
39
  api_key: str,
40
40
  path: str,
41
- idempotency_key: str | None,
42
41
  ) -> tuple[str, dict[str, str]]:
43
42
  headers = {
44
43
  "Authorization": f"Bearer {api_key}",
45
44
  "Accept": "application/json",
46
45
  }
47
- if idempotency_key is not None:
48
- headers["Idempotency-Key"] = idempotency_key
49
46
  return base_url.rstrip("/") + path, headers
50
47
 
51
48
 
@@ -113,7 +110,7 @@ class _Base:
113
110
  return None
114
111
  if isinstance(error, RateLimitError):
115
112
  # `outstanding_run_limit` is a state, not a burst; retrying never helps.
116
- if error.code not in {"rate_limit_exceeded", "idempotency_key_in_progress"}:
113
+ if error.code != "rate_limit_exceeded":
117
114
  return None
118
115
  elif not self.retry_unavailable or error.code != "temporarily_unavailable":
119
116
  return None
@@ -178,9 +175,8 @@ class Transport(_Base):
178
175
  *,
179
176
  json: Any | None = None,
180
177
  timeout_ms: int,
181
- idempotency_key: str | None = None,
182
178
  ) -> ApiResult:
183
- url, headers = _prepare(self.base_url, self.api_key, path, idempotency_key)
179
+ url, headers = _prepare(self.base_url, self.api_key, path)
184
180
  deadline = time.monotonic() + timeout_ms / 1000
185
181
  attempt = 0
186
182
  while True:
@@ -245,9 +241,8 @@ class AsyncTransport(_Base):
245
241
  *,
246
242
  json: Any | None = None,
247
243
  timeout_ms: int,
248
- idempotency_key: str | None = None,
249
244
  ) -> ApiResult:
250
- url, headers = _prepare(self.base_url, self.api_key, path, idempotency_key)
245
+ url, headers = _prepare(self.base_url, self.api_key, path)
251
246
  deadline = time.monotonic() + timeout_ms / 1000
252
247
  work = asyncio.ensure_future(
253
248
  self._send(method, url, headers, json, timeout_ms, deadline)
arex/_validate.py CHANGED
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import re
6
6
  from typing import Any, get_args
7
7
  from urllib.parse import urlsplit
8
- from uuid import uuid4
9
8
 
10
9
  from arex._wire import WIRE_LIMITS, BrowseTier, ResearchModeRequest
11
10
  from arex.errors import InvalidRequestError
@@ -13,7 +12,6 @@ from arex.errors import InvalidRequestError
13
12
  __all__ = [
14
13
  "MAX_TIMEOUT_MS",
15
14
  "is_timeout_ms",
16
- "resolve_idempotency_key",
17
15
  "validate_browse",
18
16
  "validate_research_create",
19
17
  "validate_research_wait",
@@ -140,17 +138,3 @@ def validate_timeout_ms(value: object) -> None:
140
138
  f"timeout_ms must be a positive integer of at most {MAX_TIMEOUT_MS}.",
141
139
  param="timeout_ms",
142
140
  )
143
-
144
-
145
- def resolve_idempotency_key(key: str | None) -> str:
146
- """The caller's key once validated, else a fresh UUID4 for this one call."""
147
- if key is None:
148
- return str(uuid4())
149
- pattern = WIRE_LIMITS["IdempotencyKey"]["pattern"]
150
- # `fullmatch` so the pattern's `$` cannot accept a trailing newline.
151
- if not isinstance(key, str) or not re.fullmatch(pattern, key):
152
- raise InvalidRequestError(
153
- "idempotency_key must be 1 to 255 characters from A-Z a-z 0-9 . _ -",
154
- param="idempotency_key",
155
- )
156
- return key
arex/_wire.py CHANGED
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict
6
6
  if TYPE_CHECKING:
7
7
  from typing_extensions import Required
8
8
 
9
- ErrorCode = Literal['invalid_request', 'content_blocked', 'unauthorized', 'registration_required', 'account_deletion_pending', 'feature_not_enabled', 'not_found', 'method_not_allowed', 'run_not_cancellable', 'idempotency_key_in_progress', 'source_not_available', 'idempotency_key_reused', 'rate_limit_exceeded', 'outstanding_run_limit', 'daily_credits_exhausted', 'weekly_credits_exhausted', 'internal_error', 'temporarily_unavailable']
9
+ ErrorCode = Literal['browser_session_limit', 'browser_session_busy', 'browser_session_expired', 'browser_stale_observation', 'browser_action_id_reused', 'browser_action_limit', 'browser_url_blocked', 'browser_result_unknown', 'invalid_request', 'content_blocked', 'unauthorized', 'registration_required', 'account_deletion_pending', 'feature_not_enabled', 'not_found', 'method_not_allowed', 'run_not_cancellable', 'source_not_available', 'rate_limit_exceeded', 'outstanding_run_limit', 'daily_credits_exhausted', 'weekly_credits_exhausted', 'internal_error', 'temporarily_unavailable']
10
10
  ResearchFailureCode = Literal['timeout', 'execution_failed', 'worker_unavailable', 'interrupted']
11
11
  BrowseTier = Literal['auto', 'flex']
12
12
  ResearchMode = Literal['fast', 'deep']
@@ -19,6 +19,13 @@ class BrowseRequest(TypedDict, total=False):
19
19
  livecrawl: bool
20
20
  livecrawl_timeout_ms: int
21
21
  max_characters: int
22
+ highlights: bool
23
+
24
+ class BrowseResponseHighlights(TypedDict, total=False):
25
+ applied: Required[bool]
26
+ cached: Required[bool]
27
+ kept_chunks: Required[int]
28
+ total_chunks: Required[int]
22
29
 
23
30
  class BrowseResponse(TypedDict, total=False):
24
31
  url: Required[str]
@@ -28,6 +35,7 @@ class BrowseResponse(TypedDict, total=False):
28
35
  image_urls: list[str]
29
36
  content: Required[str]
30
37
  truncated: Required[bool]
38
+ highlights: BrowseResponseHighlights
31
39
 
32
40
  class ErrorError(TypedDict, total=False):
33
41
  code: Required[ErrorCode]
@@ -99,7 +107,8 @@ class UsageResponse(TypedDict, total=False):
99
107
  week: UsageResponseWeek
100
108
 
101
109
 
102
- WIRE_LIMITS: dict[str, Any] = {'BrowseRequest': {'livecrawl': {'default': False},
110
+ WIRE_LIMITS: dict[str, Any] = {'BrowseRequest': {'highlights': {'default': False},
111
+ 'livecrawl': {'default': False},
103
112
  'livecrawl_timeout_ms': {'maximum': 90000, 'minimum': 1000},
104
113
  'max_characters': {'default': 12000,
105
114
  'maximum': 131072,
@@ -108,7 +117,6 @@ WIRE_LIMITS: dict[str, Any] = {'BrowseRequest': {'livecrawl': {'default': False}
108
117
  'url': {'maxLength': 8192,
109
118
  'pattern': '^[Hh][Tt][Tt][Pp][Ss]?://[^/@?#]+(?:[/?#]|$)'}},
110
119
  'BrowseResponse': {'image_urls': {'maxItems': 50}},
111
- 'IdempotencyKey': {'pattern': '^[A-Za-z0-9._-]{1,255}$', 'type': 'string'},
112
120
  'ResearchCreateRequest': {'mode': {'default': 'auto',
113
121
  'enum': ['fast', 'deep', 'auto']},
114
122
  'query': {'maxLength': 2000, 'minLength': 1}},
arex/async_client.py CHANGED
@@ -8,7 +8,7 @@ from typing import Any
8
8
  import httpx
9
9
 
10
10
  from arex._transport import AsyncTransport
11
- from arex._validate import resolve_idempotency_key, validate_browse, validate_search
11
+ from arex._validate import validate_browse, validate_search
12
12
  from arex.client import (
13
13
  browse_body,
14
14
  browse_timeout_ms,
@@ -58,12 +58,10 @@ class AsyncArex:
58
58
  query: str,
59
59
  *,
60
60
  limit: int | None = None,
61
- idempotency_key: str | None = None,
62
61
  timeout_ms: int | None = None,
63
62
  ) -> SearchResponse:
64
63
  """Run a web search."""
65
64
  validate_search(query, limit)
66
- key = resolve_idempotency_key(idempotency_key)
67
65
  body: dict[str, Any] = {"query": query}
68
66
  if limit is not None:
69
67
  body["limit"] = limit
@@ -71,7 +69,6 @@ class AsyncArex:
71
69
  "POST",
72
70
  "/v1/search",
73
71
  json=body,
74
- idempotency_key=key,
75
72
  timeout_ms=self._transport.resolve_timeout(SEARCH_TIMEOUT_MS, timeout_ms),
76
73
  )
77
74
  return SearchResponse.from_wire(result.data, require_credits(result.credits))
@@ -84,7 +81,6 @@ class AsyncArex:
84
81
  livecrawl: bool | None = None,
85
82
  livecrawl_timeout_ms: int | None = None,
86
83
  max_characters: int | None = None,
87
- idempotency_key: str | None = None,
88
84
  timeout_ms: int | None = None,
89
85
  ) -> BrowseDocument:
90
86
  """Fetch and extract one page."""
@@ -95,12 +91,10 @@ class AsyncArex:
95
91
  livecrawl_timeout_ms=livecrawl_timeout_ms,
96
92
  max_characters=max_characters,
97
93
  )
98
- key = resolve_idempotency_key(idempotency_key)
99
94
  default_ms = browse_timeout_ms(tier, livecrawl, livecrawl_timeout_ms)
100
95
  result = await self._transport.request(
101
96
  "POST",
102
97
  "/v1/browse",
103
- idempotency_key=key,
104
98
  json=browse_body(
105
99
  url, tier, livecrawl, livecrawl_timeout_ms, max_characters
106
100
  ),
arex/client.py CHANGED
@@ -9,7 +9,7 @@ from typing import Any
9
9
  import httpx
10
10
 
11
11
  from arex._transport import Transport
12
- from arex._validate import resolve_idempotency_key, validate_browse, validate_search
12
+ from arex._validate import validate_browse, validate_search
13
13
  from arex.errors import ArexNetworkError
14
14
  from arex.models import (
15
15
  BROWSE_AUTO_TIMEOUT_MS,
@@ -122,12 +122,10 @@ class Arex:
122
122
  query: str,
123
123
  *,
124
124
  limit: int | None = None,
125
- idempotency_key: str | None = None,
126
125
  timeout_ms: int | None = None,
127
126
  ) -> SearchResponse:
128
127
  """Run a web search."""
129
128
  validate_search(query, limit)
130
- key = resolve_idempotency_key(idempotency_key)
131
129
  body: dict[str, Any] = {"query": query}
132
130
  if limit is not None:
133
131
  body["limit"] = limit
@@ -135,7 +133,6 @@ class Arex:
135
133
  "POST",
136
134
  "/v1/search",
137
135
  json=body,
138
- idempotency_key=key,
139
136
  timeout_ms=self._transport.resolve_timeout(SEARCH_TIMEOUT_MS, timeout_ms),
140
137
  )
141
138
  return SearchResponse.from_wire(result.data, require_credits(result.credits))
@@ -148,7 +145,6 @@ class Arex:
148
145
  livecrawl: bool | None = None,
149
146
  livecrawl_timeout_ms: int | None = None,
150
147
  max_characters: int | None = None,
151
- idempotency_key: str | None = None,
152
148
  timeout_ms: int | None = None,
153
149
  ) -> BrowseDocument:
154
150
  """Fetch and extract one page."""
@@ -159,12 +155,10 @@ class Arex:
159
155
  livecrawl_timeout_ms=livecrawl_timeout_ms,
160
156
  max_characters=max_characters,
161
157
  )
162
- key = resolve_idempotency_key(idempotency_key)
163
158
  default_ms = browse_timeout_ms(tier, livecrawl, livecrawl_timeout_ms)
164
159
  result = self._transport.request(
165
160
  "POST",
166
161
  "/v1/browse",
167
- idempotency_key=key,
168
162
  json=browse_body(
169
163
  url, tier, livecrawl, livecrawl_timeout_ms, max_characters
170
164
  ),
arex/errors.py CHANGED
@@ -78,7 +78,7 @@ class PermissionDeniedError(ArexApiError):
78
78
 
79
79
 
80
80
  class InvalidRequestError(ArexApiError):
81
- """Invalid request, blocked content, reused key, or client-side validation."""
81
+ """Invalid request, blocked content, or client-side validation."""
82
82
 
83
83
  def __init__(
84
84
  self,
@@ -113,7 +113,7 @@ class NotFoundError(ArexApiError):
113
113
 
114
114
 
115
115
  class RateLimitError(ArexApiError):
116
- """Rate limits, outstanding runs, or an idempotent request still in progress."""
116
+ """Rate limits or outstanding runs."""
117
117
 
118
118
 
119
119
  class CreditsExhaustedError(ArexApiError):
@@ -210,11 +210,9 @@ class ResearchRunCancelledError(ArexError):
210
210
  _ERROR_CLASSES: dict[str, type[ArexApiError]] = {
211
211
  "invalid_request": InvalidRequestError,
212
212
  "content_blocked": InvalidRequestError,
213
- "idempotency_key_reused": InvalidRequestError,
214
213
  "source_not_available": SourceNotAvailableError,
215
214
  "not_found": NotFoundError,
216
215
  "rate_limit_exceeded": RateLimitError,
217
- "idempotency_key_in_progress": RateLimitError,
218
216
  "outstanding_run_limit": RateLimitError,
219
217
  "temporarily_unavailable": ServiceUnavailableError,
220
218
  "internal_error": ServiceUnavailableError,
arex/research.py CHANGED
@@ -9,7 +9,6 @@ from dataclasses import replace
9
9
  from arex._poll import async_poll_until, poll_until
10
10
  from arex._transport import AsyncTransport, Transport
11
11
  from arex._validate import (
12
- resolve_idempotency_key,
13
12
  validate_research_create,
14
13
  validate_research_wait,
15
14
  validate_run_id,
@@ -72,17 +71,14 @@ class Research:
72
71
  query: str,
73
72
  *,
74
73
  mode: ResearchModeRequest | None = None,
75
- idempotency_key: str | None = None,
76
74
  timeout_ms: int | None = None,
77
75
  ) -> ResearchRun:
78
76
  """Start a research run. The returned run carries the credit counters."""
79
77
  validate_research_create(query, mode)
80
- key = resolve_idempotency_key(idempotency_key)
81
78
  result = self._transport.request(
82
79
  "POST",
83
80
  "/v1/research",
84
81
  json=_create_body(query, mode),
85
- idempotency_key=key,
86
82
  timeout_ms=self._timeout(timeout_ms),
87
83
  )
88
84
  return ResearchRun.from_wire(result.data, result.credits)
@@ -138,13 +134,12 @@ class Research:
138
134
  *,
139
135
  mode: ResearchModeRequest | None = None,
140
136
  poll_interval_ms: int = DEFAULT_POLL_INTERVAL_MS,
141
- idempotency_key: str | None = None,
142
137
  timeout_ms: int | None = None,
143
138
  ) -> ResearchRun:
144
139
  """Create a run and poll until it completes. Errors raised after the
145
140
  create name the run in `run_id`."""
146
141
  validate_research_wait(poll_interval_ms, timeout_ms)
147
- created = self.create(query, mode=mode, idempotency_key=idempotency_key)
142
+ created = self.create(query, mode=mode)
148
143
  return self._await_completion(
149
144
  created.id,
150
145
  created,
@@ -223,17 +218,14 @@ class AsyncResearch:
223
218
  query: str,
224
219
  *,
225
220
  mode: ResearchModeRequest | None = None,
226
- idempotency_key: str | None = None,
227
221
  timeout_ms: int | None = None,
228
222
  ) -> ResearchRun:
229
223
  """Start a research run. The returned run carries the credit counters."""
230
224
  validate_research_create(query, mode)
231
- key = resolve_idempotency_key(idempotency_key)
232
225
  result = await self._transport.request(
233
226
  "POST",
234
227
  "/v1/research",
235
228
  json=_create_body(query, mode),
236
- idempotency_key=key,
237
229
  timeout_ms=self._timeout(timeout_ms),
238
230
  )
239
231
  return ResearchRun.from_wire(result.data, result.credits)
@@ -292,13 +284,12 @@ class AsyncResearch:
292
284
  *,
293
285
  mode: ResearchModeRequest | None = None,
294
286
  poll_interval_ms: int = DEFAULT_POLL_INTERVAL_MS,
295
- idempotency_key: str | None = None,
296
287
  timeout_ms: int | None = None,
297
288
  ) -> ResearchRun:
298
289
  """Create a run and poll until it completes. Errors raised after the
299
290
  create name the run in `run_id`."""
300
291
  validate_research_wait(poll_interval_ms, timeout_ms)
301
- created = await self.create(query, mode=mode, idempotency_key=idempotency_key)
292
+ created = await self.create(query, mode=mode)
302
293
  return await self._await_completion(
303
294
  created.id,
304
295
  created,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arex-python-sdk
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for the AREX public APIs.
5
5
  Project-URL: Homepage, https://github.com/AREX-Research/arex-sdk
6
6
  Project-URL: Repository, https://github.com/AREX-Research/arex-sdk
@@ -26,10 +26,14 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  Python client for the AREX public API, synchronous and asynchronous. Python >= 3.10; the only dependency is `httpx`.
28
28
 
29
+ Install from PyPI:
30
+
29
31
  ```bash
30
32
  pip install arex-python-sdk
31
33
  ```
32
34
 
35
+ Or download the `.whl` from [GitHub Releases](https://github.com/AREX-Research/arex-sdk/releases/latest) and run `pip install ./arex_python_sdk-*.whl`.
36
+
33
37
  ```python
34
38
  from arex import Arex
35
39
 
@@ -44,12 +48,12 @@ with Arex() as arex: # reads AREX_API_KEY
44
48
 
45
49
  `AsyncArex` mirrors every call as a coroutine under `async with`.
46
50
 
47
- The SDK follows the checked-in v1 OpenAPI contract. Charged calls reuse an idempotency key across automatic retries. Supply `idempotency_key` when you need to retry the same operation across calls or process restarts. Keep API keys server-side.
51
+ The SDK follows the checked-in v1 OpenAPI contract. Keep API keys server-side.
48
52
 
49
- Search, browse and research creation consume credits; consult your AREX account for current pricing. Usage checks, polling and other research lifecycle calls are free.
53
+ Search, browse and research creation consume credits; consult your AREX account for current pricing. Usage checks, polling and other research lifecycle calls are free. Retries are new charged requests; failures are refunded and cancellations are charged.
50
54
 
51
55
  - [API reference](https://github.com/AREX-Research/arex-sdk/blob/main/python/docs/api.md): options, models, errors, exports.
52
- - [Behavior](https://github.com/AREX-Research/arex-sdk/blob/main/python/docs/behavior.md): credits, deadlines, retries, idempotency, polling, and recovery.
56
+ - [Behavior](https://github.com/AREX-Research/arex-sdk/blob/main/python/docs/behavior.md): credits, deadlines, retries, polling, and recovery.
53
57
  - [Examples](https://github.com/AREX-Research/arex-sdk/tree/main/python/examples): runnable scripts.
54
58
 
55
59
  [Apache License 2.0](https://github.com/AREX-Research/arex-sdk/blob/main/LICENSE).
@@ -0,0 +1,15 @@
1
+ arex/__init__.py,sha256=a2kMXiAuVzTOyHfQDriVZnf7gLtZiFYFcLaGxDKgk7I,3427
2
+ arex/_poll.py,sha256=YvtqfwaNgP2caaQ9XwxA7zA--qGvZBcM33ZwhYh0zh8,3927
3
+ arex/_transport.py,sha256=0esQx-Mn-zZVzmkV5ZHa2Aj4u2fPuQCGGAXq2_vqM-Q,9823
4
+ arex/_validate.py,sha256=CBZhIIae37mxYSDE0K_wIwc3l8_-3-wxzXDvQ6UZJRc,5164
5
+ arex/_wire.py,sha256=QZsa9jE-fPRHUxLJpjGtvqNKBRaTBt1zhuLyRNgP4b4,4996
6
+ arex/async_client.py,sha256=Q9YLKpIsoMV58G5l6Iz13to8tJPNq5lzzjhDXe-iQEE,3794
7
+ arex/client.py,sha256=vtuIaMDNV3eYWlBMn9bonHuQ3PRLAFNT7cZpVr4kiU4,5722
8
+ arex/errors.py,sha256=x54kfbrYnhoJYNWaURUa2coOuYWBLhrBDRC-6wVtW-0,9976
9
+ arex/models.py,sha256=9q5vL9_KlR0IHEr98Xg23GwrPCipsbnnIQlbBsiFhy0,11998
10
+ arex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ arex/research.py,sha256=p5WmS1IfdWvXs6C8cwPVUB4StXvr5CtZVRXJC8APCms,12164
12
+ arex_python_sdk-0.3.0.dist-info/METADATA,sha256=hFHteeh8iS5F3BST5pBaWHy7_38KViaYlZ3xvot3v2E,2588
13
+ arex_python_sdk-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
14
+ arex_python_sdk-0.3.0.dist-info/licenses/LICENSE,sha256=2cJfL27uSPe4DpI1sd6sctsmMLW9_PV-KKhUYkHKQyo,11354
15
+ arex_python_sdk-0.3.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- arex/__init__.py,sha256=a2kMXiAuVzTOyHfQDriVZnf7gLtZiFYFcLaGxDKgk7I,3427
2
- arex/_poll.py,sha256=YvtqfwaNgP2caaQ9XwxA7zA--qGvZBcM33ZwhYh0zh8,3927
3
- arex/_transport.py,sha256=-UgC6UT0O30p44IV78ChXB-kbGmNaQT2tSuc3ETidLk,10104
4
- arex/_validate.py,sha256=jm9zX4SI7PCqTLMvJWOzbWyoW2KuYuqoOhGO8FZs970,5776
5
- arex/_wire.py,sha256=wn-L9oYaS1ftzm06fpZAT2ZYrv6EOgo762-ckCWB9-o,4634
6
- arex/async_client.py,sha256=Sn-P8wpyvV-cDqOixEeru8pOxjROCjraTmthKPf9PaQ,4083
7
- arex/client.py,sha256=ehYbPTxvMLtxkX3BpIRyTZZeb2bd--dIEoPnnN0_jM8,6011
8
- arex/errors.py,sha256=bxYqfZeFzKb8Qq5_W8tCYkyVrTdVy8Z8uznXiy0SwCI,10132
9
- arex/models.py,sha256=9q5vL9_KlR0IHEr98Xg23GwrPCipsbnnIQlbBsiFhy0,11998
10
- arex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- arex/research.py,sha256=jBa2nmfnBS8TICSXHqYLaAoDZ3oVx5moEJ2J3IvBxRI,12611
12
- arex_python_sdk-0.2.1.dist-info/METADATA,sha256=O8hCp_zwVz9GlO74GiSobp_Q2cOHm-uEPvlL0H5qXio,2507
13
- arex_python_sdk-0.2.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
14
- arex_python_sdk-0.2.1.dist-info/licenses/LICENSE,sha256=2cJfL27uSPe4DpI1sd6sctsmMLW9_PV-KKhUYkHKQyo,11354
15
- arex_python_sdk-0.2.1.dist-info/RECORD,,