opencode-python-sdk 0.0.1.dev1__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.
Files changed (62) hide show
  1. opencode/.generated.json +39 -0
  2. opencode/__init__.py +221 -0
  3. opencode/_base_client.py +539 -0
  4. opencode/_client.py +255 -0
  5. opencode/_constants.py +27 -0
  6. opencode/_errors.py +547 -0
  7. opencode/_events.py +732 -0
  8. opencode/_exceptions.py +271 -0
  9. opencode/_models.py +54 -0
  10. opencode/_pagination.py +192 -0
  11. opencode/_pty.py +586 -0
  12. opencode/_qs.py +95 -0
  13. opencode/_resource.py +42 -0
  14. opencode/_rpc.py +489 -0
  15. opencode/_server.py +335 -0
  16. opencode/_streaming.py +248 -0
  17. opencode/_types.py +77 -0
  18. opencode/_unions.py +46 -0
  19. opencode/_version.py +8 -0
  20. opencode/lib/__init__.py +5 -0
  21. opencode/lib/_ready.py +91 -0
  22. opencode/lib/_session.py +231 -0
  23. opencode/py.typed +0 -0
  24. opencode/resources/__init__.py +364 -0
  25. opencode/resources/agent.py +179 -0
  26. opencode/resources/command.py +100 -0
  27. opencode/resources/config.py +240 -0
  28. opencode/resources/credential.py +384 -0
  29. opencode/resources/debug.py +178 -0
  30. opencode/resources/event.py +88 -0
  31. opencode/resources/experimental.py +765 -0
  32. opencode/resources/file.py +355 -0
  33. opencode/resources/form.py +100 -0
  34. opencode/resources/generate.py +110 -0
  35. opencode/resources/integration.py +1033 -0
  36. opencode/resources/location.py +162 -0
  37. opencode/resources/mcp.py +492 -0
  38. opencode/resources/message.py +152 -0
  39. opencode/resources/migration.py +118 -0
  40. opencode/resources/model.py +177 -0
  41. opencode/resources/permission.py +704 -0
  42. opencode/resources/plugin.py +250 -0
  43. opencode/resources/project.py +187 -0
  44. opencode/resources/provider.py +183 -0
  45. opencode/resources/pty.py +522 -0
  46. opencode/resources/reference.py +100 -0
  47. opencode/resources/rpc.py +113 -0
  48. opencode/resources/server.py +230 -0
  49. opencode/resources/session.py +3567 -0
  50. opencode/resources/shell.py +412 -0
  51. opencode/resources/skill.py +100 -0
  52. opencode/resources/vcs.py +500 -0
  53. opencode/resources/voice.py +113 -0
  54. opencode/resources/websearch.py +184 -0
  55. opencode/resources/worktree.py +336 -0
  56. opencode/testing.py +508 -0
  57. opencode/types/__init__.py +1462 -0
  58. opencode/types/_generated.py +8540 -0
  59. opencode_python_sdk-0.0.1.dev1.dist-info/METADATA +468 -0
  60. opencode_python_sdk-0.0.1.dev1.dist-info/RECORD +62 -0
  61. opencode_python_sdk-0.0.1.dev1.dist-info/WHEEL +4 -0
  62. opencode_python_sdk-0.0.1.dev1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,539 @@
1
+ """HTTP transport shared by the sync and async clients.
2
+
3
+ This module is the only place that talks to httpx. It builds requests, applies authentication, retries
4
+ transient failures, and turns responses into typed values or errors.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import builtins
11
+ import email.utils
12
+ import functools
13
+ import json
14
+ import logging
15
+ import os
16
+ import platform
17
+ import random
18
+ import time
19
+ from dataclasses import dataclass, field
20
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
21
+
22
+ import anyio
23
+ import httpx
24
+ import pydantic
25
+ from typing_extensions import Self, override
26
+
27
+ from . import _constants as constants
28
+ from ._errors import TAGGED_ERRORS
29
+ from ._exceptions import (
30
+ APIConnectionError,
31
+ APIResponseValidationError,
32
+ APIStatusError,
33
+ APITimeoutError,
34
+ OpencodeError,
35
+ error_message,
36
+ status_error_class,
37
+ validation_summary,
38
+ )
39
+ from ._qs import encode_query, to_jsonable
40
+ from ._streaming import AsyncStream, Stream
41
+ from ._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
42
+ from ._version import __version__
43
+
44
+ if TYPE_CHECKING:
45
+ from collections.abc import Mapping
46
+ from types import TracebackType
47
+
48
+ from ._pagination import AsyncPaginator, SyncCursorPage
49
+
50
+ __all__ = ["AsyncAPIClient", "SyncAPIClient"]
51
+
52
+ log = logging.getLogger("opencode")
53
+
54
+ _T = TypeVar("_T")
55
+
56
+ ResponseKind = Literal["json", "void", "binary"]
57
+
58
+
59
+ @functools.cache
60
+ def _cached_adapter(tp: Any) -> pydantic.TypeAdapter[Any]:
61
+ return pydantic.TypeAdapter(tp)
62
+
63
+
64
+ @functools.cache
65
+ def _resolve_type(expr: str) -> Any:
66
+ # Generated resources name their response types as strings, so `import opencode` does not build every model.
67
+ from . import types
68
+
69
+ return eval(expr, {**vars(types), "builtins": builtins}) # expressions come from the generator
70
+
71
+
72
+ def type_adapter(tp: Any) -> pydantic.TypeAdapter[Any]:
73
+ """Returns a cached ``TypeAdapter`` for a response type, or for a type expression over ``opencode.types``."""
74
+ if isinstance(tp, str):
75
+ tp = _resolve_type(tp)
76
+ try:
77
+ return _cached_adapter(tp)
78
+ except TypeError: # unhashable type expression
79
+ return pydantic.TypeAdapter(tp)
80
+
81
+
82
+ @dataclass
83
+ class RequestOptions:
84
+ """Everything needed to send one request. Generated methods build these."""
85
+
86
+ method: str
87
+ path: str
88
+ query: Mapping[str, object] = field(default_factory=dict[str, object])
89
+ headers: Mapping[str, object] = field(default_factory=dict[str, object])
90
+ body: object | NotGiven = NOT_GIVEN
91
+ content: bytes | None = None
92
+ extra_headers: Headers | None = None
93
+ extra_query: Query | None = None
94
+ extra_body: Body | None = None
95
+ timeout: Timeout | NotGiven = NOT_GIVEN
96
+ stream: bool = False
97
+
98
+ @property
99
+ def idempotent(self) -> bool:
100
+ return self.method.upper() in constants.IDEMPOTENT_METHODS
101
+
102
+
103
+ def _platform_headers() -> dict[str, str]:
104
+ return {
105
+ "User-Agent": f"opencode-python/{__version__} python/{platform.python_version()} httpx/{httpx.__version__}",
106
+ }
107
+
108
+
109
+ def _resolve_password(password: str | None) -> str | None:
110
+ if password is not None:
111
+ return password or None
112
+ for name in constants.PASSWORD_ENVS:
113
+ value = os.environ.get(name)
114
+ if value:
115
+ return value
116
+ return None
117
+
118
+
119
+ def _resolve_base_url(base_url: str | httpx.URL | None) -> httpx.URL:
120
+ if base_url is None:
121
+ base_url = os.environ.get(constants.BASE_URL_ENV)
122
+ if not base_url:
123
+ raise OpencodeError(
124
+ "No OpenCode server URL. Pass base_url=..., set the OPENCODE_BASE_URL environment variable, "
125
+ "or start a server with opencode.OpencodeServer()."
126
+ )
127
+ url = httpx.URL(str(base_url))
128
+ if url.scheme not in ("http", "https"):
129
+ raise OpencodeError(f"OpenCode base_url must be an http(s) URL, got {str(base_url)!r}")
130
+ # A path prefix on the base URL is kept: requests are joined relative to it.
131
+ return url if url.raw_path.endswith(b"/") else url.copy_with(raw_path=url.raw_path + b"/")
132
+
133
+
134
+ class _BaseClient:
135
+ """Configuration and request building shared by both clients."""
136
+
137
+ base_url: httpx.URL
138
+ username: str
139
+ password: str | None
140
+ timeout: Timeout
141
+ max_retries: int
142
+ directory: str | None
143
+
144
+ def __init__(
145
+ self,
146
+ *,
147
+ base_url: str | httpx.URL | None,
148
+ password: str | None,
149
+ username: str,
150
+ directory: str | os.PathLike[str] | None,
151
+ timeout: Timeout,
152
+ max_retries: int,
153
+ default_headers: Mapping[str, str] | None,
154
+ default_query: Mapping[str, object] | None,
155
+ ) -> None:
156
+ if max_retries < 0:
157
+ raise ValueError("max_retries must be zero or more")
158
+ self.base_url = _resolve_base_url(base_url)
159
+ self.username = username
160
+ self.password = _resolve_password(password)
161
+ self._authorization = (
162
+ None
163
+ if self.password is None
164
+ else "Basic " + base64.b64encode(f"{username}:{self.password}".encode()).decode("ascii")
165
+ )
166
+ self.directory = os.fspath(directory) if directory is not None else None
167
+ self.timeout = timeout
168
+ self.max_retries = max_retries
169
+ self._default_headers = dict(default_headers or {})
170
+ self._default_query = dict(default_query or {})
171
+
172
+ @override
173
+ def __repr__(self) -> str:
174
+ # Never include the password.
175
+ return f"{type(self).__name__}(base_url={str(self.base_url)!r})"
176
+
177
+ def _build_request(self, options: RequestOptions) -> httpx.Request:
178
+ url = self.base_url.join(options.path.lstrip("/"))
179
+ query = {**self._default_query, **options.query, **(options.extra_query or {})}
180
+
181
+ headers: dict[str, str] = {"Accept": "application/json", **_platform_headers(), **self._default_headers}
182
+ for name, value in {**options.headers, **(options.extra_headers or {})}.items():
183
+ if isinstance(value, NotGiven):
184
+ headers.pop(name, None)
185
+ elif value is not None:
186
+ headers[name] = str(value)
187
+ if options.stream:
188
+ headers["Accept"] = "text/event-stream"
189
+ if self._authorization is not None and "Authorization" not in headers:
190
+ headers["Authorization"] = self._authorization
191
+
192
+ content: bytes | None = options.content
193
+ if content is not None:
194
+ headers.setdefault("Content-Type", "application/octet-stream")
195
+ elif not isinstance(options.body, NotGiven) or options.extra_body:
196
+ body: Any = to_jsonable(options.body) if not isinstance(options.body, NotGiven) else {}
197
+ if options.extra_body:
198
+ if not isinstance(body, dict):
199
+ raise TypeError("extra_body can only be used with JSON object bodies")
200
+ extra: dict[str, Any] = to_jsonable(options.extra_body)
201
+ body = {**cast("dict[str, Any]", body), **extra}
202
+ content = _json_bytes(body)
203
+ headers["Content-Type"] = "application/json"
204
+
205
+ timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout
206
+ if options.stream and isinstance(options.timeout, NotGiven):
207
+ # Event streams stay open between events, so only bound the connect phase by default.
208
+ base = timeout if isinstance(timeout, httpx.Timeout) else httpx.Timeout(timeout)
209
+ timeout = httpx.Timeout(connect=base.connect, read=None, write=base.write, pool=base.pool)
210
+ return httpx.Request(
211
+ options.method,
212
+ url,
213
+ params=httpx.QueryParams(cast("list[tuple[str, str | int | float | bool | None]]", encode_query(query))),
214
+ headers=headers,
215
+ content=content,
216
+ extensions={"timeout": _timeout_dict(timeout)},
217
+ )
218
+
219
+ def _status_error(self, response: httpx.Response, raw: object) -> APIStatusError:
220
+ body: object = raw
221
+ fields = cast("dict[str, Any]", raw) if isinstance(raw, dict) else {}
222
+ tag: object = fields.get("_tag")
223
+ cls = status_error_class(response.status_code, tag if isinstance(tag, str) else None, TAGGED_ERRORS)
224
+ return cls(error_message(body, response.status_code), response=response, body=body)
225
+
226
+ def _should_retry_status(self, options: RequestOptions, response: httpx.Response) -> bool:
227
+ statuses = constants.RETRYABLE_STATUSES if options.idempotent else constants.RETRYABLE_STATUSES_UNSAFE
228
+ return response.status_code in statuses
229
+
230
+ @staticmethod
231
+ def _should_retry_error(options: RequestOptions, error: httpx.TransportError) -> bool:
232
+ # These fail before the request reaches the server, so retrying never repeats work.
233
+ if isinstance(error, (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout)):
234
+ return True
235
+ return options.idempotent
236
+
237
+ @staticmethod
238
+ def _retry_delay(attempt: int, response: httpx.Response | None) -> float:
239
+ if response is not None:
240
+ retry_after = _parse_retry_after(response.headers)
241
+ if retry_after is not None and 0 <= retry_after <= constants.MAX_RETRY_AFTER:
242
+ return retry_after
243
+ delay: float = min(constants.INITIAL_RETRY_DELAY * 2.0**attempt, constants.MAX_RETRY_DELAY)
244
+ return delay * (1 - 0.25 * random.random())
245
+
246
+ @staticmethod
247
+ def _decode_error_body(response: httpx.Response) -> object:
248
+ if not response.content:
249
+ return None
250
+ try:
251
+ return response.json()
252
+ except ValueError:
253
+ return response.text
254
+
255
+ def _parse(self, response: httpx.Response, kind: ResponseKind, response_type: Any, unwrap: bool) -> Any:
256
+ if kind == "void":
257
+ return None
258
+ if kind == "binary":
259
+ return response.content
260
+ try:
261
+ payload: Any = response.json()
262
+ except ValueError as exc:
263
+ raise APIResponseValidationError(response, response.text, message="Response is not valid JSON") from exc
264
+ if unwrap:
265
+ if not isinstance(payload, dict) or "data" not in payload:
266
+ raise APIResponseValidationError(
267
+ response, cast("object", payload), message="Response has no `data` field"
268
+ )
269
+ payload = cast("dict[str, Any]", payload)["data"]
270
+ try:
271
+ return type_adapter(response_type).validate_python(payload)
272
+ except pydantic.ValidationError as exc:
273
+ raise APIResponseValidationError(response, payload, message=validation_summary(exc)) from exc
274
+
275
+
276
+ def _json_bytes(body: object) -> bytes:
277
+ return json.dumps(body, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8")
278
+
279
+
280
+ def _timeout_dict(timeout: Timeout) -> dict[str, float | None]:
281
+ resolved = timeout if isinstance(timeout, httpx.Timeout) else httpx.Timeout(timeout)
282
+ return resolved.as_dict()
283
+
284
+
285
+ def _parse_retry_after(headers: httpx.Headers) -> float | None:
286
+ milliseconds = headers.get("retry-after-ms")
287
+ if milliseconds is not None:
288
+ try:
289
+ return float(milliseconds) / 1000
290
+ except ValueError:
291
+ pass
292
+ value = headers.get("retry-after")
293
+ if value is None:
294
+ return None
295
+ try:
296
+ return float(value)
297
+ except ValueError:
298
+ parsed = email.utils.parsedate_to_datetime(value) if value else None
299
+ if parsed is None:
300
+ return None
301
+ return max(parsed.timestamp() - time.time(), 0.0)
302
+
303
+
304
+ class SyncAPIClient(_BaseClient):
305
+ """Synchronous transport. Thread-safe: share one client across threads."""
306
+
307
+ _http: httpx.Client
308
+
309
+ def __init__(
310
+ self,
311
+ *,
312
+ base_url: str | httpx.URL | None,
313
+ password: str | None,
314
+ username: str,
315
+ directory: str | os.PathLike[str] | None,
316
+ timeout: Timeout,
317
+ max_retries: int,
318
+ default_headers: Mapping[str, str] | None,
319
+ default_query: Mapping[str, object] | None,
320
+ http_client: httpx.Client | None,
321
+ transport: httpx.BaseTransport | None = None,
322
+ ) -> None:
323
+ super().__init__(
324
+ base_url=base_url,
325
+ password=password,
326
+ username=username,
327
+ directory=directory,
328
+ timeout=timeout,
329
+ max_retries=max_retries,
330
+ default_headers=default_headers,
331
+ default_query=default_query,
332
+ )
333
+ self._owns_http = http_client is None
334
+ if http_client is not None and transport is not None:
335
+ raise ValueError("Pass either http_client or transport, not both")
336
+ self._http = http_client or httpx.Client(
337
+ limits=constants.DEFAULT_CONNECTION_LIMITS, follow_redirects=True, transport=transport
338
+ )
339
+
340
+ def close(self) -> None:
341
+ """Closes the connection pool, unless it was passed in by the caller."""
342
+ if self._owns_http:
343
+ self._http.close()
344
+
345
+ @property
346
+ def is_closed(self) -> bool:
347
+ return self._http.is_closed
348
+
349
+ def __enter__(self) -> Self:
350
+ return self
351
+
352
+ def __exit__(
353
+ self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
354
+ ) -> None:
355
+ self.close()
356
+
357
+ def _send(self, options: RequestOptions) -> httpx.Response:
358
+ attempt = 0
359
+ while True:
360
+ request = self._build_request(options)
361
+ log.debug("Request %s %s (attempt %d)", request.method, request.url, attempt + 1)
362
+ try:
363
+ response = self._http.send(request, stream=options.stream)
364
+ except httpx.TransportError as exc:
365
+ if attempt < self.max_retries and self._should_retry_error(options, exc):
366
+ delay = self._retry_delay(attempt, None)
367
+ log.info("Retrying %s %s in %.2fs after %s", request.method, request.url, delay, type(exc).__name__)
368
+ time.sleep(delay)
369
+ attempt += 1
370
+ continue
371
+ if isinstance(exc, httpx.TimeoutException):
372
+ raise APITimeoutError(request) from exc
373
+ raise APIConnectionError(request=request, message=f"Connection error: {exc}") from exc
374
+
375
+ log.debug("Response %s %s -> %d", request.method, request.url, response.status_code)
376
+ if response.is_success:
377
+ return response
378
+ if attempt < self.max_retries and self._should_retry_status(options, response):
379
+ delay = self._retry_delay(attempt, response)
380
+ response.close()
381
+ log.info(
382
+ "Retrying %s %s in %.2fs after status %d", request.method, request.url, delay, response.status_code
383
+ )
384
+ time.sleep(delay)
385
+ attempt += 1
386
+ continue
387
+ if options.stream:
388
+ response.read()
389
+ raise self._status_error(response, self._decode_error_body(response))
390
+
391
+ def request(
392
+ self,
393
+ options: RequestOptions,
394
+ *,
395
+ kind: ResponseKind = "json",
396
+ response_type: Any = object,
397
+ unwrap: bool = False,
398
+ ) -> Any:
399
+ """Sends a request and returns the decoded response."""
400
+ response = self._send(options)
401
+ return self._parse(response, kind, response_type, unwrap)
402
+
403
+ def stream(self, options: RequestOptions, *, item_type: type[_T] | Any) -> Stream[Any]:
404
+ """Opens a server-sent event stream."""
405
+ options.stream = True
406
+ response = self._send(options)
407
+ content_type = response.headers.get("content-type", "")
408
+ if content_type.split(";", 1)[0].strip().lower() != "text/event-stream":
409
+ response.read()
410
+ response.close()
411
+ raise APIResponseValidationError(
412
+ response, response.text, message=f"Expected an event stream, got {content_type!r}"
413
+ )
414
+ return Stream(response, type_adapter(item_type))
415
+
416
+ def page(self, options: RequestOptions, *, response_type: Any) -> SyncCursorPage[Any]:
417
+ """Fetches the first page of a cursor-paginated list."""
418
+ from ._pagination import SyncCursorPage
419
+
420
+ return SyncCursorPage.fetch(self, options, response_type)
421
+
422
+
423
+ class AsyncAPIClient(_BaseClient):
424
+ """Asynchronous transport. Works with asyncio and trio."""
425
+
426
+ _http: httpx.AsyncClient
427
+
428
+ def __init__(
429
+ self,
430
+ *,
431
+ base_url: str | httpx.URL | None,
432
+ password: str | None,
433
+ username: str,
434
+ directory: str | os.PathLike[str] | None,
435
+ timeout: Timeout,
436
+ max_retries: int,
437
+ default_headers: Mapping[str, str] | None,
438
+ default_query: Mapping[str, object] | None,
439
+ http_client: httpx.AsyncClient | None,
440
+ transport: httpx.AsyncBaseTransport | None = None,
441
+ ) -> None:
442
+ super().__init__(
443
+ base_url=base_url,
444
+ password=password,
445
+ username=username,
446
+ directory=directory,
447
+ timeout=timeout,
448
+ max_retries=max_retries,
449
+ default_headers=default_headers,
450
+ default_query=default_query,
451
+ )
452
+ self._owns_http = http_client is None
453
+ if http_client is not None and transport is not None:
454
+ raise ValueError("Pass either http_client or transport, not both")
455
+ self._http = http_client or httpx.AsyncClient(
456
+ limits=constants.DEFAULT_CONNECTION_LIMITS, follow_redirects=True, transport=transport
457
+ )
458
+
459
+ async def close(self) -> None:
460
+ """Closes the connection pool, unless it was passed in by the caller."""
461
+ if self._owns_http:
462
+ await self._http.aclose()
463
+
464
+ @property
465
+ def is_closed(self) -> bool:
466
+ return self._http.is_closed
467
+
468
+ async def __aenter__(self) -> Self:
469
+ return self
470
+
471
+ async def __aexit__(
472
+ self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
473
+ ) -> None:
474
+ await self.close()
475
+
476
+ async def _send(self, options: RequestOptions) -> httpx.Response:
477
+ attempt = 0
478
+ while True:
479
+ request = self._build_request(options)
480
+ log.debug("Request %s %s (attempt %d)", request.method, request.url, attempt + 1)
481
+ try:
482
+ response = await self._http.send(request, stream=options.stream)
483
+ except httpx.TransportError as exc:
484
+ if attempt < self.max_retries and self._should_retry_error(options, exc):
485
+ delay = self._retry_delay(attempt, None)
486
+ log.info("Retrying %s %s in %.2fs after %s", request.method, request.url, delay, type(exc).__name__)
487
+ await anyio.sleep(delay)
488
+ attempt += 1
489
+ continue
490
+ if isinstance(exc, httpx.TimeoutException):
491
+ raise APITimeoutError(request) from exc
492
+ raise APIConnectionError(request=request, message=f"Connection error: {exc}") from exc
493
+
494
+ log.debug("Response %s %s -> %d", request.method, request.url, response.status_code)
495
+ if response.is_success:
496
+ return response
497
+ if attempt < self.max_retries and self._should_retry_status(options, response):
498
+ delay = self._retry_delay(attempt, response)
499
+ await response.aclose()
500
+ log.info(
501
+ "Retrying %s %s in %.2fs after status %d", request.method, request.url, delay, response.status_code
502
+ )
503
+ await anyio.sleep(delay)
504
+ attempt += 1
505
+ continue
506
+ if options.stream:
507
+ await response.aread()
508
+ raise self._status_error(response, self._decode_error_body(response))
509
+
510
+ async def request(
511
+ self,
512
+ options: RequestOptions,
513
+ *,
514
+ kind: ResponseKind = "json",
515
+ response_type: Any = object,
516
+ unwrap: bool = False,
517
+ ) -> Any:
518
+ """Sends a request and returns the decoded response."""
519
+ response = await self._send(options)
520
+ return self._parse(response, kind, response_type, unwrap)
521
+
522
+ async def stream(self, options: RequestOptions, *, item_type: type[_T] | Any) -> AsyncStream[Any]:
523
+ """Opens a server-sent event stream."""
524
+ options.stream = True
525
+ response = await self._send(options)
526
+ content_type = response.headers.get("content-type", "")
527
+ if content_type.split(";", 1)[0].strip().lower() != "text/event-stream":
528
+ await response.aread()
529
+ await response.aclose()
530
+ raise APIResponseValidationError(
531
+ response, response.text, message=f"Expected an event stream, got {content_type!r}"
532
+ )
533
+ return AsyncStream(response, type_adapter(item_type))
534
+
535
+ def page(self, options: RequestOptions, *, response_type: Any) -> AsyncPaginator[Any]:
536
+ """Returns an awaitable, async-iterable paginator over a cursor-paginated list."""
537
+ from ._pagination import AsyncPaginator
538
+
539
+ return AsyncPaginator(self, options, response_type)