google-genai 1.54.0__py3-none-any.whl → 1.56.0__py3-none-any.whl

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