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