_client.py 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019
  1. from __future__ import annotations
  2. import datetime
  3. import enum
  4. import logging
  5. import time
  6. import typing
  7. import warnings
  8. from contextlib import asynccontextmanager, contextmanager
  9. from types import TracebackType
  10. from .__version__ import __version__
  11. from ._auth import Auth, BasicAuth, FunctionAuth
  12. from ._config import (
  13. DEFAULT_LIMITS,
  14. DEFAULT_MAX_REDIRECTS,
  15. DEFAULT_TIMEOUT_CONFIG,
  16. Limits,
  17. Proxy,
  18. Timeout,
  19. )
  20. from ._decoders import SUPPORTED_DECODERS
  21. from ._exceptions import (
  22. InvalidURL,
  23. RemoteProtocolError,
  24. TooManyRedirects,
  25. request_context,
  26. )
  27. from ._models import Cookies, Headers, Request, Response
  28. from ._status_codes import codes
  29. from ._transports.base import AsyncBaseTransport, BaseTransport
  30. from ._transports.default import AsyncHTTPTransport, HTTPTransport
  31. from ._types import (
  32. AsyncByteStream,
  33. AuthTypes,
  34. CertTypes,
  35. CookieTypes,
  36. HeaderTypes,
  37. ProxyTypes,
  38. QueryParamTypes,
  39. RequestContent,
  40. RequestData,
  41. RequestExtensions,
  42. RequestFiles,
  43. SyncByteStream,
  44. TimeoutTypes,
  45. )
  46. from ._urls import URL, QueryParams
  47. from ._utils import URLPattern, get_environment_proxies
  48. if typing.TYPE_CHECKING:
  49. import ssl # pragma: no cover
  50. __all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]
  51. # The type annotation for @classmethod and context managers here follows PEP 484
  52. # https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods
  53. T = typing.TypeVar("T", bound="Client")
  54. U = typing.TypeVar("U", bound="AsyncClient")
  55. def _is_https_redirect(url: URL, location: URL) -> bool:
  56. """
  57. Return 'True' if 'location' is a HTTPS upgrade of 'url'
  58. """
  59. if url.host != location.host:
  60. return False
  61. return (
  62. url.scheme == "http"
  63. and _port_or_default(url) == 80
  64. and location.scheme == "https"
  65. and _port_or_default(location) == 443
  66. )
  67. def _port_or_default(url: URL) -> int | None:
  68. if url.port is not None:
  69. return url.port
  70. return {"http": 80, "https": 443}.get(url.scheme)
  71. def _same_origin(url: URL, other: URL) -> bool:
  72. """
  73. Return 'True' if the given URLs share the same origin.
  74. """
  75. return (
  76. url.scheme == other.scheme
  77. and url.host == other.host
  78. and _port_or_default(url) == _port_or_default(other)
  79. )
  80. class UseClientDefault:
  81. """
  82. For some parameters such as `auth=...` and `timeout=...` we need to be able
  83. to indicate the default "unset" state, in a way that is distinctly different
  84. to using `None`.
  85. The default "unset" state indicates that whatever default is set on the
  86. client should be used. This is different to setting `None`, which
  87. explicitly disables the parameter, possibly overriding a client default.
  88. For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature.
  89. Omitting the `timeout` parameter will send a request using whatever default
  90. timeout has been configured on the client. Including `timeout=None` will
  91. ensure no timeout is used.
  92. Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant,
  93. but it is used internally when a parameter is not included.
  94. """
  95. USE_CLIENT_DEFAULT = UseClientDefault()
  96. logger = logging.getLogger("httpx")
  97. USER_AGENT = f"python-httpx/{__version__}"
  98. ACCEPT_ENCODING = ", ".join(
  99. [key for key in SUPPORTED_DECODERS.keys() if key != "identity"]
  100. )
  101. class ClientState(enum.Enum):
  102. # UNOPENED:
  103. # The client has been instantiated, but has not been used to send a request,
  104. # or been opened by entering the context of a `with` block.
  105. UNOPENED = 1
  106. # OPENED:
  107. # The client has either sent a request, or is within a `with` block.
  108. OPENED = 2
  109. # CLOSED:
  110. # The client has either exited the `with` block, or `close()` has
  111. # been called explicitly.
  112. CLOSED = 3
  113. class BoundSyncStream(SyncByteStream):
  114. """
  115. A byte stream that is bound to a given response instance, and that
  116. ensures the `response.elapsed` is set once the response is closed.
  117. """
  118. def __init__(
  119. self, stream: SyncByteStream, response: Response, start: float
  120. ) -> None:
  121. self._stream = stream
  122. self._response = response
  123. self._start = start
  124. def __iter__(self) -> typing.Iterator[bytes]:
  125. for chunk in self._stream:
  126. yield chunk
  127. def close(self) -> None:
  128. elapsed = time.perf_counter() - self._start
  129. self._response.elapsed = datetime.timedelta(seconds=elapsed)
  130. self._stream.close()
  131. class BoundAsyncStream(AsyncByteStream):
  132. """
  133. An async byte stream that is bound to a given response instance, and that
  134. ensures the `response.elapsed` is set once the response is closed.
  135. """
  136. def __init__(
  137. self, stream: AsyncByteStream, response: Response, start: float
  138. ) -> None:
  139. self._stream = stream
  140. self._response = response
  141. self._start = start
  142. async def __aiter__(self) -> typing.AsyncIterator[bytes]:
  143. async for chunk in self._stream:
  144. yield chunk
  145. async def aclose(self) -> None:
  146. elapsed = time.perf_counter() - self._start
  147. self._response.elapsed = datetime.timedelta(seconds=elapsed)
  148. await self._stream.aclose()
  149. EventHook = typing.Callable[..., typing.Any]
  150. class BaseClient:
  151. def __init__(
  152. self,
  153. *,
  154. auth: AuthTypes | None = None,
  155. params: QueryParamTypes | None = None,
  156. headers: HeaderTypes | None = None,
  157. cookies: CookieTypes | None = None,
  158. timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
  159. follow_redirects: bool = False,
  160. max_redirects: int = DEFAULT_MAX_REDIRECTS,
  161. event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
  162. base_url: URL | str = "",
  163. trust_env: bool = True,
  164. default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
  165. ) -> None:
  166. event_hooks = {} if event_hooks is None else event_hooks
  167. self._base_url = self._enforce_trailing_slash(URL(base_url))
  168. self._auth = self._build_auth(auth)
  169. self._params = QueryParams(params)
  170. self.headers = Headers(headers)
  171. self._cookies = Cookies(cookies)
  172. self._timeout = Timeout(timeout)
  173. self.follow_redirects = follow_redirects
  174. self.max_redirects = max_redirects
  175. self._event_hooks = {
  176. "request": list(event_hooks.get("request", [])),
  177. "response": list(event_hooks.get("response", [])),
  178. }
  179. self._trust_env = trust_env
  180. self._default_encoding = default_encoding
  181. self._state = ClientState.UNOPENED
  182. @property
  183. def is_closed(self) -> bool:
  184. """
  185. Check if the client being closed
  186. """
  187. return self._state == ClientState.CLOSED
  188. @property
  189. def trust_env(self) -> bool:
  190. return self._trust_env
  191. def _enforce_trailing_slash(self, url: URL) -> URL:
  192. if url.raw_path.endswith(b"/"):
  193. return url
  194. return url.copy_with(raw_path=url.raw_path + b"/")
  195. def _get_proxy_map(
  196. self, proxy: ProxyTypes | None, allow_env_proxies: bool
  197. ) -> dict[str, Proxy | None]:
  198. if proxy is None:
  199. if allow_env_proxies:
  200. return {
  201. key: None if url is None else Proxy(url=url)
  202. for key, url in get_environment_proxies().items()
  203. }
  204. return {}
  205. else:
  206. proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
  207. return {"all://": proxy}
  208. @property
  209. def timeout(self) -> Timeout:
  210. return self._timeout
  211. @timeout.setter
  212. def timeout(self, timeout: TimeoutTypes) -> None:
  213. self._timeout = Timeout(timeout)
  214. @property
  215. def event_hooks(self) -> dict[str, list[EventHook]]:
  216. return self._event_hooks
  217. @event_hooks.setter
  218. def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None:
  219. self._event_hooks = {
  220. "request": list(event_hooks.get("request", [])),
  221. "response": list(event_hooks.get("response", [])),
  222. }
  223. @property
  224. def auth(self) -> Auth | None:
  225. """
  226. Authentication class used when none is passed at the request-level.
  227. See also [Authentication][0].
  228. [0]: /quickstart/#authentication
  229. """
  230. return self._auth
  231. @auth.setter
  232. def auth(self, auth: AuthTypes) -> None:
  233. self._auth = self._build_auth(auth)
  234. @property
  235. def base_url(self) -> URL:
  236. """
  237. Base URL to use when sending requests with relative URLs.
  238. """
  239. return self._base_url
  240. @base_url.setter
  241. def base_url(self, url: URL | str) -> None:
  242. self._base_url = self._enforce_trailing_slash(URL(url))
  243. @property
  244. def headers(self) -> Headers:
  245. """
  246. HTTP headers to include when sending requests.
  247. """
  248. return self._headers
  249. @headers.setter
  250. def headers(self, headers: HeaderTypes) -> None:
  251. client_headers = Headers(
  252. {
  253. b"Accept": b"*/*",
  254. b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"),
  255. b"Connection": b"keep-alive",
  256. b"User-Agent": USER_AGENT.encode("ascii"),
  257. }
  258. )
  259. client_headers.update(headers)
  260. self._headers = client_headers
  261. @property
  262. def cookies(self) -> Cookies:
  263. """
  264. Cookie values to include when sending requests.
  265. """
  266. return self._cookies
  267. @cookies.setter
  268. def cookies(self, cookies: CookieTypes) -> None:
  269. self._cookies = Cookies(cookies)
  270. @property
  271. def params(self) -> QueryParams:
  272. """
  273. Query parameters to include in the URL when sending requests.
  274. """
  275. return self._params
  276. @params.setter
  277. def params(self, params: QueryParamTypes) -> None:
  278. self._params = QueryParams(params)
  279. def build_request(
  280. self,
  281. method: str,
  282. url: URL | str,
  283. *,
  284. content: RequestContent | None = None,
  285. data: RequestData | None = None,
  286. files: RequestFiles | None = None,
  287. json: typing.Any | None = None,
  288. params: QueryParamTypes | None = None,
  289. headers: HeaderTypes | None = None,
  290. cookies: CookieTypes | None = None,
  291. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  292. extensions: RequestExtensions | None = None,
  293. ) -> Request:
  294. """
  295. Build and return a request instance.
  296. * The `params`, `headers` and `cookies` arguments
  297. are merged with any values set on the client.
  298. * The `url` argument is merged with any `base_url` set on the client.
  299. See also: [Request instances][0]
  300. [0]: /advanced/clients/#request-instances
  301. """
  302. url = self._merge_url(url)
  303. headers = self._merge_headers(headers)
  304. cookies = self._merge_cookies(cookies)
  305. params = self._merge_queryparams(params)
  306. extensions = {} if extensions is None else extensions
  307. if "timeout" not in extensions:
  308. timeout = (
  309. self.timeout
  310. if isinstance(timeout, UseClientDefault)
  311. else Timeout(timeout)
  312. )
  313. extensions = dict(**extensions, timeout=timeout.as_dict())
  314. return Request(
  315. method,
  316. url,
  317. content=content,
  318. data=data,
  319. files=files,
  320. json=json,
  321. params=params,
  322. headers=headers,
  323. cookies=cookies,
  324. extensions=extensions,
  325. )
  326. def _merge_url(self, url: URL | str) -> URL:
  327. """
  328. Merge a URL argument together with any 'base_url' on the client,
  329. to create the URL used for the outgoing request.
  330. """
  331. merge_url = URL(url)
  332. if merge_url.is_relative_url:
  333. # To merge URLs we always append to the base URL. To get this
  334. # behaviour correct we always ensure the base URL ends in a '/'
  335. # separator, and strip any leading '/' from the merge URL.
  336. #
  337. # So, eg...
  338. #
  339. # >>> client = Client(base_url="https://www.example.com/subpath")
  340. # >>> client.base_url
  341. # URL('https://www.example.com/subpath/')
  342. # >>> client.build_request("GET", "/path").url
  343. # URL('https://www.example.com/subpath/path')
  344. merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/")
  345. return self.base_url.copy_with(raw_path=merge_raw_path)
  346. return merge_url
  347. def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | None:
  348. """
  349. Merge a cookies argument together with any cookies on the client,
  350. to create the cookies used for the outgoing request.
  351. """
  352. if cookies or self.cookies:
  353. merged_cookies = Cookies(self.cookies)
  354. merged_cookies.update(cookies)
  355. return merged_cookies
  356. return cookies
  357. def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None:
  358. """
  359. Merge a headers argument together with any headers on the client,
  360. to create the headers used for the outgoing request.
  361. """
  362. merged_headers = Headers(self.headers)
  363. merged_headers.update(headers)
  364. return merged_headers
  365. def _merge_queryparams(
  366. self, params: QueryParamTypes | None = None
  367. ) -> QueryParamTypes | None:
  368. """
  369. Merge a queryparams argument together with any queryparams on the client,
  370. to create the queryparams used for the outgoing request.
  371. """
  372. if params or self.params:
  373. merged_queryparams = QueryParams(self.params)
  374. return merged_queryparams.merge(params)
  375. return params
  376. def _build_auth(self, auth: AuthTypes | None) -> Auth | None:
  377. if auth is None:
  378. return None
  379. elif isinstance(auth, tuple):
  380. return BasicAuth(username=auth[0], password=auth[1])
  381. elif isinstance(auth, Auth):
  382. return auth
  383. elif callable(auth):
  384. return FunctionAuth(func=auth)
  385. else:
  386. raise TypeError(f'Invalid "auth" argument: {auth!r}')
  387. def _build_request_auth(
  388. self,
  389. request: Request,
  390. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  391. ) -> Auth:
  392. auth = (
  393. self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth)
  394. )
  395. if auth is not None:
  396. return auth
  397. username, password = request.url.username, request.url.password
  398. if username or password:
  399. return BasicAuth(username=username, password=password)
  400. return Auth()
  401. def _build_redirect_request(self, request: Request, response: Response) -> Request:
  402. """
  403. Given a request and a redirect response, return a new request that
  404. should be used to effect the redirect.
  405. """
  406. method = self._redirect_method(request, response)
  407. url = self._redirect_url(request, response)
  408. headers = self._redirect_headers(request, url, method)
  409. stream = self._redirect_stream(request, method)
  410. cookies = Cookies(self.cookies)
  411. return Request(
  412. method=method,
  413. url=url,
  414. headers=headers,
  415. cookies=cookies,
  416. stream=stream,
  417. extensions=request.extensions,
  418. )
  419. def _redirect_method(self, request: Request, response: Response) -> str:
  420. """
  421. When being redirected we may want to change the method of the request
  422. based on certain specs or browser behavior.
  423. """
  424. method = request.method
  425. # https://tools.ietf.org/html/rfc7231#section-6.4.4
  426. if response.status_code == codes.SEE_OTHER and method != "HEAD":
  427. method = "GET"
  428. # Do what the browsers do, despite standards...
  429. # Turn 302s into GETs.
  430. if response.status_code == codes.FOUND and method != "HEAD":
  431. method = "GET"
  432. # If a POST is responded to with a 301, turn it into a GET.
  433. # This bizarre behaviour is explained in 'requests' issue 1704.
  434. if response.status_code == codes.MOVED_PERMANENTLY and method == "POST":
  435. method = "GET"
  436. return method
  437. def _redirect_url(self, request: Request, response: Response) -> URL:
  438. """
  439. Return the URL for the redirect to follow.
  440. """
  441. location = response.headers["Location"]
  442. try:
  443. url = URL(location)
  444. except InvalidURL as exc:
  445. raise RemoteProtocolError(
  446. f"Invalid URL in location header: {exc}.", request=request
  447. ) from None
  448. # Handle malformed 'Location' headers that are "absolute" form, have no host.
  449. # See: https://github.com/encode/httpx/issues/771
  450. if url.scheme and not url.host:
  451. url = url.copy_with(host=request.url.host)
  452. # Facilitate relative 'Location' headers, as allowed by RFC 7231.
  453. # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
  454. if url.is_relative_url:
  455. url = request.url.join(url)
  456. # Attach previous fragment if needed (RFC 7231 7.1.2)
  457. if request.url.fragment and not url.fragment:
  458. url = url.copy_with(fragment=request.url.fragment)
  459. return url
  460. def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers:
  461. """
  462. Return the headers that should be used for the redirect request.
  463. """
  464. headers = Headers(request.headers)
  465. if not _same_origin(url, request.url):
  466. if not _is_https_redirect(request.url, url):
  467. # Strip Authorization headers when responses are redirected
  468. # away from the origin. (Except for direct HTTP to HTTPS redirects.)
  469. headers.pop("Authorization", None)
  470. # Update the Host header.
  471. headers["Host"] = url.netloc.decode("ascii")
  472. if method != request.method and method == "GET":
  473. # If we've switch to a 'GET' request, then strip any headers which
  474. # are only relevant to the request body.
  475. headers.pop("Content-Length", None)
  476. headers.pop("Transfer-Encoding", None)
  477. # We should use the client cookie store to determine any cookie header,
  478. # rather than whatever was on the original outgoing request.
  479. headers.pop("Cookie", None)
  480. return headers
  481. def _redirect_stream(
  482. self, request: Request, method: str
  483. ) -> SyncByteStream | AsyncByteStream | None:
  484. """
  485. Return the body that should be used for the redirect request.
  486. """
  487. if method != request.method and method == "GET":
  488. return None
  489. return request.stream
  490. def _set_timeout(self, request: Request) -> None:
  491. if "timeout" not in request.extensions:
  492. timeout = (
  493. self.timeout
  494. if isinstance(self.timeout, UseClientDefault)
  495. else Timeout(self.timeout)
  496. )
  497. request.extensions = dict(**request.extensions, timeout=timeout.as_dict())
  498. class Client(BaseClient):
  499. """
  500. An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.
  501. It can be shared between threads.
  502. Usage:
  503. ```python
  504. >>> client = httpx.Client()
  505. >>> response = client.get('https://example.org')
  506. ```
  507. **Parameters:**
  508. * **auth** - *(optional)* An authentication class to use when sending
  509. requests.
  510. * **params** - *(optional)* Query parameters to include in request URLs, as
  511. a string, dictionary, or sequence of two-tuples.
  512. * **headers** - *(optional)* Dictionary of HTTP headers to include when
  513. sending requests.
  514. * **cookies** - *(optional)* Dictionary of Cookie items to include when
  515. sending requests.
  516. * **verify** - *(optional)* Either `True` to use an SSL context with the
  517. default CA bundle, `False` to disable verification, or an instance of
  518. `ssl.SSLContext` to use a custom context.
  519. * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
  520. enabled. Defaults to `False`.
  521. * **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
  522. * **timeout** - *(optional)* The timeout configuration to use when sending
  523. requests.
  524. * **limits** - *(optional)* The limits configuration to use.
  525. * **max_redirects** - *(optional)* The maximum number of redirect responses
  526. that should be followed.
  527. * **base_url** - *(optional)* A URL to use as the base when building
  528. request URLs.
  529. * **transport** - *(optional)* A transport class to use for sending requests
  530. over the network.
  531. * **trust_env** - *(optional)* Enables or disables usage of environment
  532. variables for configuration.
  533. * **default_encoding** - *(optional)* The default encoding to use for decoding
  534. response text, if no charset information is included in a response Content-Type
  535. header. Set to a callable for automatic character set detection. Default: "utf-8".
  536. """
  537. def __init__(
  538. self,
  539. *,
  540. auth: AuthTypes | None = None,
  541. params: QueryParamTypes | None = None,
  542. headers: HeaderTypes | None = None,
  543. cookies: CookieTypes | None = None,
  544. verify: ssl.SSLContext | str | bool = True,
  545. cert: CertTypes | None = None,
  546. trust_env: bool = True,
  547. http1: bool = True,
  548. http2: bool = False,
  549. proxy: ProxyTypes | None = None,
  550. mounts: None | (typing.Mapping[str, BaseTransport | None]) = None,
  551. timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
  552. follow_redirects: bool = False,
  553. limits: Limits = DEFAULT_LIMITS,
  554. max_redirects: int = DEFAULT_MAX_REDIRECTS,
  555. event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
  556. base_url: URL | str = "",
  557. transport: BaseTransport | None = None,
  558. default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
  559. ) -> None:
  560. super().__init__(
  561. auth=auth,
  562. params=params,
  563. headers=headers,
  564. cookies=cookies,
  565. timeout=timeout,
  566. follow_redirects=follow_redirects,
  567. max_redirects=max_redirects,
  568. event_hooks=event_hooks,
  569. base_url=base_url,
  570. trust_env=trust_env,
  571. default_encoding=default_encoding,
  572. )
  573. if http2:
  574. try:
  575. import h2 # noqa
  576. except ImportError: # pragma: no cover
  577. raise ImportError(
  578. "Using http2=True, but the 'h2' package is not installed. "
  579. "Make sure to install httpx using `pip install httpx[http2]`."
  580. ) from None
  581. allow_env_proxies = trust_env and transport is None
  582. proxy_map = self._get_proxy_map(proxy, allow_env_proxies)
  583. self._transport = self._init_transport(
  584. verify=verify,
  585. cert=cert,
  586. trust_env=trust_env,
  587. http1=http1,
  588. http2=http2,
  589. limits=limits,
  590. transport=transport,
  591. )
  592. self._mounts: dict[URLPattern, BaseTransport | None] = {
  593. URLPattern(key): None
  594. if proxy is None
  595. else self._init_proxy_transport(
  596. proxy,
  597. verify=verify,
  598. cert=cert,
  599. trust_env=trust_env,
  600. http1=http1,
  601. http2=http2,
  602. limits=limits,
  603. )
  604. for key, proxy in proxy_map.items()
  605. }
  606. if mounts is not None:
  607. self._mounts.update(
  608. {URLPattern(key): transport for key, transport in mounts.items()}
  609. )
  610. self._mounts = dict(sorted(self._mounts.items()))
  611. def _init_transport(
  612. self,
  613. verify: ssl.SSLContext | str | bool = True,
  614. cert: CertTypes | None = None,
  615. trust_env: bool = True,
  616. http1: bool = True,
  617. http2: bool = False,
  618. limits: Limits = DEFAULT_LIMITS,
  619. transport: BaseTransport | None = None,
  620. ) -> BaseTransport:
  621. if transport is not None:
  622. return transport
  623. return HTTPTransport(
  624. verify=verify,
  625. cert=cert,
  626. trust_env=trust_env,
  627. http1=http1,
  628. http2=http2,
  629. limits=limits,
  630. )
  631. def _init_proxy_transport(
  632. self,
  633. proxy: Proxy,
  634. verify: ssl.SSLContext | str | bool = True,
  635. cert: CertTypes | None = None,
  636. trust_env: bool = True,
  637. http1: bool = True,
  638. http2: bool = False,
  639. limits: Limits = DEFAULT_LIMITS,
  640. ) -> BaseTransport:
  641. return HTTPTransport(
  642. verify=verify,
  643. cert=cert,
  644. trust_env=trust_env,
  645. http1=http1,
  646. http2=http2,
  647. limits=limits,
  648. proxy=proxy,
  649. )
  650. def _transport_for_url(self, url: URL) -> BaseTransport:
  651. """
  652. Returns the transport instance that should be used for a given URL.
  653. This will either be the standard connection pool, or a proxy.
  654. """
  655. for pattern, transport in self._mounts.items():
  656. if pattern.matches(url):
  657. return self._transport if transport is None else transport
  658. return self._transport
  659. def request(
  660. self,
  661. method: str,
  662. url: URL | str,
  663. *,
  664. content: RequestContent | None = None,
  665. data: RequestData | None = None,
  666. files: RequestFiles | None = None,
  667. json: typing.Any | None = None,
  668. params: QueryParamTypes | None = None,
  669. headers: HeaderTypes | None = None,
  670. cookies: CookieTypes | None = None,
  671. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  672. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  673. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  674. extensions: RequestExtensions | None = None,
  675. ) -> Response:
  676. """
  677. Build and send a request.
  678. Equivalent to:
  679. ```python
  680. request = client.build_request(...)
  681. response = client.send(request, ...)
  682. ```
  683. See `Client.build_request()`, `Client.send()` and
  684. [Merging of configuration][0] for how the various parameters
  685. are merged with client-level configuration.
  686. [0]: /advanced/clients/#merging-of-configuration
  687. """
  688. if cookies is not None:
  689. message = (
  690. "Setting per-request cookies=<...> is being deprecated, because "
  691. "the expected behaviour on cookie persistence is ambiguous. Set "
  692. "cookies directly on the client instance instead."
  693. )
  694. warnings.warn(message, DeprecationWarning, stacklevel=2)
  695. request = self.build_request(
  696. method=method,
  697. url=url,
  698. content=content,
  699. data=data,
  700. files=files,
  701. json=json,
  702. params=params,
  703. headers=headers,
  704. cookies=cookies,
  705. timeout=timeout,
  706. extensions=extensions,
  707. )
  708. return self.send(request, auth=auth, follow_redirects=follow_redirects)
  709. @contextmanager
  710. def stream(
  711. self,
  712. method: str,
  713. url: URL | str,
  714. *,
  715. content: RequestContent | None = None,
  716. data: RequestData | None = None,
  717. files: RequestFiles | None = None,
  718. json: typing.Any | None = None,
  719. params: QueryParamTypes | None = None,
  720. headers: HeaderTypes | None = None,
  721. cookies: CookieTypes | None = None,
  722. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  723. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  724. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  725. extensions: RequestExtensions | None = None,
  726. ) -> typing.Iterator[Response]:
  727. """
  728. Alternative to `httpx.request()` that streams the response body
  729. instead of loading it into memory at once.
  730. **Parameters**: See `httpx.request`.
  731. See also: [Streaming Responses][0]
  732. [0]: /quickstart#streaming-responses
  733. """
  734. request = self.build_request(
  735. method=method,
  736. url=url,
  737. content=content,
  738. data=data,
  739. files=files,
  740. json=json,
  741. params=params,
  742. headers=headers,
  743. cookies=cookies,
  744. timeout=timeout,
  745. extensions=extensions,
  746. )
  747. response = self.send(
  748. request=request,
  749. auth=auth,
  750. follow_redirects=follow_redirects,
  751. stream=True,
  752. )
  753. try:
  754. yield response
  755. finally:
  756. response.close()
  757. def send(
  758. self,
  759. request: Request,
  760. *,
  761. stream: bool = False,
  762. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  763. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  764. ) -> Response:
  765. """
  766. Send a request.
  767. The request is sent as-is, unmodified.
  768. Typically you'll want to build one with `Client.build_request()`
  769. so that any client-level configuration is merged into the request,
  770. but passing an explicit `httpx.Request()` is supported as well.
  771. See also: [Request instances][0]
  772. [0]: /advanced/clients/#request-instances
  773. """
  774. if self._state == ClientState.CLOSED:
  775. raise RuntimeError("Cannot send a request, as the client has been closed.")
  776. self._state = ClientState.OPENED
  777. follow_redirects = (
  778. self.follow_redirects
  779. if isinstance(follow_redirects, UseClientDefault)
  780. else follow_redirects
  781. )
  782. self._set_timeout(request)
  783. auth = self._build_request_auth(request, auth)
  784. response = self._send_handling_auth(
  785. request,
  786. auth=auth,
  787. follow_redirects=follow_redirects,
  788. history=[],
  789. )
  790. try:
  791. if not stream:
  792. response.read()
  793. return response
  794. except BaseException as exc:
  795. response.close()
  796. raise exc
  797. def _send_handling_auth(
  798. self,
  799. request: Request,
  800. auth: Auth,
  801. follow_redirects: bool,
  802. history: list[Response],
  803. ) -> Response:
  804. auth_flow = auth.sync_auth_flow(request)
  805. try:
  806. request = next(auth_flow)
  807. while True:
  808. response = self._send_handling_redirects(
  809. request,
  810. follow_redirects=follow_redirects,
  811. history=history,
  812. )
  813. try:
  814. try:
  815. next_request = auth_flow.send(response)
  816. except StopIteration:
  817. return response
  818. response.history = list(history)
  819. response.read()
  820. request = next_request
  821. history.append(response)
  822. except BaseException as exc:
  823. response.close()
  824. raise exc
  825. finally:
  826. auth_flow.close()
  827. def _send_handling_redirects(
  828. self,
  829. request: Request,
  830. follow_redirects: bool,
  831. history: list[Response],
  832. ) -> Response:
  833. while True:
  834. if len(history) > self.max_redirects:
  835. raise TooManyRedirects(
  836. "Exceeded maximum allowed redirects.", request=request
  837. )
  838. for hook in self._event_hooks["request"]:
  839. hook(request)
  840. response = self._send_single_request(request)
  841. try:
  842. for hook in self._event_hooks["response"]:
  843. hook(response)
  844. response.history = list(history)
  845. if not response.has_redirect_location:
  846. return response
  847. request = self._build_redirect_request(request, response)
  848. history = history + [response]
  849. if follow_redirects:
  850. response.read()
  851. else:
  852. response.next_request = request
  853. return response
  854. except BaseException as exc:
  855. response.close()
  856. raise exc
  857. def _send_single_request(self, request: Request) -> Response:
  858. """
  859. Sends a single request, without handling any redirections.
  860. """
  861. transport = self._transport_for_url(request.url)
  862. start = time.perf_counter()
  863. if not isinstance(request.stream, SyncByteStream):
  864. raise RuntimeError(
  865. "Attempted to send an async request with a sync Client instance."
  866. )
  867. with request_context(request=request):
  868. response = transport.handle_request(request)
  869. assert isinstance(response.stream, SyncByteStream)
  870. response.request = request
  871. response.stream = BoundSyncStream(
  872. response.stream, response=response, start=start
  873. )
  874. self.cookies.extract_cookies(response)
  875. response.default_encoding = self._default_encoding
  876. logger.info(
  877. 'HTTP Request: %s %s "%s %d %s"',
  878. request.method,
  879. request.url,
  880. response.http_version,
  881. response.status_code,
  882. response.reason_phrase,
  883. )
  884. return response
  885. def get(
  886. self,
  887. url: URL | str,
  888. *,
  889. params: QueryParamTypes | None = None,
  890. headers: HeaderTypes | None = None,
  891. cookies: CookieTypes | None = None,
  892. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  893. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  894. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  895. extensions: RequestExtensions | None = None,
  896. ) -> Response:
  897. """
  898. Send a `GET` request.
  899. **Parameters**: See `httpx.request`.
  900. """
  901. return self.request(
  902. "GET",
  903. url,
  904. params=params,
  905. headers=headers,
  906. cookies=cookies,
  907. auth=auth,
  908. follow_redirects=follow_redirects,
  909. timeout=timeout,
  910. extensions=extensions,
  911. )
  912. def options(
  913. self,
  914. url: URL | str,
  915. *,
  916. params: QueryParamTypes | None = None,
  917. headers: HeaderTypes | None = None,
  918. cookies: CookieTypes | None = None,
  919. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  920. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  921. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  922. extensions: RequestExtensions | None = None,
  923. ) -> Response:
  924. """
  925. Send an `OPTIONS` request.
  926. **Parameters**: See `httpx.request`.
  927. """
  928. return self.request(
  929. "OPTIONS",
  930. url,
  931. params=params,
  932. headers=headers,
  933. cookies=cookies,
  934. auth=auth,
  935. follow_redirects=follow_redirects,
  936. timeout=timeout,
  937. extensions=extensions,
  938. )
  939. def head(
  940. self,
  941. url: URL | str,
  942. *,
  943. params: QueryParamTypes | None = None,
  944. headers: HeaderTypes | None = None,
  945. cookies: CookieTypes | None = None,
  946. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  947. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  948. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  949. extensions: RequestExtensions | None = None,
  950. ) -> Response:
  951. """
  952. Send a `HEAD` request.
  953. **Parameters**: See `httpx.request`.
  954. """
  955. return self.request(
  956. "HEAD",
  957. url,
  958. params=params,
  959. headers=headers,
  960. cookies=cookies,
  961. auth=auth,
  962. follow_redirects=follow_redirects,
  963. timeout=timeout,
  964. extensions=extensions,
  965. )
  966. def post(
  967. self,
  968. url: URL | str,
  969. *,
  970. content: RequestContent | None = None,
  971. data: RequestData | None = None,
  972. files: RequestFiles | None = None,
  973. json: typing.Any | None = None,
  974. params: QueryParamTypes | None = None,
  975. headers: HeaderTypes | None = None,
  976. cookies: CookieTypes | None = None,
  977. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  978. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  979. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  980. extensions: RequestExtensions | None = None,
  981. ) -> Response:
  982. """
  983. Send a `POST` request.
  984. **Parameters**: See `httpx.request`.
  985. """
  986. return self.request(
  987. "POST",
  988. url,
  989. content=content,
  990. data=data,
  991. files=files,
  992. json=json,
  993. params=params,
  994. headers=headers,
  995. cookies=cookies,
  996. auth=auth,
  997. follow_redirects=follow_redirects,
  998. timeout=timeout,
  999. extensions=extensions,
  1000. )
  1001. def put(
  1002. self,
  1003. url: URL | str,
  1004. *,
  1005. content: RequestContent | None = None,
  1006. data: RequestData | None = None,
  1007. files: RequestFiles | None = None,
  1008. json: typing.Any | None = None,
  1009. params: QueryParamTypes | None = None,
  1010. headers: HeaderTypes | None = None,
  1011. cookies: CookieTypes | None = None,
  1012. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1013. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1014. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1015. extensions: RequestExtensions | None = None,
  1016. ) -> Response:
  1017. """
  1018. Send a `PUT` request.
  1019. **Parameters**: See `httpx.request`.
  1020. """
  1021. return self.request(
  1022. "PUT",
  1023. url,
  1024. content=content,
  1025. data=data,
  1026. files=files,
  1027. json=json,
  1028. params=params,
  1029. headers=headers,
  1030. cookies=cookies,
  1031. auth=auth,
  1032. follow_redirects=follow_redirects,
  1033. timeout=timeout,
  1034. extensions=extensions,
  1035. )
  1036. def patch(
  1037. self,
  1038. url: URL | str,
  1039. *,
  1040. content: RequestContent | None = None,
  1041. data: RequestData | None = None,
  1042. files: RequestFiles | None = None,
  1043. json: typing.Any | None = None,
  1044. params: QueryParamTypes | None = None,
  1045. headers: HeaderTypes | None = None,
  1046. cookies: CookieTypes | None = None,
  1047. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1048. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1049. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1050. extensions: RequestExtensions | None = None,
  1051. ) -> Response:
  1052. """
  1053. Send a `PATCH` request.
  1054. **Parameters**: See `httpx.request`.
  1055. """
  1056. return self.request(
  1057. "PATCH",
  1058. url,
  1059. content=content,
  1060. data=data,
  1061. files=files,
  1062. json=json,
  1063. params=params,
  1064. headers=headers,
  1065. cookies=cookies,
  1066. auth=auth,
  1067. follow_redirects=follow_redirects,
  1068. timeout=timeout,
  1069. extensions=extensions,
  1070. )
  1071. def delete(
  1072. self,
  1073. url: URL | str,
  1074. *,
  1075. params: QueryParamTypes | None = None,
  1076. headers: HeaderTypes | None = None,
  1077. cookies: CookieTypes | None = None,
  1078. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1079. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1080. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1081. extensions: RequestExtensions | None = None,
  1082. ) -> Response:
  1083. """
  1084. Send a `DELETE` request.
  1085. **Parameters**: See `httpx.request`.
  1086. """
  1087. return self.request(
  1088. "DELETE",
  1089. url,
  1090. params=params,
  1091. headers=headers,
  1092. cookies=cookies,
  1093. auth=auth,
  1094. follow_redirects=follow_redirects,
  1095. timeout=timeout,
  1096. extensions=extensions,
  1097. )
  1098. def close(self) -> None:
  1099. """
  1100. Close transport and proxies.
  1101. """
  1102. if self._state != ClientState.CLOSED:
  1103. self._state = ClientState.CLOSED
  1104. self._transport.close()
  1105. for transport in self._mounts.values():
  1106. if transport is not None:
  1107. transport.close()
  1108. def __enter__(self: T) -> T:
  1109. if self._state != ClientState.UNOPENED:
  1110. msg = {
  1111. ClientState.OPENED: "Cannot open a client instance more than once.",
  1112. ClientState.CLOSED: (
  1113. "Cannot reopen a client instance, once it has been closed."
  1114. ),
  1115. }[self._state]
  1116. raise RuntimeError(msg)
  1117. self._state = ClientState.OPENED
  1118. self._transport.__enter__()
  1119. for transport in self._mounts.values():
  1120. if transport is not None:
  1121. transport.__enter__()
  1122. return self
  1123. def __exit__(
  1124. self,
  1125. exc_type: type[BaseException] | None = None,
  1126. exc_value: BaseException | None = None,
  1127. traceback: TracebackType | None = None,
  1128. ) -> None:
  1129. self._state = ClientState.CLOSED
  1130. self._transport.__exit__(exc_type, exc_value, traceback)
  1131. for transport in self._mounts.values():
  1132. if transport is not None:
  1133. transport.__exit__(exc_type, exc_value, traceback)
  1134. class AsyncClient(BaseClient):
  1135. """
  1136. An asynchronous HTTP client, with connection pooling, HTTP/2, redirects,
  1137. cookie persistence, etc.
  1138. It can be shared between tasks.
  1139. Usage:
  1140. ```python
  1141. >>> async with httpx.AsyncClient() as client:
  1142. >>> response = await client.get('https://example.org')
  1143. ```
  1144. **Parameters:**
  1145. * **auth** - *(optional)* An authentication class to use when sending
  1146. requests.
  1147. * **params** - *(optional)* Query parameters to include in request URLs, as
  1148. a string, dictionary, or sequence of two-tuples.
  1149. * **headers** - *(optional)* Dictionary of HTTP headers to include when
  1150. sending requests.
  1151. * **cookies** - *(optional)* Dictionary of Cookie items to include when
  1152. sending requests.
  1153. * **verify** - *(optional)* Either `True` to use an SSL context with the
  1154. default CA bundle, `False` to disable verification, or an instance of
  1155. `ssl.SSLContext` to use a custom context.
  1156. * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
  1157. enabled. Defaults to `False`.
  1158. * **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
  1159. * **timeout** - *(optional)* The timeout configuration to use when sending
  1160. requests.
  1161. * **limits** - *(optional)* The limits configuration to use.
  1162. * **max_redirects** - *(optional)* The maximum number of redirect responses
  1163. that should be followed.
  1164. * **base_url** - *(optional)* A URL to use as the base when building
  1165. request URLs.
  1166. * **transport** - *(optional)* A transport class to use for sending requests
  1167. over the network.
  1168. * **trust_env** - *(optional)* Enables or disables usage of environment
  1169. variables for configuration.
  1170. * **default_encoding** - *(optional)* The default encoding to use for decoding
  1171. response text, if no charset information is included in a response Content-Type
  1172. header. Set to a callable for automatic character set detection. Default: "utf-8".
  1173. """
  1174. def __init__(
  1175. self,
  1176. *,
  1177. auth: AuthTypes | None = None,
  1178. params: QueryParamTypes | None = None,
  1179. headers: HeaderTypes | None = None,
  1180. cookies: CookieTypes | None = None,
  1181. verify: ssl.SSLContext | str | bool = True,
  1182. cert: CertTypes | None = None,
  1183. http1: bool = True,
  1184. http2: bool = False,
  1185. proxy: ProxyTypes | None = None,
  1186. mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None,
  1187. timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
  1188. follow_redirects: bool = False,
  1189. limits: Limits = DEFAULT_LIMITS,
  1190. max_redirects: int = DEFAULT_MAX_REDIRECTS,
  1191. event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
  1192. base_url: URL | str = "",
  1193. transport: AsyncBaseTransport | None = None,
  1194. trust_env: bool = True,
  1195. default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
  1196. ) -> None:
  1197. super().__init__(
  1198. auth=auth,
  1199. params=params,
  1200. headers=headers,
  1201. cookies=cookies,
  1202. timeout=timeout,
  1203. follow_redirects=follow_redirects,
  1204. max_redirects=max_redirects,
  1205. event_hooks=event_hooks,
  1206. base_url=base_url,
  1207. trust_env=trust_env,
  1208. default_encoding=default_encoding,
  1209. )
  1210. if http2:
  1211. try:
  1212. import h2 # noqa
  1213. except ImportError: # pragma: no cover
  1214. raise ImportError(
  1215. "Using http2=True, but the 'h2' package is not installed. "
  1216. "Make sure to install httpx using `pip install httpx[http2]`."
  1217. ) from None
  1218. allow_env_proxies = trust_env and transport is None
  1219. proxy_map = self._get_proxy_map(proxy, allow_env_proxies)
  1220. self._transport = self._init_transport(
  1221. verify=verify,
  1222. cert=cert,
  1223. trust_env=trust_env,
  1224. http1=http1,
  1225. http2=http2,
  1226. limits=limits,
  1227. transport=transport,
  1228. )
  1229. self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
  1230. URLPattern(key): None
  1231. if proxy is None
  1232. else self._init_proxy_transport(
  1233. proxy,
  1234. verify=verify,
  1235. cert=cert,
  1236. trust_env=trust_env,
  1237. http1=http1,
  1238. http2=http2,
  1239. limits=limits,
  1240. )
  1241. for key, proxy in proxy_map.items()
  1242. }
  1243. if mounts is not None:
  1244. self._mounts.update(
  1245. {URLPattern(key): transport for key, transport in mounts.items()}
  1246. )
  1247. self._mounts = dict(sorted(self._mounts.items()))
  1248. def _init_transport(
  1249. self,
  1250. verify: ssl.SSLContext | str | bool = True,
  1251. cert: CertTypes | None = None,
  1252. trust_env: bool = True,
  1253. http1: bool = True,
  1254. http2: bool = False,
  1255. limits: Limits = DEFAULT_LIMITS,
  1256. transport: AsyncBaseTransport | None = None,
  1257. ) -> AsyncBaseTransport:
  1258. if transport is not None:
  1259. return transport
  1260. return AsyncHTTPTransport(
  1261. verify=verify,
  1262. cert=cert,
  1263. trust_env=trust_env,
  1264. http1=http1,
  1265. http2=http2,
  1266. limits=limits,
  1267. )
  1268. def _init_proxy_transport(
  1269. self,
  1270. proxy: Proxy,
  1271. verify: ssl.SSLContext | str | bool = True,
  1272. cert: CertTypes | None = None,
  1273. trust_env: bool = True,
  1274. http1: bool = True,
  1275. http2: bool = False,
  1276. limits: Limits = DEFAULT_LIMITS,
  1277. ) -> AsyncBaseTransport:
  1278. return AsyncHTTPTransport(
  1279. verify=verify,
  1280. cert=cert,
  1281. trust_env=trust_env,
  1282. http1=http1,
  1283. http2=http2,
  1284. limits=limits,
  1285. proxy=proxy,
  1286. )
  1287. def _transport_for_url(self, url: URL) -> AsyncBaseTransport:
  1288. """
  1289. Returns the transport instance that should be used for a given URL.
  1290. This will either be the standard connection pool, or a proxy.
  1291. """
  1292. for pattern, transport in self._mounts.items():
  1293. if pattern.matches(url):
  1294. return self._transport if transport is None else transport
  1295. return self._transport
  1296. async def request(
  1297. self,
  1298. method: str,
  1299. url: URL | str,
  1300. *,
  1301. content: RequestContent | None = None,
  1302. data: RequestData | None = None,
  1303. files: RequestFiles | None = None,
  1304. json: typing.Any | None = None,
  1305. params: QueryParamTypes | None = None,
  1306. headers: HeaderTypes | None = None,
  1307. cookies: CookieTypes | None = None,
  1308. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  1309. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1310. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1311. extensions: RequestExtensions | None = None,
  1312. ) -> Response:
  1313. """
  1314. Build and send a request.
  1315. Equivalent to:
  1316. ```python
  1317. request = client.build_request(...)
  1318. response = await client.send(request, ...)
  1319. ```
  1320. See `AsyncClient.build_request()`, `AsyncClient.send()`
  1321. and [Merging of configuration][0] for how the various parameters
  1322. are merged with client-level configuration.
  1323. [0]: /advanced/clients/#merging-of-configuration
  1324. """
  1325. if cookies is not None: # pragma: no cover
  1326. message = (
  1327. "Setting per-request cookies=<...> is being deprecated, because "
  1328. "the expected behaviour on cookie persistence is ambiguous. Set "
  1329. "cookies directly on the client instance instead."
  1330. )
  1331. warnings.warn(message, DeprecationWarning, stacklevel=2)
  1332. request = self.build_request(
  1333. method=method,
  1334. url=url,
  1335. content=content,
  1336. data=data,
  1337. files=files,
  1338. json=json,
  1339. params=params,
  1340. headers=headers,
  1341. cookies=cookies,
  1342. timeout=timeout,
  1343. extensions=extensions,
  1344. )
  1345. return await self.send(request, auth=auth, follow_redirects=follow_redirects)
  1346. @asynccontextmanager
  1347. async def stream(
  1348. self,
  1349. method: str,
  1350. url: URL | str,
  1351. *,
  1352. content: RequestContent | None = None,
  1353. data: RequestData | None = None,
  1354. files: RequestFiles | None = None,
  1355. json: typing.Any | None = None,
  1356. params: QueryParamTypes | None = None,
  1357. headers: HeaderTypes | None = None,
  1358. cookies: CookieTypes | None = None,
  1359. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  1360. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1361. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1362. extensions: RequestExtensions | None = None,
  1363. ) -> typing.AsyncIterator[Response]:
  1364. """
  1365. Alternative to `httpx.request()` that streams the response body
  1366. instead of loading it into memory at once.
  1367. **Parameters**: See `httpx.request`.
  1368. See also: [Streaming Responses][0]
  1369. [0]: /quickstart#streaming-responses
  1370. """
  1371. request = self.build_request(
  1372. method=method,
  1373. url=url,
  1374. content=content,
  1375. data=data,
  1376. files=files,
  1377. json=json,
  1378. params=params,
  1379. headers=headers,
  1380. cookies=cookies,
  1381. timeout=timeout,
  1382. extensions=extensions,
  1383. )
  1384. response = await self.send(
  1385. request=request,
  1386. auth=auth,
  1387. follow_redirects=follow_redirects,
  1388. stream=True,
  1389. )
  1390. try:
  1391. yield response
  1392. finally:
  1393. await response.aclose()
  1394. async def send(
  1395. self,
  1396. request: Request,
  1397. *,
  1398. stream: bool = False,
  1399. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  1400. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1401. ) -> Response:
  1402. """
  1403. Send a request.
  1404. The request is sent as-is, unmodified.
  1405. Typically you'll want to build one with `AsyncClient.build_request()`
  1406. so that any client-level configuration is merged into the request,
  1407. but passing an explicit `httpx.Request()` is supported as well.
  1408. See also: [Request instances][0]
  1409. [0]: /advanced/clients/#request-instances
  1410. """
  1411. if self._state == ClientState.CLOSED:
  1412. raise RuntimeError("Cannot send a request, as the client has been closed.")
  1413. self._state = ClientState.OPENED
  1414. follow_redirects = (
  1415. self.follow_redirects
  1416. if isinstance(follow_redirects, UseClientDefault)
  1417. else follow_redirects
  1418. )
  1419. self._set_timeout(request)
  1420. auth = self._build_request_auth(request, auth)
  1421. response = await self._send_handling_auth(
  1422. request,
  1423. auth=auth,
  1424. follow_redirects=follow_redirects,
  1425. history=[],
  1426. )
  1427. try:
  1428. if not stream:
  1429. await response.aread()
  1430. return response
  1431. except BaseException as exc:
  1432. await response.aclose()
  1433. raise exc
  1434. async def _send_handling_auth(
  1435. self,
  1436. request: Request,
  1437. auth: Auth,
  1438. follow_redirects: bool,
  1439. history: list[Response],
  1440. ) -> Response:
  1441. auth_flow = auth.async_auth_flow(request)
  1442. try:
  1443. request = await auth_flow.__anext__()
  1444. while True:
  1445. response = await self._send_handling_redirects(
  1446. request,
  1447. follow_redirects=follow_redirects,
  1448. history=history,
  1449. )
  1450. try:
  1451. try:
  1452. next_request = await auth_flow.asend(response)
  1453. except StopAsyncIteration:
  1454. return response
  1455. response.history = list(history)
  1456. await response.aread()
  1457. request = next_request
  1458. history.append(response)
  1459. except BaseException as exc:
  1460. await response.aclose()
  1461. raise exc
  1462. finally:
  1463. await auth_flow.aclose()
  1464. async def _send_handling_redirects(
  1465. self,
  1466. request: Request,
  1467. follow_redirects: bool,
  1468. history: list[Response],
  1469. ) -> Response:
  1470. while True:
  1471. if len(history) > self.max_redirects:
  1472. raise TooManyRedirects(
  1473. "Exceeded maximum allowed redirects.", request=request
  1474. )
  1475. for hook in self._event_hooks["request"]:
  1476. await hook(request)
  1477. response = await self._send_single_request(request)
  1478. try:
  1479. for hook in self._event_hooks["response"]:
  1480. await hook(response)
  1481. response.history = list(history)
  1482. if not response.has_redirect_location:
  1483. return response
  1484. request = self._build_redirect_request(request, response)
  1485. history = history + [response]
  1486. if follow_redirects:
  1487. await response.aread()
  1488. else:
  1489. response.next_request = request
  1490. return response
  1491. except BaseException as exc:
  1492. await response.aclose()
  1493. raise exc
  1494. async def _send_single_request(self, request: Request) -> Response:
  1495. """
  1496. Sends a single request, without handling any redirections.
  1497. """
  1498. transport = self._transport_for_url(request.url)
  1499. start = time.perf_counter()
  1500. if not isinstance(request.stream, AsyncByteStream):
  1501. raise RuntimeError(
  1502. "Attempted to send an sync request with an AsyncClient instance."
  1503. )
  1504. with request_context(request=request):
  1505. response = await transport.handle_async_request(request)
  1506. assert isinstance(response.stream, AsyncByteStream)
  1507. response.request = request
  1508. response.stream = BoundAsyncStream(
  1509. response.stream, response=response, start=start
  1510. )
  1511. self.cookies.extract_cookies(response)
  1512. response.default_encoding = self._default_encoding
  1513. logger.info(
  1514. 'HTTP Request: %s %s "%s %d %s"',
  1515. request.method,
  1516. request.url,
  1517. response.http_version,
  1518. response.status_code,
  1519. response.reason_phrase,
  1520. )
  1521. return response
  1522. async def get(
  1523. self,
  1524. url: URL | str,
  1525. *,
  1526. params: QueryParamTypes | None = None,
  1527. headers: HeaderTypes | None = None,
  1528. cookies: CookieTypes | None = None,
  1529. auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
  1530. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1531. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1532. extensions: RequestExtensions | None = None,
  1533. ) -> Response:
  1534. """
  1535. Send a `GET` request.
  1536. **Parameters**: See `httpx.request`.
  1537. """
  1538. return await self.request(
  1539. "GET",
  1540. url,
  1541. params=params,
  1542. headers=headers,
  1543. cookies=cookies,
  1544. auth=auth,
  1545. follow_redirects=follow_redirects,
  1546. timeout=timeout,
  1547. extensions=extensions,
  1548. )
  1549. async def options(
  1550. self,
  1551. url: URL | str,
  1552. *,
  1553. params: QueryParamTypes | None = None,
  1554. headers: HeaderTypes | None = None,
  1555. cookies: CookieTypes | None = None,
  1556. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1557. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1558. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1559. extensions: RequestExtensions | None = None,
  1560. ) -> Response:
  1561. """
  1562. Send an `OPTIONS` request.
  1563. **Parameters**: See `httpx.request`.
  1564. """
  1565. return await self.request(
  1566. "OPTIONS",
  1567. url,
  1568. params=params,
  1569. headers=headers,
  1570. cookies=cookies,
  1571. auth=auth,
  1572. follow_redirects=follow_redirects,
  1573. timeout=timeout,
  1574. extensions=extensions,
  1575. )
  1576. async def head(
  1577. self,
  1578. url: URL | str,
  1579. *,
  1580. params: QueryParamTypes | None = None,
  1581. headers: HeaderTypes | None = None,
  1582. cookies: CookieTypes | None = None,
  1583. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1584. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1585. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1586. extensions: RequestExtensions | None = None,
  1587. ) -> Response:
  1588. """
  1589. Send a `HEAD` request.
  1590. **Parameters**: See `httpx.request`.
  1591. """
  1592. return await self.request(
  1593. "HEAD",
  1594. url,
  1595. params=params,
  1596. headers=headers,
  1597. cookies=cookies,
  1598. auth=auth,
  1599. follow_redirects=follow_redirects,
  1600. timeout=timeout,
  1601. extensions=extensions,
  1602. )
  1603. async def post(
  1604. self,
  1605. url: URL | str,
  1606. *,
  1607. content: RequestContent | None = None,
  1608. data: RequestData | None = None,
  1609. files: RequestFiles | None = None,
  1610. json: typing.Any | None = None,
  1611. params: QueryParamTypes | None = None,
  1612. headers: HeaderTypes | None = None,
  1613. cookies: CookieTypes | None = None,
  1614. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1615. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1616. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1617. extensions: RequestExtensions | None = None,
  1618. ) -> Response:
  1619. """
  1620. Send a `POST` request.
  1621. **Parameters**: See `httpx.request`.
  1622. """
  1623. return await self.request(
  1624. "POST",
  1625. url,
  1626. content=content,
  1627. data=data,
  1628. files=files,
  1629. json=json,
  1630. params=params,
  1631. headers=headers,
  1632. cookies=cookies,
  1633. auth=auth,
  1634. follow_redirects=follow_redirects,
  1635. timeout=timeout,
  1636. extensions=extensions,
  1637. )
  1638. async def put(
  1639. self,
  1640. url: URL | str,
  1641. *,
  1642. content: RequestContent | None = None,
  1643. data: RequestData | None = None,
  1644. files: RequestFiles | None = None,
  1645. json: typing.Any | None = None,
  1646. params: QueryParamTypes | None = None,
  1647. headers: HeaderTypes | None = None,
  1648. cookies: CookieTypes | None = None,
  1649. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1650. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1651. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1652. extensions: RequestExtensions | None = None,
  1653. ) -> Response:
  1654. """
  1655. Send a `PUT` request.
  1656. **Parameters**: See `httpx.request`.
  1657. """
  1658. return await self.request(
  1659. "PUT",
  1660. url,
  1661. content=content,
  1662. data=data,
  1663. files=files,
  1664. json=json,
  1665. params=params,
  1666. headers=headers,
  1667. cookies=cookies,
  1668. auth=auth,
  1669. follow_redirects=follow_redirects,
  1670. timeout=timeout,
  1671. extensions=extensions,
  1672. )
  1673. async def patch(
  1674. self,
  1675. url: URL | str,
  1676. *,
  1677. content: RequestContent | None = None,
  1678. data: RequestData | None = None,
  1679. files: RequestFiles | None = None,
  1680. json: typing.Any | None = None,
  1681. params: QueryParamTypes | None = None,
  1682. headers: HeaderTypes | None = None,
  1683. cookies: CookieTypes | None = None,
  1684. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1685. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1686. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1687. extensions: RequestExtensions | None = None,
  1688. ) -> Response:
  1689. """
  1690. Send a `PATCH` request.
  1691. **Parameters**: See `httpx.request`.
  1692. """
  1693. return await self.request(
  1694. "PATCH",
  1695. url,
  1696. content=content,
  1697. data=data,
  1698. files=files,
  1699. json=json,
  1700. params=params,
  1701. headers=headers,
  1702. cookies=cookies,
  1703. auth=auth,
  1704. follow_redirects=follow_redirects,
  1705. timeout=timeout,
  1706. extensions=extensions,
  1707. )
  1708. async def delete(
  1709. self,
  1710. url: URL | str,
  1711. *,
  1712. params: QueryParamTypes | None = None,
  1713. headers: HeaderTypes | None = None,
  1714. cookies: CookieTypes | None = None,
  1715. auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1716. follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
  1717. timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
  1718. extensions: RequestExtensions | None = None,
  1719. ) -> Response:
  1720. """
  1721. Send a `DELETE` request.
  1722. **Parameters**: See `httpx.request`.
  1723. """
  1724. return await self.request(
  1725. "DELETE",
  1726. url,
  1727. params=params,
  1728. headers=headers,
  1729. cookies=cookies,
  1730. auth=auth,
  1731. follow_redirects=follow_redirects,
  1732. timeout=timeout,
  1733. extensions=extensions,
  1734. )
  1735. async def aclose(self) -> None:
  1736. """
  1737. Close transport and proxies.
  1738. """
  1739. if self._state != ClientState.CLOSED:
  1740. self._state = ClientState.CLOSED
  1741. await self._transport.aclose()
  1742. for proxy in self._mounts.values():
  1743. if proxy is not None:
  1744. await proxy.aclose()
  1745. async def __aenter__(self: U) -> U:
  1746. if self._state != ClientState.UNOPENED:
  1747. msg = {
  1748. ClientState.OPENED: "Cannot open a client instance more than once.",
  1749. ClientState.CLOSED: (
  1750. "Cannot reopen a client instance, once it has been closed."
  1751. ),
  1752. }[self._state]
  1753. raise RuntimeError(msg)
  1754. self._state = ClientState.OPENED
  1755. await self._transport.__aenter__()
  1756. for proxy in self._mounts.values():
  1757. if proxy is not None:
  1758. await proxy.__aenter__()
  1759. return self
  1760. async def __aexit__(
  1761. self,
  1762. exc_type: type[BaseException] | None = None,
  1763. exc_value: BaseException | None = None,
  1764. traceback: TracebackType | None = None,
  1765. ) -> None:
  1766. self._state = ClientState.CLOSED
  1767. await self._transport.__aexit__(exc_type, exc_value, traceback)
  1768. for proxy in self._mounts.values():
  1769. if proxy is not None:
  1770. await proxy.__aexit__(exc_type, exc_value, traceback)