scale-gp-beta 0.1.0a2__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 (78) hide show
  1. scale_gp/__init__.py +96 -0
  2. scale_gp/_base_client.py +2058 -0
  3. scale_gp/_client.py +544 -0
  4. scale_gp/_compat.py +219 -0
  5. scale_gp/_constants.py +14 -0
  6. scale_gp/_exceptions.py +108 -0
  7. scale_gp/_files.py +123 -0
  8. scale_gp/_models.py +801 -0
  9. scale_gp/_qs.py +150 -0
  10. scale_gp/_resource.py +43 -0
  11. scale_gp/_response.py +830 -0
  12. scale_gp/_streaming.py +333 -0
  13. scale_gp/_types.py +217 -0
  14. scale_gp/_utils/__init__.py +57 -0
  15. scale_gp/_utils/_logs.py +25 -0
  16. scale_gp/_utils/_proxy.py +62 -0
  17. scale_gp/_utils/_reflection.py +42 -0
  18. scale_gp/_utils/_streams.py +12 -0
  19. scale_gp/_utils/_sync.py +86 -0
  20. scale_gp/_utils/_transform.py +402 -0
  21. scale_gp/_utils/_typing.py +149 -0
  22. scale_gp/_utils/_utils.py +414 -0
  23. scale_gp/_version.py +4 -0
  24. scale_gp/lib/.keep +4 -0
  25. scale_gp/pagination.py +83 -0
  26. scale_gp/py.typed +0 -0
  27. scale_gp/resources/__init__.py +103 -0
  28. scale_gp/resources/chat/__init__.py +33 -0
  29. scale_gp/resources/chat/chat.py +102 -0
  30. scale_gp/resources/chat/completions.py +1054 -0
  31. scale_gp/resources/completions.py +765 -0
  32. scale_gp/resources/files/__init__.py +33 -0
  33. scale_gp/resources/files/content.py +162 -0
  34. scale_gp/resources/files/files.py +558 -0
  35. scale_gp/resources/inference.py +210 -0
  36. scale_gp/resources/models.py +834 -0
  37. scale_gp/resources/question_sets.py +680 -0
  38. scale_gp/resources/questions.py +396 -0
  39. scale_gp/types/__init__.py +33 -0
  40. scale_gp/types/chat/__init__.py +8 -0
  41. scale_gp/types/chat/chat_completion.py +257 -0
  42. scale_gp/types/chat/chat_completion_chunk.py +240 -0
  43. scale_gp/types/chat/completion_create_params.py +156 -0
  44. scale_gp/types/chat/completion_create_response.py +11 -0
  45. scale_gp/types/completion.py +116 -0
  46. scale_gp/types/completion_create_params.py +108 -0
  47. scale_gp/types/file.py +30 -0
  48. scale_gp/types/file_create_params.py +13 -0
  49. scale_gp/types/file_delete_response.py +16 -0
  50. scale_gp/types/file_list.py +27 -0
  51. scale_gp/types/file_list_params.py +16 -0
  52. scale_gp/types/file_update_params.py +12 -0
  53. scale_gp/types/files/__init__.py +3 -0
  54. scale_gp/types/inference_create_params.py +25 -0
  55. scale_gp/types/inference_create_response.py +11 -0
  56. scale_gp/types/inference_model.py +167 -0
  57. scale_gp/types/inference_model_list.py +27 -0
  58. scale_gp/types/inference_response.py +14 -0
  59. scale_gp/types/inference_response_chunk.py +14 -0
  60. scale_gp/types/model_create_params.py +165 -0
  61. scale_gp/types/model_delete_response.py +16 -0
  62. scale_gp/types/model_list_params.py +20 -0
  63. scale_gp/types/model_update_params.py +161 -0
  64. scale_gp/types/question.py +68 -0
  65. scale_gp/types/question_create_params.py +59 -0
  66. scale_gp/types/question_list.py +27 -0
  67. scale_gp/types/question_list_params.py +16 -0
  68. scale_gp/types/question_set.py +106 -0
  69. scale_gp/types/question_set_create_params.py +115 -0
  70. scale_gp/types/question_set_delete_response.py +16 -0
  71. scale_gp/types/question_set_list.py +27 -0
  72. scale_gp/types/question_set_list_params.py +20 -0
  73. scale_gp/types/question_set_retrieve_params.py +12 -0
  74. scale_gp/types/question_set_update_params.py +23 -0
  75. scale_gp_beta-0.1.0a2.dist-info/METADATA +440 -0
  76. scale_gp_beta-0.1.0a2.dist-info/RECORD +78 -0
  77. scale_gp_beta-0.1.0a2.dist-info/WHEEL +4 -0
  78. scale_gp_beta-0.1.0a2.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,2058 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import json
5
+ import time
6
+ import uuid
7
+ import email
8
+ import asyncio
9
+ import inspect
10
+ import logging
11
+ import platform
12
+ import warnings
13
+ import email.utils
14
+ from types import TracebackType
15
+ from random import random
16
+ from typing import (
17
+ TYPE_CHECKING,
18
+ Any,
19
+ Dict,
20
+ Type,
21
+ Union,
22
+ Generic,
23
+ Mapping,
24
+ TypeVar,
25
+ Iterable,
26
+ Iterator,
27
+ Optional,
28
+ Generator,
29
+ AsyncIterator,
30
+ cast,
31
+ overload,
32
+ )
33
+ from typing_extensions import Literal, override, get_origin
34
+
35
+ import anyio
36
+ import httpx
37
+ import distro
38
+ import pydantic
39
+ from httpx import URL, Limits
40
+ from pydantic import PrivateAttr
41
+
42
+ from . import _exceptions
43
+ from ._qs import Querystring
44
+ from ._files import to_httpx_files, async_to_httpx_files
45
+ from ._types import (
46
+ NOT_GIVEN,
47
+ Body,
48
+ Omit,
49
+ Query,
50
+ Headers,
51
+ Timeout,
52
+ NotGiven,
53
+ ResponseT,
54
+ Transport,
55
+ AnyMapping,
56
+ PostParser,
57
+ ProxiesTypes,
58
+ RequestFiles,
59
+ HttpxSendArgs,
60
+ AsyncTransport,
61
+ RequestOptions,
62
+ HttpxRequestFiles,
63
+ ModelBuilderProtocol,
64
+ )
65
+ from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
66
+ from ._compat import model_copy, model_dump
67
+ from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
68
+ from ._response import (
69
+ APIResponse,
70
+ BaseAPIResponse,
71
+ AsyncAPIResponse,
72
+ extract_response_type,
73
+ )
74
+ from ._constants import (
75
+ DEFAULT_TIMEOUT,
76
+ MAX_RETRY_DELAY,
77
+ DEFAULT_MAX_RETRIES,
78
+ INITIAL_RETRY_DELAY,
79
+ RAW_RESPONSE_HEADER,
80
+ OVERRIDE_CAST_TO_HEADER,
81
+ DEFAULT_CONNECTION_LIMITS,
82
+ )
83
+ from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder
84
+ from ._exceptions import (
85
+ APIStatusError,
86
+ APITimeoutError,
87
+ APIConnectionError,
88
+ APIResponseValidationError,
89
+ )
90
+
91
+ log: logging.Logger = logging.getLogger(__name__)
92
+
93
+ # TODO: make base page type vars covariant
94
+ SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]")
95
+ AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]")
96
+
97
+
98
+ _T = TypeVar("_T")
99
+ _T_co = TypeVar("_T_co", covariant=True)
100
+
101
+ _StreamT = TypeVar("_StreamT", bound=Stream[Any])
102
+ _AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any])
103
+
104
+ if TYPE_CHECKING:
105
+ from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT
106
+ else:
107
+ try:
108
+ from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT
109
+ except ImportError:
110
+ # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366
111
+ HTTPX_DEFAULT_TIMEOUT = Timeout(5.0)
112
+
113
+
114
+ class PageInfo:
115
+ """Stores the necessary information to build the request to retrieve the next page.
116
+
117
+ Either `url` or `params` must be set.
118
+ """
119
+
120
+ url: URL | NotGiven
121
+ params: Query | NotGiven
122
+
123
+ @overload
124
+ def __init__(
125
+ self,
126
+ *,
127
+ url: URL,
128
+ ) -> None: ...
129
+
130
+ @overload
131
+ def __init__(
132
+ self,
133
+ *,
134
+ params: Query,
135
+ ) -> None: ...
136
+
137
+ def __init__(
138
+ self,
139
+ *,
140
+ url: URL | NotGiven = NOT_GIVEN,
141
+ params: Query | NotGiven = NOT_GIVEN,
142
+ ) -> None:
143
+ self.url = url
144
+ self.params = params
145
+
146
+ @override
147
+ def __repr__(self) -> str:
148
+ if self.url:
149
+ return f"{self.__class__.__name__}(url={self.url})"
150
+ return f"{self.__class__.__name__}(params={self.params})"
151
+
152
+
153
+ class BasePage(GenericModel, Generic[_T]):
154
+ """
155
+ Defines the core interface for pagination.
156
+
157
+ Type Args:
158
+ ModelT: The pydantic model that represents an item in the response.
159
+
160
+ Methods:
161
+ has_next_page(): Check if there is another page available
162
+ next_page_info(): Get the necessary information to make a request for the next page
163
+ """
164
+
165
+ _options: FinalRequestOptions = PrivateAttr()
166
+ _model: Type[_T] = PrivateAttr()
167
+
168
+ def has_next_page(self) -> bool:
169
+ items = self._get_page_items()
170
+ if not items:
171
+ return False
172
+ return self.next_page_info() is not None
173
+
174
+ def next_page_info(self) -> Optional[PageInfo]: ...
175
+
176
+ def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body]
177
+ ...
178
+
179
+ def _params_from_url(self, url: URL) -> httpx.QueryParams:
180
+ # TODO: do we have to preprocess params here?
181
+ return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params)
182
+
183
+ def _info_to_options(self, info: PageInfo) -> FinalRequestOptions:
184
+ options = model_copy(self._options)
185
+ options._strip_raw_response_header()
186
+
187
+ if not isinstance(info.params, NotGiven):
188
+ options.params = {**options.params, **info.params}
189
+ return options
190
+
191
+ if not isinstance(info.url, NotGiven):
192
+ params = self._params_from_url(info.url)
193
+ url = info.url.copy_with(params=params)
194
+ options.params = dict(url.params)
195
+ options.url = str(url)
196
+ return options
197
+
198
+ raise ValueError("Unexpected PageInfo state")
199
+
200
+
201
+ class BaseSyncPage(BasePage[_T], Generic[_T]):
202
+ _client: SyncAPIClient = pydantic.PrivateAttr()
203
+
204
+ def _set_private_attributes(
205
+ self,
206
+ client: SyncAPIClient,
207
+ model: Type[_T],
208
+ options: FinalRequestOptions,
209
+ ) -> None:
210
+ self._model = model
211
+ self._client = client
212
+ self._options = options
213
+
214
+ # Pydantic uses a custom `__iter__` method to support casting BaseModels
215
+ # to dictionaries. e.g. dict(model).
216
+ # As we want to support `for item in page`, this is inherently incompatible
217
+ # with the default pydantic behaviour. It is not possible to support both
218
+ # use cases at once. Fortunately, this is not a big deal as all other pydantic
219
+ # methods should continue to work as expected as there is an alternative method
220
+ # to cast a model to a dictionary, model.dict(), which is used internally
221
+ # by pydantic.
222
+ def __iter__(self) -> Iterator[_T]: # type: ignore
223
+ for page in self.iter_pages():
224
+ for item in page._get_page_items():
225
+ yield item
226
+
227
+ def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]:
228
+ page = self
229
+ while True:
230
+ yield page
231
+ if page.has_next_page():
232
+ page = page.get_next_page()
233
+ else:
234
+ return
235
+
236
+ def get_next_page(self: SyncPageT) -> SyncPageT:
237
+ info = self.next_page_info()
238
+ if not info:
239
+ raise RuntimeError(
240
+ "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`."
241
+ )
242
+
243
+ options = self._info_to_options(info)
244
+ return self._client._request_api_list(self._model, page=self.__class__, options=options)
245
+
246
+
247
+ class AsyncPaginator(Generic[_T, AsyncPageT]):
248
+ def __init__(
249
+ self,
250
+ client: AsyncAPIClient,
251
+ options: FinalRequestOptions,
252
+ page_cls: Type[AsyncPageT],
253
+ model: Type[_T],
254
+ ) -> None:
255
+ self._model = model
256
+ self._client = client
257
+ self._options = options
258
+ self._page_cls = page_cls
259
+
260
+ def __await__(self) -> Generator[Any, None, AsyncPageT]:
261
+ return self._get_page().__await__()
262
+
263
+ async def _get_page(self) -> AsyncPageT:
264
+ def _parser(resp: AsyncPageT) -> AsyncPageT:
265
+ resp._set_private_attributes(
266
+ model=self._model,
267
+ options=self._options,
268
+ client=self._client,
269
+ )
270
+ return resp
271
+
272
+ self._options.post_parser = _parser
273
+
274
+ return await self._client.request(self._page_cls, self._options)
275
+
276
+ async def __aiter__(self) -> AsyncIterator[_T]:
277
+ # https://github.com/microsoft/pyright/issues/3464
278
+ page = cast(
279
+ AsyncPageT,
280
+ await self, # type: ignore
281
+ )
282
+ async for item in page:
283
+ yield item
284
+
285
+
286
+ class BaseAsyncPage(BasePage[_T], Generic[_T]):
287
+ _client: AsyncAPIClient = pydantic.PrivateAttr()
288
+
289
+ def _set_private_attributes(
290
+ self,
291
+ model: Type[_T],
292
+ client: AsyncAPIClient,
293
+ options: FinalRequestOptions,
294
+ ) -> None:
295
+ self._model = model
296
+ self._client = client
297
+ self._options = options
298
+
299
+ async def __aiter__(self) -> AsyncIterator[_T]:
300
+ async for page in self.iter_pages():
301
+ for item in page._get_page_items():
302
+ yield item
303
+
304
+ async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]:
305
+ page = self
306
+ while True:
307
+ yield page
308
+ if page.has_next_page():
309
+ page = await page.get_next_page()
310
+ else:
311
+ return
312
+
313
+ async def get_next_page(self: AsyncPageT) -> AsyncPageT:
314
+ info = self.next_page_info()
315
+ if not info:
316
+ raise RuntimeError(
317
+ "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`."
318
+ )
319
+
320
+ options = self._info_to_options(info)
321
+ return await self._client._request_api_list(self._model, page=self.__class__, options=options)
322
+
323
+
324
+ _HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient])
325
+ _DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]])
326
+
327
+
328
+ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
329
+ _client: _HttpxClientT
330
+ _version: str
331
+ _base_url: URL
332
+ max_retries: int
333
+ timeout: Union[float, Timeout, None]
334
+ _limits: httpx.Limits
335
+ _proxies: ProxiesTypes | None
336
+ _transport: Transport | AsyncTransport | None
337
+ _strict_response_validation: bool
338
+ _idempotency_header: str | None
339
+ _default_stream_cls: type[_DefaultStreamT] | None = None
340
+
341
+ def __init__(
342
+ self,
343
+ *,
344
+ version: str,
345
+ base_url: str | URL,
346
+ _strict_response_validation: bool,
347
+ max_retries: int = DEFAULT_MAX_RETRIES,
348
+ timeout: float | Timeout | None = DEFAULT_TIMEOUT,
349
+ limits: httpx.Limits,
350
+ transport: Transport | AsyncTransport | None,
351
+ proxies: ProxiesTypes | None,
352
+ custom_headers: Mapping[str, str] | None = None,
353
+ custom_query: Mapping[str, object] | None = None,
354
+ ) -> None:
355
+ self._version = version
356
+ self._base_url = self._enforce_trailing_slash(URL(base_url))
357
+ self.max_retries = max_retries
358
+ self.timeout = timeout
359
+ self._limits = limits
360
+ self._proxies = proxies
361
+ self._transport = transport
362
+ self._custom_headers = custom_headers or {}
363
+ self._custom_query = custom_query or {}
364
+ self._strict_response_validation = _strict_response_validation
365
+ self._idempotency_header = None
366
+ self._platform: Platform | None = None
367
+
368
+ if max_retries is None: # pyright: ignore[reportUnnecessaryComparison]
369
+ raise TypeError(
370
+ "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `scale_gp.DEFAULT_MAX_RETRIES`"
371
+ )
372
+
373
+ def _enforce_trailing_slash(self, url: URL) -> URL:
374
+ if url.raw_path.endswith(b"/"):
375
+ return url
376
+ return url.copy_with(raw_path=url.raw_path + b"/")
377
+
378
+ def _make_status_error_from_response(
379
+ self,
380
+ response: httpx.Response,
381
+ ) -> APIStatusError:
382
+ if response.is_closed and not response.is_stream_consumed:
383
+ # We can't read the response body as it has been closed
384
+ # before it was read. This can happen if an event hook
385
+ # raises a status error.
386
+ body = None
387
+ err_msg = f"Error code: {response.status_code}"
388
+ else:
389
+ err_text = response.text.strip()
390
+ body = err_text
391
+
392
+ try:
393
+ body = json.loads(err_text)
394
+ err_msg = f"Error code: {response.status_code} - {body}"
395
+ except Exception:
396
+ err_msg = err_text or f"Error code: {response.status_code}"
397
+
398
+ return self._make_status_error(err_msg, body=body, response=response)
399
+
400
+ def _make_status_error(
401
+ self,
402
+ err_msg: str,
403
+ *,
404
+ body: object,
405
+ response: httpx.Response,
406
+ ) -> _exceptions.APIStatusError:
407
+ raise NotImplementedError()
408
+
409
+ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers:
410
+ custom_headers = options.headers or {}
411
+ headers_dict = _merge_mappings(self.default_headers, custom_headers)
412
+ self._validate_headers(headers_dict, custom_headers)
413
+
414
+ # headers are case-insensitive while dictionaries are not.
415
+ headers = httpx.Headers(headers_dict)
416
+
417
+ idempotency_header = self._idempotency_header
418
+ if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
419
+ headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
420
+
421
+ # Don't set these headers if they were already set or removed by the caller. We check
422
+ # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
423
+ lower_custom_headers = [header.lower() for header in custom_headers]
424
+ if "x-stainless-retry-count" not in lower_custom_headers:
425
+ headers["x-stainless-retry-count"] = str(retries_taken)
426
+ if "x-stainless-read-timeout" not in lower_custom_headers:
427
+ timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout
428
+ if isinstance(timeout, Timeout):
429
+ timeout = timeout.read
430
+ if timeout is not None:
431
+ headers["x-stainless-read-timeout"] = str(timeout)
432
+
433
+ return headers
434
+
435
+ def _prepare_url(self, url: str) -> URL:
436
+ """
437
+ Merge a URL argument together with any 'base_url' on the client,
438
+ to create the URL used for the outgoing request.
439
+ """
440
+ # Copied from httpx's `_merge_url` method.
441
+ merge_url = URL(url)
442
+ if merge_url.is_relative_url:
443
+ merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/")
444
+ return self.base_url.copy_with(raw_path=merge_raw_path)
445
+
446
+ return merge_url
447
+
448
+ def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder:
449
+ return SSEDecoder()
450
+
451
+ def _build_request(
452
+ self,
453
+ options: FinalRequestOptions,
454
+ *,
455
+ retries_taken: int = 0,
456
+ ) -> httpx.Request:
457
+ if log.isEnabledFor(logging.DEBUG):
458
+ log.debug("Request options: %s", model_dump(options, exclude_unset=True))
459
+
460
+ kwargs: dict[str, Any] = {}
461
+
462
+ json_data = options.json_data
463
+ if options.extra_json is not None:
464
+ if json_data is None:
465
+ json_data = cast(Body, options.extra_json)
466
+ elif is_mapping(json_data):
467
+ json_data = _merge_mappings(json_data, options.extra_json)
468
+ else:
469
+ raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`")
470
+
471
+ headers = self._build_headers(options, retries_taken=retries_taken)
472
+ params = _merge_mappings(self.default_query, options.params)
473
+ content_type = headers.get("Content-Type")
474
+ files = options.files
475
+
476
+ # If the given Content-Type header is multipart/form-data then it
477
+ # has to be removed so that httpx can generate the header with
478
+ # additional information for us as it has to be in this form
479
+ # for the server to be able to correctly parse the request:
480
+ # multipart/form-data; boundary=---abc--
481
+ if content_type is not None and content_type.startswith("multipart/form-data"):
482
+ if "boundary" not in content_type:
483
+ # only remove the header if the boundary hasn't been explicitly set
484
+ # as the caller doesn't want httpx to come up with their own boundary
485
+ headers.pop("Content-Type")
486
+
487
+ # As we are now sending multipart/form-data instead of application/json
488
+ # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
489
+ if json_data:
490
+ if not is_dict(json_data):
491
+ raise TypeError(
492
+ f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead."
493
+ )
494
+ kwargs["data"] = self._serialize_multipartform(json_data)
495
+
496
+ # httpx determines whether or not to send a "multipart/form-data"
497
+ # request based on the truthiness of the "files" argument.
498
+ # This gets around that issue by generating a dict value that
499
+ # evaluates to true.
500
+ #
501
+ # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186
502
+ if not files:
503
+ files = cast(HttpxRequestFiles, ForceMultipartDict())
504
+
505
+ prepared_url = self._prepare_url(options.url)
506
+ if "_" in prepared_url.host:
507
+ # work around https://github.com/encode/httpx/discussions/2880
508
+ kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
509
+
510
+ # TODO: report this error to httpx
511
+ return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
512
+ headers=headers,
513
+ timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout,
514
+ method=options.method,
515
+ url=prepared_url,
516
+ # the `Query` type that we use is incompatible with qs'
517
+ # `Params` type as it needs to be typed as `Mapping[str, object]`
518
+ # so that passing a `TypedDict` doesn't cause an error.
519
+ # https://github.com/microsoft/pyright/issues/3526#event-6715453066
520
+ params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
521
+ json=json_data,
522
+ files=files,
523
+ **kwargs,
524
+ )
525
+
526
+ def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]:
527
+ items = self.qs.stringify_items(
528
+ # TODO: type ignore is required as stringify_items is well typed but we can't be
529
+ # well typed without heavy validation.
530
+ data, # type: ignore
531
+ array_format="brackets",
532
+ )
533
+ serialized: dict[str, object] = {}
534
+ for key, value in items:
535
+ existing = serialized.get(key)
536
+
537
+ if not existing:
538
+ serialized[key] = value
539
+ continue
540
+
541
+ # If a value has already been set for this key then that
542
+ # means we're sending data like `array[]=[1, 2, 3]` and we
543
+ # need to tell httpx that we want to send multiple values with
544
+ # the same key which is done by using a list or a tuple.
545
+ #
546
+ # Note: 2d arrays should never result in the same key at both
547
+ # levels so it's safe to assume that if the value is a list,
548
+ # it was because we changed it to be a list.
549
+ if is_list(existing):
550
+ existing.append(value)
551
+ else:
552
+ serialized[key] = [existing, value]
553
+
554
+ return serialized
555
+
556
+ def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]:
557
+ if not is_given(options.headers):
558
+ return cast_to
559
+
560
+ # make a copy of the headers so we don't mutate user-input
561
+ headers = dict(options.headers)
562
+
563
+ # we internally support defining a temporary header to override the
564
+ # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response`
565
+ # see _response.py for implementation details
566
+ override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, NOT_GIVEN)
567
+ if is_given(override_cast_to):
568
+ options.headers = headers
569
+ return cast(Type[ResponseT], override_cast_to)
570
+
571
+ return cast_to
572
+
573
+ def _should_stream_response_body(self, request: httpx.Request) -> bool:
574
+ return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return]
575
+
576
+ def _process_response_data(
577
+ self,
578
+ *,
579
+ data: object,
580
+ cast_to: type[ResponseT],
581
+ response: httpx.Response,
582
+ ) -> ResponseT:
583
+ if data is None:
584
+ return cast(ResponseT, None)
585
+
586
+ if cast_to is object:
587
+ return cast(ResponseT, data)
588
+
589
+ try:
590
+ if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol):
591
+ return cast(ResponseT, cast_to.build(response=response, data=data))
592
+
593
+ if self._strict_response_validation:
594
+ return cast(ResponseT, validate_type(type_=cast_to, value=data))
595
+
596
+ return cast(ResponseT, construct_type(type_=cast_to, value=data))
597
+ except pydantic.ValidationError as err:
598
+ raise APIResponseValidationError(response=response, body=data) from err
599
+
600
+ @property
601
+ def qs(self) -> Querystring:
602
+ return Querystring()
603
+
604
+ @property
605
+ def custom_auth(self) -> httpx.Auth | None:
606
+ return None
607
+
608
+ @property
609
+ def auth_headers(self) -> dict[str, str]:
610
+ return {}
611
+
612
+ @property
613
+ def default_headers(self) -> dict[str, str | Omit]:
614
+ return {
615
+ "Accept": "application/json",
616
+ "Content-Type": "application/json",
617
+ "User-Agent": self.user_agent,
618
+ **self.platform_headers(),
619
+ **self.auth_headers,
620
+ **self._custom_headers,
621
+ }
622
+
623
+ @property
624
+ def default_query(self) -> dict[str, object]:
625
+ return {
626
+ **self._custom_query,
627
+ }
628
+
629
+ def _validate_headers(
630
+ self,
631
+ headers: Headers, # noqa: ARG002
632
+ custom_headers: Headers, # noqa: ARG002
633
+ ) -> None:
634
+ """Validate the given default headers and custom headers.
635
+
636
+ Does nothing by default.
637
+ """
638
+ return
639
+
640
+ @property
641
+ def user_agent(self) -> str:
642
+ return f"{self.__class__.__name__}/Python {self._version}"
643
+
644
+ @property
645
+ def base_url(self) -> URL:
646
+ return self._base_url
647
+
648
+ @base_url.setter
649
+ def base_url(self, url: URL | str) -> None:
650
+ self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url))
651
+
652
+ def platform_headers(self) -> Dict[str, str]:
653
+ # the actual implementation is in a separate `lru_cache` decorated
654
+ # function because adding `lru_cache` to methods will leak memory
655
+ # https://github.com/python/cpython/issues/88476
656
+ return platform_headers(self._version, platform=self._platform)
657
+
658
+ def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None:
659
+ """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified.
660
+
661
+ About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
662
+ See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax
663
+ """
664
+ if response_headers is None:
665
+ return None
666
+
667
+ # First, try the non-standard `retry-after-ms` header for milliseconds,
668
+ # which is more precise than integer-seconds `retry-after`
669
+ try:
670
+ retry_ms_header = response_headers.get("retry-after-ms", None)
671
+ return float(retry_ms_header) / 1000
672
+ except (TypeError, ValueError):
673
+ pass
674
+
675
+ # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats).
676
+ retry_header = response_headers.get("retry-after")
677
+ try:
678
+ # note: the spec indicates that this should only ever be an integer
679
+ # but if someone sends a float there's no reason for us to not respect it
680
+ return float(retry_header)
681
+ except (TypeError, ValueError):
682
+ pass
683
+
684
+ # Last, try parsing `retry-after` as a date.
685
+ retry_date_tuple = email.utils.parsedate_tz(retry_header)
686
+ if retry_date_tuple is None:
687
+ return None
688
+
689
+ retry_date = email.utils.mktime_tz(retry_date_tuple)
690
+ return float(retry_date - time.time())
691
+
692
+ def _calculate_retry_timeout(
693
+ self,
694
+ remaining_retries: int,
695
+ options: FinalRequestOptions,
696
+ response_headers: Optional[httpx.Headers] = None,
697
+ ) -> float:
698
+ max_retries = options.get_max_retries(self.max_retries)
699
+
700
+ # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says.
701
+ retry_after = self._parse_retry_after_header(response_headers)
702
+ if retry_after is not None and 0 < retry_after <= 60:
703
+ return retry_after
704
+
705
+ # Also cap retry count to 1000 to avoid any potential overflows with `pow`
706
+ nb_retries = min(max_retries - remaining_retries, 1000)
707
+
708
+ # Apply exponential backoff, but not more than the max.
709
+ sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY)
710
+
711
+ # Apply some jitter, plus-or-minus half a second.
712
+ jitter = 1 - 0.25 * random()
713
+ timeout = sleep_seconds * jitter
714
+ return timeout if timeout >= 0 else 0
715
+
716
+ def _should_retry(self, response: httpx.Response) -> bool:
717
+ # Note: this is not a standard header
718
+ should_retry_header = response.headers.get("x-should-retry")
719
+
720
+ # If the server explicitly says whether or not to retry, obey.
721
+ if should_retry_header == "true":
722
+ log.debug("Retrying as header `x-should-retry` is set to `true`")
723
+ return True
724
+ if should_retry_header == "false":
725
+ log.debug("Not retrying as header `x-should-retry` is set to `false`")
726
+ return False
727
+
728
+ # Retry on request timeouts.
729
+ if response.status_code == 408:
730
+ log.debug("Retrying due to status code %i", response.status_code)
731
+ return True
732
+
733
+ # Retry on lock timeouts.
734
+ if response.status_code == 409:
735
+ log.debug("Retrying due to status code %i", response.status_code)
736
+ return True
737
+
738
+ # Retry on rate limits.
739
+ if response.status_code == 429:
740
+ log.debug("Retrying due to status code %i", response.status_code)
741
+ return True
742
+
743
+ # Retry internal errors.
744
+ if response.status_code >= 500:
745
+ log.debug("Retrying due to status code %i", response.status_code)
746
+ return True
747
+
748
+ log.debug("Not retrying")
749
+ return False
750
+
751
+ def _idempotency_key(self) -> str:
752
+ return f"stainless-python-retry-{uuid.uuid4()}"
753
+
754
+
755
+ class _DefaultHttpxClient(httpx.Client):
756
+ def __init__(self, **kwargs: Any) -> None:
757
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
758
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
759
+ kwargs.setdefault("follow_redirects", True)
760
+ super().__init__(**kwargs)
761
+
762
+
763
+ if TYPE_CHECKING:
764
+ DefaultHttpxClient = httpx.Client
765
+ """An alias to `httpx.Client` that provides the same defaults that this SDK
766
+ uses internally.
767
+
768
+ This is useful because overriding the `http_client` with your own instance of
769
+ `httpx.Client` will result in httpx's defaults being used, not ours.
770
+ """
771
+ else:
772
+ DefaultHttpxClient = _DefaultHttpxClient
773
+
774
+
775
+ class SyncHttpxClientWrapper(DefaultHttpxClient):
776
+ def __del__(self) -> None:
777
+ if self.is_closed:
778
+ return
779
+
780
+ try:
781
+ self.close()
782
+ except Exception:
783
+ pass
784
+
785
+
786
+ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
787
+ _client: httpx.Client
788
+ _default_stream_cls: type[Stream[Any]] | None = None
789
+
790
+ def __init__(
791
+ self,
792
+ *,
793
+ version: str,
794
+ base_url: str | URL,
795
+ max_retries: int = DEFAULT_MAX_RETRIES,
796
+ timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
797
+ transport: Transport | None = None,
798
+ proxies: ProxiesTypes | None = None,
799
+ limits: Limits | None = None,
800
+ http_client: httpx.Client | None = None,
801
+ custom_headers: Mapping[str, str] | None = None,
802
+ custom_query: Mapping[str, object] | None = None,
803
+ _strict_response_validation: bool,
804
+ ) -> None:
805
+ kwargs: dict[str, Any] = {}
806
+ if limits is not None:
807
+ warnings.warn(
808
+ "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
809
+ category=DeprecationWarning,
810
+ stacklevel=3,
811
+ )
812
+ if http_client is not None:
813
+ raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
814
+ else:
815
+ limits = DEFAULT_CONNECTION_LIMITS
816
+
817
+ if transport is not None:
818
+ kwargs["transport"] = transport
819
+ warnings.warn(
820
+ "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
821
+ category=DeprecationWarning,
822
+ stacklevel=3,
823
+ )
824
+ if http_client is not None:
825
+ raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
826
+
827
+ if proxies is not None:
828
+ kwargs["proxies"] = proxies
829
+ warnings.warn(
830
+ "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
831
+ category=DeprecationWarning,
832
+ stacklevel=3,
833
+ )
834
+ if http_client is not None:
835
+ raise ValueError("The `http_client` argument is mutually exclusive with `proxies`")
836
+
837
+ if not is_given(timeout):
838
+ # if the user passed in a custom http client with a non-default
839
+ # timeout set then we use that timeout.
840
+ #
841
+ # note: there is an edge case here where the user passes in a client
842
+ # where they've explicitly set the timeout to match the default timeout
843
+ # as this check is structural, meaning that we'll think they didn't
844
+ # pass in a timeout and will ignore it
845
+ if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT:
846
+ timeout = http_client.timeout
847
+ else:
848
+ timeout = DEFAULT_TIMEOUT
849
+
850
+ if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance]
851
+ raise TypeError(
852
+ f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}"
853
+ )
854
+
855
+ super().__init__(
856
+ version=version,
857
+ limits=limits,
858
+ # cast to a valid type because mypy doesn't understand our type narrowing
859
+ timeout=cast(Timeout, timeout),
860
+ proxies=proxies,
861
+ base_url=base_url,
862
+ transport=transport,
863
+ max_retries=max_retries,
864
+ custom_query=custom_query,
865
+ custom_headers=custom_headers,
866
+ _strict_response_validation=_strict_response_validation,
867
+ )
868
+ self._client = http_client or SyncHttpxClientWrapper(
869
+ base_url=base_url,
870
+ # cast to a valid type because mypy doesn't understand our type narrowing
871
+ timeout=cast(Timeout, timeout),
872
+ limits=limits,
873
+ follow_redirects=True,
874
+ **kwargs, # type: ignore
875
+ )
876
+
877
+ def is_closed(self) -> bool:
878
+ return self._client.is_closed
879
+
880
+ def close(self) -> None:
881
+ """Close the underlying HTTPX client.
882
+
883
+ The client will *not* be usable after this.
884
+ """
885
+ # If an error is thrown while constructing a client, self._client
886
+ # may not be present
887
+ if hasattr(self, "_client"):
888
+ self._client.close()
889
+
890
+ def __enter__(self: _T) -> _T:
891
+ return self
892
+
893
+ def __exit__(
894
+ self,
895
+ exc_type: type[BaseException] | None,
896
+ exc: BaseException | None,
897
+ exc_tb: TracebackType | None,
898
+ ) -> None:
899
+ self.close()
900
+
901
+ def _prepare_options(
902
+ self,
903
+ options: FinalRequestOptions, # noqa: ARG002
904
+ ) -> FinalRequestOptions:
905
+ """Hook for mutating the given options"""
906
+ return options
907
+
908
+ def _prepare_request(
909
+ self,
910
+ request: httpx.Request, # noqa: ARG002
911
+ ) -> None:
912
+ """This method is used as a callback for mutating the `Request` object
913
+ after it has been constructed.
914
+ This is useful for cases where you want to add certain headers based off of
915
+ the request properties, e.g. `url`, `method` etc.
916
+ """
917
+ return None
918
+
919
+ @overload
920
+ def request(
921
+ self,
922
+ cast_to: Type[ResponseT],
923
+ options: FinalRequestOptions,
924
+ remaining_retries: Optional[int] = None,
925
+ *,
926
+ stream: Literal[True],
927
+ stream_cls: Type[_StreamT],
928
+ ) -> _StreamT: ...
929
+
930
+ @overload
931
+ def request(
932
+ self,
933
+ cast_to: Type[ResponseT],
934
+ options: FinalRequestOptions,
935
+ remaining_retries: Optional[int] = None,
936
+ *,
937
+ stream: Literal[False] = False,
938
+ ) -> ResponseT: ...
939
+
940
+ @overload
941
+ def request(
942
+ self,
943
+ cast_to: Type[ResponseT],
944
+ options: FinalRequestOptions,
945
+ remaining_retries: Optional[int] = None,
946
+ *,
947
+ stream: bool = False,
948
+ stream_cls: Type[_StreamT] | None = None,
949
+ ) -> ResponseT | _StreamT: ...
950
+
951
+ def request(
952
+ self,
953
+ cast_to: Type[ResponseT],
954
+ options: FinalRequestOptions,
955
+ remaining_retries: Optional[int] = None,
956
+ *,
957
+ stream: bool = False,
958
+ stream_cls: type[_StreamT] | None = None,
959
+ ) -> ResponseT | _StreamT:
960
+ if remaining_retries is not None:
961
+ retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
962
+ else:
963
+ retries_taken = 0
964
+
965
+ return self._request(
966
+ cast_to=cast_to,
967
+ options=options,
968
+ stream=stream,
969
+ stream_cls=stream_cls,
970
+ retries_taken=retries_taken,
971
+ )
972
+
973
+ def _request(
974
+ self,
975
+ *,
976
+ cast_to: Type[ResponseT],
977
+ options: FinalRequestOptions,
978
+ retries_taken: int,
979
+ stream: bool,
980
+ stream_cls: type[_StreamT] | None,
981
+ ) -> ResponseT | _StreamT:
982
+ # create a copy of the options we were given so that if the
983
+ # options are mutated later & we then retry, the retries are
984
+ # given the original options
985
+ input_options = model_copy(options)
986
+
987
+ cast_to = self._maybe_override_cast_to(cast_to, options)
988
+ options = self._prepare_options(options)
989
+
990
+ remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
991
+ request = self._build_request(options, retries_taken=retries_taken)
992
+ self._prepare_request(request)
993
+
994
+ kwargs: HttpxSendArgs = {}
995
+ if self.custom_auth is not None:
996
+ kwargs["auth"] = self.custom_auth
997
+
998
+ log.debug("Sending HTTP Request: %s %s", request.method, request.url)
999
+
1000
+ try:
1001
+ response = self._client.send(
1002
+ request,
1003
+ stream=stream or self._should_stream_response_body(request=request),
1004
+ **kwargs,
1005
+ )
1006
+ except httpx.TimeoutException as err:
1007
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
1008
+
1009
+ if remaining_retries > 0:
1010
+ return self._retry_request(
1011
+ input_options,
1012
+ cast_to,
1013
+ retries_taken=retries_taken,
1014
+ stream=stream,
1015
+ stream_cls=stream_cls,
1016
+ response_headers=None,
1017
+ )
1018
+
1019
+ log.debug("Raising timeout error")
1020
+ raise APITimeoutError(request=request) from err
1021
+ except Exception as err:
1022
+ log.debug("Encountered Exception", exc_info=True)
1023
+
1024
+ if remaining_retries > 0:
1025
+ return self._retry_request(
1026
+ input_options,
1027
+ cast_to,
1028
+ retries_taken=retries_taken,
1029
+ stream=stream,
1030
+ stream_cls=stream_cls,
1031
+ response_headers=None,
1032
+ )
1033
+
1034
+ log.debug("Raising connection error")
1035
+ raise APIConnectionError(request=request) from err
1036
+
1037
+ log.debug(
1038
+ 'HTTP Response: %s %s "%i %s" %s',
1039
+ request.method,
1040
+ request.url,
1041
+ response.status_code,
1042
+ response.reason_phrase,
1043
+ response.headers,
1044
+ )
1045
+
1046
+ try:
1047
+ response.raise_for_status()
1048
+ except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1049
+ log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1050
+
1051
+ if remaining_retries > 0 and self._should_retry(err.response):
1052
+ err.response.close()
1053
+ return self._retry_request(
1054
+ input_options,
1055
+ cast_to,
1056
+ retries_taken=retries_taken,
1057
+ response_headers=err.response.headers,
1058
+ stream=stream,
1059
+ stream_cls=stream_cls,
1060
+ )
1061
+
1062
+ # If the response is streamed then we need to explicitly read the response
1063
+ # to completion before attempting to access the response text.
1064
+ if not err.response.is_closed:
1065
+ err.response.read()
1066
+
1067
+ log.debug("Re-raising status error")
1068
+ raise self._make_status_error_from_response(err.response) from None
1069
+
1070
+ return self._process_response(
1071
+ cast_to=cast_to,
1072
+ options=options,
1073
+ response=response,
1074
+ stream=stream,
1075
+ stream_cls=stream_cls,
1076
+ retries_taken=retries_taken,
1077
+ )
1078
+
1079
+ def _retry_request(
1080
+ self,
1081
+ options: FinalRequestOptions,
1082
+ cast_to: Type[ResponseT],
1083
+ *,
1084
+ retries_taken: int,
1085
+ response_headers: httpx.Headers | None,
1086
+ stream: bool,
1087
+ stream_cls: type[_StreamT] | None,
1088
+ ) -> ResponseT | _StreamT:
1089
+ remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1090
+ if remaining_retries == 1:
1091
+ log.debug("1 retry left")
1092
+ else:
1093
+ log.debug("%i retries left", remaining_retries)
1094
+
1095
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
1096
+ log.info("Retrying request to %s in %f seconds", options.url, timeout)
1097
+
1098
+ # In a synchronous context we are blocking the entire thread. Up to the library user to run the client in a
1099
+ # different thread if necessary.
1100
+ time.sleep(timeout)
1101
+
1102
+ return self._request(
1103
+ options=options,
1104
+ cast_to=cast_to,
1105
+ retries_taken=retries_taken + 1,
1106
+ stream=stream,
1107
+ stream_cls=stream_cls,
1108
+ )
1109
+
1110
+ def _process_response(
1111
+ self,
1112
+ *,
1113
+ cast_to: Type[ResponseT],
1114
+ options: FinalRequestOptions,
1115
+ response: httpx.Response,
1116
+ stream: bool,
1117
+ stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
1118
+ retries_taken: int = 0,
1119
+ ) -> ResponseT:
1120
+ origin = get_origin(cast_to) or cast_to
1121
+
1122
+ if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse):
1123
+ if not issubclass(origin, APIResponse):
1124
+ raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}")
1125
+
1126
+ response_cls = cast("type[BaseAPIResponse[Any]]", cast_to)
1127
+ return cast(
1128
+ ResponseT,
1129
+ response_cls(
1130
+ raw=response,
1131
+ client=self,
1132
+ cast_to=extract_response_type(response_cls),
1133
+ stream=stream,
1134
+ stream_cls=stream_cls,
1135
+ options=options,
1136
+ retries_taken=retries_taken,
1137
+ ),
1138
+ )
1139
+
1140
+ if cast_to == httpx.Response:
1141
+ return cast(ResponseT, response)
1142
+
1143
+ api_response = APIResponse(
1144
+ raw=response,
1145
+ client=self,
1146
+ cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast]
1147
+ stream=stream,
1148
+ stream_cls=stream_cls,
1149
+ options=options,
1150
+ retries_taken=retries_taken,
1151
+ )
1152
+ if bool(response.request.headers.get(RAW_RESPONSE_HEADER)):
1153
+ return cast(ResponseT, api_response)
1154
+
1155
+ return api_response.parse()
1156
+
1157
+ def _request_api_list(
1158
+ self,
1159
+ model: Type[object],
1160
+ page: Type[SyncPageT],
1161
+ options: FinalRequestOptions,
1162
+ ) -> SyncPageT:
1163
+ def _parser(resp: SyncPageT) -> SyncPageT:
1164
+ resp._set_private_attributes(
1165
+ client=self,
1166
+ model=model,
1167
+ options=options,
1168
+ )
1169
+ return resp
1170
+
1171
+ options.post_parser = _parser
1172
+
1173
+ return self.request(page, options, stream=False)
1174
+
1175
+ @overload
1176
+ def get(
1177
+ self,
1178
+ path: str,
1179
+ *,
1180
+ cast_to: Type[ResponseT],
1181
+ options: RequestOptions = {},
1182
+ stream: Literal[False] = False,
1183
+ ) -> ResponseT: ...
1184
+
1185
+ @overload
1186
+ def get(
1187
+ self,
1188
+ path: str,
1189
+ *,
1190
+ cast_to: Type[ResponseT],
1191
+ options: RequestOptions = {},
1192
+ stream: Literal[True],
1193
+ stream_cls: type[_StreamT],
1194
+ ) -> _StreamT: ...
1195
+
1196
+ @overload
1197
+ def get(
1198
+ self,
1199
+ path: str,
1200
+ *,
1201
+ cast_to: Type[ResponseT],
1202
+ options: RequestOptions = {},
1203
+ stream: bool,
1204
+ stream_cls: type[_StreamT] | None = None,
1205
+ ) -> ResponseT | _StreamT: ...
1206
+
1207
+ def get(
1208
+ self,
1209
+ path: str,
1210
+ *,
1211
+ cast_to: Type[ResponseT],
1212
+ options: RequestOptions = {},
1213
+ stream: bool = False,
1214
+ stream_cls: type[_StreamT] | None = None,
1215
+ ) -> ResponseT | _StreamT:
1216
+ opts = FinalRequestOptions.construct(method="get", url=path, **options)
1217
+ # cast is required because mypy complains about returning Any even though
1218
+ # it understands the type variables
1219
+ return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
1220
+
1221
+ @overload
1222
+ def post(
1223
+ self,
1224
+ path: str,
1225
+ *,
1226
+ cast_to: Type[ResponseT],
1227
+ body: Body | None = None,
1228
+ options: RequestOptions = {},
1229
+ files: RequestFiles | None = None,
1230
+ stream: Literal[False] = False,
1231
+ ) -> ResponseT: ...
1232
+
1233
+ @overload
1234
+ def post(
1235
+ self,
1236
+ path: str,
1237
+ *,
1238
+ cast_to: Type[ResponseT],
1239
+ body: Body | None = None,
1240
+ options: RequestOptions = {},
1241
+ files: RequestFiles | None = None,
1242
+ stream: Literal[True],
1243
+ stream_cls: type[_StreamT],
1244
+ ) -> _StreamT: ...
1245
+
1246
+ @overload
1247
+ def post(
1248
+ self,
1249
+ path: str,
1250
+ *,
1251
+ cast_to: Type[ResponseT],
1252
+ body: Body | None = None,
1253
+ options: RequestOptions = {},
1254
+ files: RequestFiles | None = None,
1255
+ stream: bool,
1256
+ stream_cls: type[_StreamT] | None = None,
1257
+ ) -> ResponseT | _StreamT: ...
1258
+
1259
+ def post(
1260
+ self,
1261
+ path: str,
1262
+ *,
1263
+ cast_to: Type[ResponseT],
1264
+ body: Body | None = None,
1265
+ options: RequestOptions = {},
1266
+ files: RequestFiles | None = None,
1267
+ stream: bool = False,
1268
+ stream_cls: type[_StreamT] | None = None,
1269
+ ) -> ResponseT | _StreamT:
1270
+ opts = FinalRequestOptions.construct(
1271
+ method="post", url=path, json_data=body, files=to_httpx_files(files), **options
1272
+ )
1273
+ return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
1274
+
1275
+ def patch(
1276
+ self,
1277
+ path: str,
1278
+ *,
1279
+ cast_to: Type[ResponseT],
1280
+ body: Body | None = None,
1281
+ options: RequestOptions = {},
1282
+ ) -> ResponseT:
1283
+ opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
1284
+ return self.request(cast_to, opts)
1285
+
1286
+ def put(
1287
+ self,
1288
+ path: str,
1289
+ *,
1290
+ cast_to: Type[ResponseT],
1291
+ body: Body | None = None,
1292
+ files: RequestFiles | None = None,
1293
+ options: RequestOptions = {},
1294
+ ) -> ResponseT:
1295
+ opts = FinalRequestOptions.construct(
1296
+ method="put", url=path, json_data=body, files=to_httpx_files(files), **options
1297
+ )
1298
+ return self.request(cast_to, opts)
1299
+
1300
+ def delete(
1301
+ self,
1302
+ path: str,
1303
+ *,
1304
+ cast_to: Type[ResponseT],
1305
+ body: Body | None = None,
1306
+ options: RequestOptions = {},
1307
+ ) -> ResponseT:
1308
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
1309
+ return self.request(cast_to, opts)
1310
+
1311
+ def get_api_list(
1312
+ self,
1313
+ path: str,
1314
+ *,
1315
+ model: Type[object],
1316
+ page: Type[SyncPageT],
1317
+ body: Body | None = None,
1318
+ options: RequestOptions = {},
1319
+ method: str = "get",
1320
+ ) -> SyncPageT:
1321
+ opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options)
1322
+ return self._request_api_list(model, page, opts)
1323
+
1324
+
1325
+ class _DefaultAsyncHttpxClient(httpx.AsyncClient):
1326
+ def __init__(self, **kwargs: Any) -> None:
1327
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
1328
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
1329
+ kwargs.setdefault("follow_redirects", True)
1330
+ super().__init__(**kwargs)
1331
+
1332
+
1333
+ if TYPE_CHECKING:
1334
+ DefaultAsyncHttpxClient = httpx.AsyncClient
1335
+ """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
1336
+ uses internally.
1337
+
1338
+ This is useful because overriding the `http_client` with your own instance of
1339
+ `httpx.AsyncClient` will result in httpx's defaults being used, not ours.
1340
+ """
1341
+ else:
1342
+ DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
1343
+
1344
+
1345
+ class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
1346
+ def __del__(self) -> None:
1347
+ if self.is_closed:
1348
+ return
1349
+
1350
+ try:
1351
+ # TODO(someday): support non asyncio runtimes here
1352
+ asyncio.get_running_loop().create_task(self.aclose())
1353
+ except Exception:
1354
+ pass
1355
+
1356
+
1357
+ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1358
+ _client: httpx.AsyncClient
1359
+ _default_stream_cls: type[AsyncStream[Any]] | None = None
1360
+
1361
+ def __init__(
1362
+ self,
1363
+ *,
1364
+ version: str,
1365
+ base_url: str | URL,
1366
+ _strict_response_validation: bool,
1367
+ max_retries: int = DEFAULT_MAX_RETRIES,
1368
+ timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
1369
+ transport: AsyncTransport | None = None,
1370
+ proxies: ProxiesTypes | None = None,
1371
+ limits: Limits | None = None,
1372
+ http_client: httpx.AsyncClient | None = None,
1373
+ custom_headers: Mapping[str, str] | None = None,
1374
+ custom_query: Mapping[str, object] | None = None,
1375
+ ) -> None:
1376
+ kwargs: dict[str, Any] = {}
1377
+ if limits is not None:
1378
+ warnings.warn(
1379
+ "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
1380
+ category=DeprecationWarning,
1381
+ stacklevel=3,
1382
+ )
1383
+ if http_client is not None:
1384
+ raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
1385
+ else:
1386
+ limits = DEFAULT_CONNECTION_LIMITS
1387
+
1388
+ if transport is not None:
1389
+ kwargs["transport"] = transport
1390
+ warnings.warn(
1391
+ "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
1392
+ category=DeprecationWarning,
1393
+ stacklevel=3,
1394
+ )
1395
+ if http_client is not None:
1396
+ raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
1397
+
1398
+ if proxies is not None:
1399
+ kwargs["proxies"] = proxies
1400
+ warnings.warn(
1401
+ "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
1402
+ category=DeprecationWarning,
1403
+ stacklevel=3,
1404
+ )
1405
+ if http_client is not None:
1406
+ raise ValueError("The `http_client` argument is mutually exclusive with `proxies`")
1407
+
1408
+ if not is_given(timeout):
1409
+ # if the user passed in a custom http client with a non-default
1410
+ # timeout set then we use that timeout.
1411
+ #
1412
+ # note: there is an edge case here where the user passes in a client
1413
+ # where they've explicitly set the timeout to match the default timeout
1414
+ # as this check is structural, meaning that we'll think they didn't
1415
+ # pass in a timeout and will ignore it
1416
+ if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT:
1417
+ timeout = http_client.timeout
1418
+ else:
1419
+ timeout = DEFAULT_TIMEOUT
1420
+
1421
+ if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance]
1422
+ raise TypeError(
1423
+ f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}"
1424
+ )
1425
+
1426
+ super().__init__(
1427
+ version=version,
1428
+ base_url=base_url,
1429
+ limits=limits,
1430
+ # cast to a valid type because mypy doesn't understand our type narrowing
1431
+ timeout=cast(Timeout, timeout),
1432
+ proxies=proxies,
1433
+ transport=transport,
1434
+ max_retries=max_retries,
1435
+ custom_query=custom_query,
1436
+ custom_headers=custom_headers,
1437
+ _strict_response_validation=_strict_response_validation,
1438
+ )
1439
+ self._client = http_client or AsyncHttpxClientWrapper(
1440
+ base_url=base_url,
1441
+ # cast to a valid type because mypy doesn't understand our type narrowing
1442
+ timeout=cast(Timeout, timeout),
1443
+ limits=limits,
1444
+ follow_redirects=True,
1445
+ **kwargs, # type: ignore
1446
+ )
1447
+
1448
+ def is_closed(self) -> bool:
1449
+ return self._client.is_closed
1450
+
1451
+ async def close(self) -> None:
1452
+ """Close the underlying HTTPX client.
1453
+
1454
+ The client will *not* be usable after this.
1455
+ """
1456
+ await self._client.aclose()
1457
+
1458
+ async def __aenter__(self: _T) -> _T:
1459
+ return self
1460
+
1461
+ async def __aexit__(
1462
+ self,
1463
+ exc_type: type[BaseException] | None,
1464
+ exc: BaseException | None,
1465
+ exc_tb: TracebackType | None,
1466
+ ) -> None:
1467
+ await self.close()
1468
+
1469
+ async def _prepare_options(
1470
+ self,
1471
+ options: FinalRequestOptions, # noqa: ARG002
1472
+ ) -> FinalRequestOptions:
1473
+ """Hook for mutating the given options"""
1474
+ return options
1475
+
1476
+ async def _prepare_request(
1477
+ self,
1478
+ request: httpx.Request, # noqa: ARG002
1479
+ ) -> None:
1480
+ """This method is used as a callback for mutating the `Request` object
1481
+ after it has been constructed.
1482
+ This is useful for cases where you want to add certain headers based off of
1483
+ the request properties, e.g. `url`, `method` etc.
1484
+ """
1485
+ return None
1486
+
1487
+ @overload
1488
+ async def request(
1489
+ self,
1490
+ cast_to: Type[ResponseT],
1491
+ options: FinalRequestOptions,
1492
+ *,
1493
+ stream: Literal[False] = False,
1494
+ remaining_retries: Optional[int] = None,
1495
+ ) -> ResponseT: ...
1496
+
1497
+ @overload
1498
+ async def request(
1499
+ self,
1500
+ cast_to: Type[ResponseT],
1501
+ options: FinalRequestOptions,
1502
+ *,
1503
+ stream: Literal[True],
1504
+ stream_cls: type[_AsyncStreamT],
1505
+ remaining_retries: Optional[int] = None,
1506
+ ) -> _AsyncStreamT: ...
1507
+
1508
+ @overload
1509
+ async def request(
1510
+ self,
1511
+ cast_to: Type[ResponseT],
1512
+ options: FinalRequestOptions,
1513
+ *,
1514
+ stream: bool,
1515
+ stream_cls: type[_AsyncStreamT] | None = None,
1516
+ remaining_retries: Optional[int] = None,
1517
+ ) -> ResponseT | _AsyncStreamT: ...
1518
+
1519
+ async def request(
1520
+ self,
1521
+ cast_to: Type[ResponseT],
1522
+ options: FinalRequestOptions,
1523
+ *,
1524
+ stream: bool = False,
1525
+ stream_cls: type[_AsyncStreamT] | None = None,
1526
+ remaining_retries: Optional[int] = None,
1527
+ ) -> ResponseT | _AsyncStreamT:
1528
+ if remaining_retries is not None:
1529
+ retries_taken = options.get_max_retries(self.max_retries) - remaining_retries
1530
+ else:
1531
+ retries_taken = 0
1532
+
1533
+ return await self._request(
1534
+ cast_to=cast_to,
1535
+ options=options,
1536
+ stream=stream,
1537
+ stream_cls=stream_cls,
1538
+ retries_taken=retries_taken,
1539
+ )
1540
+
1541
+ async def _request(
1542
+ self,
1543
+ cast_to: Type[ResponseT],
1544
+ options: FinalRequestOptions,
1545
+ *,
1546
+ stream: bool,
1547
+ stream_cls: type[_AsyncStreamT] | None,
1548
+ retries_taken: int,
1549
+ ) -> ResponseT | _AsyncStreamT:
1550
+ if self._platform is None:
1551
+ # `get_platform` can make blocking IO calls so we
1552
+ # execute it earlier while we are in an async context
1553
+ self._platform = await asyncify(get_platform)()
1554
+
1555
+ # create a copy of the options we were given so that if the
1556
+ # options are mutated later & we then retry, the retries are
1557
+ # given the original options
1558
+ input_options = model_copy(options)
1559
+
1560
+ cast_to = self._maybe_override_cast_to(cast_to, options)
1561
+ options = await self._prepare_options(options)
1562
+
1563
+ remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1564
+ request = self._build_request(options, retries_taken=retries_taken)
1565
+ await self._prepare_request(request)
1566
+
1567
+ kwargs: HttpxSendArgs = {}
1568
+ if self.custom_auth is not None:
1569
+ kwargs["auth"] = self.custom_auth
1570
+
1571
+ try:
1572
+ response = await self._client.send(
1573
+ request,
1574
+ stream=stream or self._should_stream_response_body(request=request),
1575
+ **kwargs,
1576
+ )
1577
+ except httpx.TimeoutException as err:
1578
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
1579
+
1580
+ if remaining_retries > 0:
1581
+ return await self._retry_request(
1582
+ input_options,
1583
+ cast_to,
1584
+ retries_taken=retries_taken,
1585
+ stream=stream,
1586
+ stream_cls=stream_cls,
1587
+ response_headers=None,
1588
+ )
1589
+
1590
+ log.debug("Raising timeout error")
1591
+ raise APITimeoutError(request=request) from err
1592
+ except Exception as err:
1593
+ log.debug("Encountered Exception", exc_info=True)
1594
+
1595
+ if remaining_retries > 0:
1596
+ return await self._retry_request(
1597
+ input_options,
1598
+ cast_to,
1599
+ retries_taken=retries_taken,
1600
+ stream=stream,
1601
+ stream_cls=stream_cls,
1602
+ response_headers=None,
1603
+ )
1604
+
1605
+ log.debug("Raising connection error")
1606
+ raise APIConnectionError(request=request) from err
1607
+
1608
+ log.debug(
1609
+ 'HTTP Request: %s %s "%i %s"', request.method, request.url, response.status_code, response.reason_phrase
1610
+ )
1611
+
1612
+ try:
1613
+ response.raise_for_status()
1614
+ except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code
1615
+ log.debug("Encountered httpx.HTTPStatusError", exc_info=True)
1616
+
1617
+ if remaining_retries > 0 and self._should_retry(err.response):
1618
+ await err.response.aclose()
1619
+ return await self._retry_request(
1620
+ input_options,
1621
+ cast_to,
1622
+ retries_taken=retries_taken,
1623
+ response_headers=err.response.headers,
1624
+ stream=stream,
1625
+ stream_cls=stream_cls,
1626
+ )
1627
+
1628
+ # If the response is streamed then we need to explicitly read the response
1629
+ # to completion before attempting to access the response text.
1630
+ if not err.response.is_closed:
1631
+ await err.response.aread()
1632
+
1633
+ log.debug("Re-raising status error")
1634
+ raise self._make_status_error_from_response(err.response) from None
1635
+
1636
+ return await self._process_response(
1637
+ cast_to=cast_to,
1638
+ options=options,
1639
+ response=response,
1640
+ stream=stream,
1641
+ stream_cls=stream_cls,
1642
+ retries_taken=retries_taken,
1643
+ )
1644
+
1645
+ async def _retry_request(
1646
+ self,
1647
+ options: FinalRequestOptions,
1648
+ cast_to: Type[ResponseT],
1649
+ *,
1650
+ retries_taken: int,
1651
+ response_headers: httpx.Headers | None,
1652
+ stream: bool,
1653
+ stream_cls: type[_AsyncStreamT] | None,
1654
+ ) -> ResponseT | _AsyncStreamT:
1655
+ remaining_retries = options.get_max_retries(self.max_retries) - retries_taken
1656
+ if remaining_retries == 1:
1657
+ log.debug("1 retry left")
1658
+ else:
1659
+ log.debug("%i retries left", remaining_retries)
1660
+
1661
+ timeout = self._calculate_retry_timeout(remaining_retries, options, response_headers)
1662
+ log.info("Retrying request to %s in %f seconds", options.url, timeout)
1663
+
1664
+ await anyio.sleep(timeout)
1665
+
1666
+ return await self._request(
1667
+ options=options,
1668
+ cast_to=cast_to,
1669
+ retries_taken=retries_taken + 1,
1670
+ stream=stream,
1671
+ stream_cls=stream_cls,
1672
+ )
1673
+
1674
+ async def _process_response(
1675
+ self,
1676
+ *,
1677
+ cast_to: Type[ResponseT],
1678
+ options: FinalRequestOptions,
1679
+ response: httpx.Response,
1680
+ stream: bool,
1681
+ stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
1682
+ retries_taken: int = 0,
1683
+ ) -> ResponseT:
1684
+ origin = get_origin(cast_to) or cast_to
1685
+
1686
+ if inspect.isclass(origin) and issubclass(origin, BaseAPIResponse):
1687
+ if not issubclass(origin, AsyncAPIResponse):
1688
+ raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}")
1689
+
1690
+ response_cls = cast("type[BaseAPIResponse[Any]]", cast_to)
1691
+ return cast(
1692
+ "ResponseT",
1693
+ response_cls(
1694
+ raw=response,
1695
+ client=self,
1696
+ cast_to=extract_response_type(response_cls),
1697
+ stream=stream,
1698
+ stream_cls=stream_cls,
1699
+ options=options,
1700
+ retries_taken=retries_taken,
1701
+ ),
1702
+ )
1703
+
1704
+ if cast_to == httpx.Response:
1705
+ return cast(ResponseT, response)
1706
+
1707
+ api_response = AsyncAPIResponse(
1708
+ raw=response,
1709
+ client=self,
1710
+ cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast]
1711
+ stream=stream,
1712
+ stream_cls=stream_cls,
1713
+ options=options,
1714
+ retries_taken=retries_taken,
1715
+ )
1716
+ if bool(response.request.headers.get(RAW_RESPONSE_HEADER)):
1717
+ return cast(ResponseT, api_response)
1718
+
1719
+ return await api_response.parse()
1720
+
1721
+ def _request_api_list(
1722
+ self,
1723
+ model: Type[_T],
1724
+ page: Type[AsyncPageT],
1725
+ options: FinalRequestOptions,
1726
+ ) -> AsyncPaginator[_T, AsyncPageT]:
1727
+ return AsyncPaginator(client=self, options=options, page_cls=page, model=model)
1728
+
1729
+ @overload
1730
+ async def get(
1731
+ self,
1732
+ path: str,
1733
+ *,
1734
+ cast_to: Type[ResponseT],
1735
+ options: RequestOptions = {},
1736
+ stream: Literal[False] = False,
1737
+ ) -> ResponseT: ...
1738
+
1739
+ @overload
1740
+ async def get(
1741
+ self,
1742
+ path: str,
1743
+ *,
1744
+ cast_to: Type[ResponseT],
1745
+ options: RequestOptions = {},
1746
+ stream: Literal[True],
1747
+ stream_cls: type[_AsyncStreamT],
1748
+ ) -> _AsyncStreamT: ...
1749
+
1750
+ @overload
1751
+ async def get(
1752
+ self,
1753
+ path: str,
1754
+ *,
1755
+ cast_to: Type[ResponseT],
1756
+ options: RequestOptions = {},
1757
+ stream: bool,
1758
+ stream_cls: type[_AsyncStreamT] | None = None,
1759
+ ) -> ResponseT | _AsyncStreamT: ...
1760
+
1761
+ async def get(
1762
+ self,
1763
+ path: str,
1764
+ *,
1765
+ cast_to: Type[ResponseT],
1766
+ options: RequestOptions = {},
1767
+ stream: bool = False,
1768
+ stream_cls: type[_AsyncStreamT] | None = None,
1769
+ ) -> ResponseT | _AsyncStreamT:
1770
+ opts = FinalRequestOptions.construct(method="get", url=path, **options)
1771
+ return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
1772
+
1773
+ @overload
1774
+ async def post(
1775
+ self,
1776
+ path: str,
1777
+ *,
1778
+ cast_to: Type[ResponseT],
1779
+ body: Body | None = None,
1780
+ files: RequestFiles | None = None,
1781
+ options: RequestOptions = {},
1782
+ stream: Literal[False] = False,
1783
+ ) -> ResponseT: ...
1784
+
1785
+ @overload
1786
+ async def post(
1787
+ self,
1788
+ path: str,
1789
+ *,
1790
+ cast_to: Type[ResponseT],
1791
+ body: Body | None = None,
1792
+ files: RequestFiles | None = None,
1793
+ options: RequestOptions = {},
1794
+ stream: Literal[True],
1795
+ stream_cls: type[_AsyncStreamT],
1796
+ ) -> _AsyncStreamT: ...
1797
+
1798
+ @overload
1799
+ async def post(
1800
+ self,
1801
+ path: str,
1802
+ *,
1803
+ cast_to: Type[ResponseT],
1804
+ body: Body | None = None,
1805
+ files: RequestFiles | None = None,
1806
+ options: RequestOptions = {},
1807
+ stream: bool,
1808
+ stream_cls: type[_AsyncStreamT] | None = None,
1809
+ ) -> ResponseT | _AsyncStreamT: ...
1810
+
1811
+ async def post(
1812
+ self,
1813
+ path: str,
1814
+ *,
1815
+ cast_to: Type[ResponseT],
1816
+ body: Body | None = None,
1817
+ files: RequestFiles | None = None,
1818
+ options: RequestOptions = {},
1819
+ stream: bool = False,
1820
+ stream_cls: type[_AsyncStreamT] | None = None,
1821
+ ) -> ResponseT | _AsyncStreamT:
1822
+ opts = FinalRequestOptions.construct(
1823
+ method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options
1824
+ )
1825
+ return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)
1826
+
1827
+ async def patch(
1828
+ self,
1829
+ path: str,
1830
+ *,
1831
+ cast_to: Type[ResponseT],
1832
+ body: Body | None = None,
1833
+ options: RequestOptions = {},
1834
+ ) -> ResponseT:
1835
+ opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options)
1836
+ return await self.request(cast_to, opts)
1837
+
1838
+ async def put(
1839
+ self,
1840
+ path: str,
1841
+ *,
1842
+ cast_to: Type[ResponseT],
1843
+ body: Body | None = None,
1844
+ files: RequestFiles | None = None,
1845
+ options: RequestOptions = {},
1846
+ ) -> ResponseT:
1847
+ opts = FinalRequestOptions.construct(
1848
+ method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options
1849
+ )
1850
+ return await self.request(cast_to, opts)
1851
+
1852
+ async def delete(
1853
+ self,
1854
+ path: str,
1855
+ *,
1856
+ cast_to: Type[ResponseT],
1857
+ body: Body | None = None,
1858
+ options: RequestOptions = {},
1859
+ ) -> ResponseT:
1860
+ opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options)
1861
+ return await self.request(cast_to, opts)
1862
+
1863
+ def get_api_list(
1864
+ self,
1865
+ path: str,
1866
+ *,
1867
+ model: Type[_T],
1868
+ page: Type[AsyncPageT],
1869
+ body: Body | None = None,
1870
+ options: RequestOptions = {},
1871
+ method: str = "get",
1872
+ ) -> AsyncPaginator[_T, AsyncPageT]:
1873
+ opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options)
1874
+ return self._request_api_list(model, page, opts)
1875
+
1876
+
1877
+ def make_request_options(
1878
+ *,
1879
+ query: Query | None = None,
1880
+ extra_headers: Headers | None = None,
1881
+ extra_query: Query | None = None,
1882
+ extra_body: Body | None = None,
1883
+ idempotency_key: str | None = None,
1884
+ timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
1885
+ post_parser: PostParser | NotGiven = NOT_GIVEN,
1886
+ ) -> RequestOptions:
1887
+ """Create a dict of type RequestOptions without keys of NotGiven values."""
1888
+ options: RequestOptions = {}
1889
+ if extra_headers is not None:
1890
+ options["headers"] = extra_headers
1891
+
1892
+ if extra_body is not None:
1893
+ options["extra_json"] = cast(AnyMapping, extra_body)
1894
+
1895
+ if query is not None:
1896
+ options["params"] = query
1897
+
1898
+ if extra_query is not None:
1899
+ options["params"] = {**options.get("params", {}), **extra_query}
1900
+
1901
+ if not isinstance(timeout, NotGiven):
1902
+ options["timeout"] = timeout
1903
+
1904
+ if idempotency_key is not None:
1905
+ options["idempotency_key"] = idempotency_key
1906
+
1907
+ if is_given(post_parser):
1908
+ # internal
1909
+ options["post_parser"] = post_parser # type: ignore
1910
+
1911
+ return options
1912
+
1913
+
1914
+ class ForceMultipartDict(Dict[str, None]):
1915
+ def __bool__(self) -> bool:
1916
+ return True
1917
+
1918
+
1919
+ class OtherPlatform:
1920
+ def __init__(self, name: str) -> None:
1921
+ self.name = name
1922
+
1923
+ @override
1924
+ def __str__(self) -> str:
1925
+ return f"Other:{self.name}"
1926
+
1927
+
1928
+ Platform = Union[
1929
+ OtherPlatform,
1930
+ Literal[
1931
+ "MacOS",
1932
+ "Linux",
1933
+ "Windows",
1934
+ "FreeBSD",
1935
+ "OpenBSD",
1936
+ "iOS",
1937
+ "Android",
1938
+ "Unknown",
1939
+ ],
1940
+ ]
1941
+
1942
+
1943
+ def get_platform() -> Platform:
1944
+ try:
1945
+ system = platform.system().lower()
1946
+ platform_name = platform.platform().lower()
1947
+ except Exception:
1948
+ return "Unknown"
1949
+
1950
+ if "iphone" in platform_name or "ipad" in platform_name:
1951
+ # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7
1952
+ # system is Darwin and platform_name is a string like:
1953
+ # - Darwin-21.6.0-iPhone12,1-64bit
1954
+ # - Darwin-21.6.0-iPad7,11-64bit
1955
+ return "iOS"
1956
+
1957
+ if system == "darwin":
1958
+ return "MacOS"
1959
+
1960
+ if system == "windows":
1961
+ return "Windows"
1962
+
1963
+ if "android" in platform_name:
1964
+ # Tested using Pydroid 3
1965
+ # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc'
1966
+ return "Android"
1967
+
1968
+ if system == "linux":
1969
+ # https://distro.readthedocs.io/en/latest/#distro.id
1970
+ distro_id = distro.id()
1971
+ if distro_id == "freebsd":
1972
+ return "FreeBSD"
1973
+
1974
+ if distro_id == "openbsd":
1975
+ return "OpenBSD"
1976
+
1977
+ return "Linux"
1978
+
1979
+ if platform_name:
1980
+ return OtherPlatform(platform_name)
1981
+
1982
+ return "Unknown"
1983
+
1984
+
1985
+ @lru_cache(maxsize=None)
1986
+ def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]:
1987
+ return {
1988
+ "X-Stainless-Lang": "python",
1989
+ "X-Stainless-Package-Version": version,
1990
+ "X-Stainless-OS": str(platform or get_platform()),
1991
+ "X-Stainless-Arch": str(get_architecture()),
1992
+ "X-Stainless-Runtime": get_python_runtime(),
1993
+ "X-Stainless-Runtime-Version": get_python_version(),
1994
+ }
1995
+
1996
+
1997
+ class OtherArch:
1998
+ def __init__(self, name: str) -> None:
1999
+ self.name = name
2000
+
2001
+ @override
2002
+ def __str__(self) -> str:
2003
+ return f"other:{self.name}"
2004
+
2005
+
2006
+ Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]]
2007
+
2008
+
2009
+ def get_python_runtime() -> str:
2010
+ try:
2011
+ return platform.python_implementation()
2012
+ except Exception:
2013
+ return "unknown"
2014
+
2015
+
2016
+ def get_python_version() -> str:
2017
+ try:
2018
+ return platform.python_version()
2019
+ except Exception:
2020
+ return "unknown"
2021
+
2022
+
2023
+ def get_architecture() -> Arch:
2024
+ try:
2025
+ machine = platform.machine().lower()
2026
+ except Exception:
2027
+ return "unknown"
2028
+
2029
+ if machine in ("arm64", "aarch64"):
2030
+ return "arm64"
2031
+
2032
+ # TODO: untested
2033
+ if machine == "arm":
2034
+ return "arm"
2035
+
2036
+ if machine == "x86_64":
2037
+ return "x64"
2038
+
2039
+ # TODO: untested
2040
+ if sys.maxsize <= 2**32:
2041
+ return "x32"
2042
+
2043
+ if machine:
2044
+ return OtherArch(machine)
2045
+
2046
+ return "unknown"
2047
+
2048
+
2049
+ def _merge_mappings(
2050
+ obj1: Mapping[_T_co, Union[_T, Omit]],
2051
+ obj2: Mapping[_T_co, Union[_T, Omit]],
2052
+ ) -> Dict[_T_co, _T]:
2053
+ """Merge two mappings of the same type, removing any values that are instances of `Omit`.
2054
+
2055
+ In cases with duplicate keys the second mapping takes precedence.
2056
+ """
2057
+ merged = {**obj1, **obj2}
2058
+ return {key: value for key, value in merged.items() if not isinstance(value, Omit)}