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