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