_base_client.py 67 KB

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