Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/httpx/_client.py: 25%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import datetime
4import enum
5import logging
6import typing
7import warnings
8from contextlib import asynccontextmanager, contextmanager
9from types import TracebackType
11from .__version__ import __version__
12from ._auth import Auth, BasicAuth, FunctionAuth
13from ._config import (
14 DEFAULT_LIMITS,
15 DEFAULT_MAX_REDIRECTS,
16 DEFAULT_TIMEOUT_CONFIG,
17 Limits,
18 Proxy,
19 Timeout,
20)
21from ._decoders import SUPPORTED_DECODERS
22from ._exceptions import (
23 InvalidURL,
24 RemoteProtocolError,
25 TooManyRedirects,
26 request_context,
27)
28from ._models import Cookies, Headers, Request, Response
29from ._status_codes import codes
30from ._transports.asgi import ASGITransport
31from ._transports.base import AsyncBaseTransport, BaseTransport
32from ._transports.default import AsyncHTTPTransport, HTTPTransport
33from ._transports.wsgi import WSGITransport
34from ._types import (
35 AsyncByteStream,
36 AuthTypes,
37 CertTypes,
38 CookieTypes,
39 HeaderTypes,
40 ProxiesTypes,
41 ProxyTypes,
42 QueryParamTypes,
43 RequestContent,
44 RequestData,
45 RequestExtensions,
46 RequestFiles,
47 SyncByteStream,
48 TimeoutTypes,
49 VerifyTypes,
50)
51from ._urls import URL, QueryParams
52from ._utils import (
53 Timer,
54 URLPattern,
55 get_environment_proxies,
56 is_https_redirect,
57 same_origin,
58)
60__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]
62# The type annotation for @classmethod and context managers here follows PEP 484
63# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods
64T = typing.TypeVar("T", bound="Client")
65U = typing.TypeVar("U", bound="AsyncClient")
68class UseClientDefault:
69 """
70 For some parameters such as `auth=...` and `timeout=...` we need to be able
71 to indicate the default "unset" state, in a way that is distinctly different
72 to using `None`.
74 The default "unset" state indicates that whatever default is set on the
75 client should be used. This is different to setting `None`, which
76 explicitly disables the parameter, possibly overriding a client default.
78 For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature.
79 Omitting the `timeout` parameter will send a request using whatever default
80 timeout has been configured on the client. Including `timeout=None` will
81 ensure no timeout is used.
83 Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant,
84 but it is used internally when a parameter is not included.
85 """
88USE_CLIENT_DEFAULT = UseClientDefault()
91logger = logging.getLogger("httpx")
93USER_AGENT = f"python-httpx/{__version__}"
94ACCEPT_ENCODING = ", ".join(
95 [key for key in SUPPORTED_DECODERS.keys() if key != "identity"]
96)
99class ClientState(enum.Enum):
100 # UNOPENED:
101 # The client has been instantiated, but has not been used to send a request,
102 # or been opened by entering the context of a `with` block.
103 UNOPENED = 1
104 # OPENED:
105 # The client has either sent a request, or is within a `with` block.
106 OPENED = 2
107 # CLOSED:
108 # The client has either exited the `with` block, or `close()` has
109 # been called explicitly.
110 CLOSED = 3
113class BoundSyncStream(SyncByteStream):
114 """
115 A byte stream that is bound to a given response instance, and that
116 ensures the `response.elapsed` is set once the response is closed.
117 """
119 def __init__(
120 self, stream: SyncByteStream, response: Response, timer: Timer
121 ) -> None:
122 self._stream = stream
123 self._response = response
124 self._timer = timer
126 def __iter__(self) -> typing.Iterator[bytes]:
127 for chunk in self._stream:
128 yield chunk
130 def close(self) -> None:
131 seconds = self._timer.sync_elapsed()
132 self._response.elapsed = datetime.timedelta(seconds=seconds)
133 self._stream.close()
136class BoundAsyncStream(AsyncByteStream):
137 """
138 An async byte stream that is bound to a given response instance, and that
139 ensures the `response.elapsed` is set once the response is closed.
140 """
142 def __init__(
143 self, stream: AsyncByteStream, response: Response, timer: Timer
144 ) -> None:
145 self._stream = stream
146 self._response = response
147 self._timer = timer
149 async def __aiter__(self) -> typing.AsyncIterator[bytes]:
150 async for chunk in self._stream:
151 yield chunk
153 async def aclose(self) -> None:
154 seconds = await self._timer.async_elapsed()
155 self._response.elapsed = datetime.timedelta(seconds=seconds)
156 await self._stream.aclose()
159EventHook = typing.Callable[..., typing.Any]
162class BaseClient:
163 def __init__(
164 self,
165 *,
166 auth: AuthTypes | None = None,
167 params: QueryParamTypes | None = None,
168 headers: HeaderTypes | None = None,
169 cookies: CookieTypes | None = None,
170 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
171 follow_redirects: bool = False,
172 max_redirects: int = DEFAULT_MAX_REDIRECTS,
173 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
174 base_url: URL | str = "",
175 trust_env: bool = True,
176 default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
177 ) -> None:
178 event_hooks = {} if event_hooks is None else event_hooks
180 self._base_url = self._enforce_trailing_slash(URL(base_url))
182 self._auth = self._build_auth(auth)
183 self._params = QueryParams(params)
184 self.headers = Headers(headers)
185 self._cookies = Cookies(cookies)
186 self._timeout = Timeout(timeout)
187 self.follow_redirects = follow_redirects
188 self.max_redirects = max_redirects
189 self._event_hooks = {
190 "request": list(event_hooks.get("request", [])),
191 "response": list(event_hooks.get("response", [])),
192 }
193 self._trust_env = trust_env
194 self._default_encoding = default_encoding
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: ProxiesTypes | None, allow_env_proxies: bool
215 ) -> dict[str, Proxy | None]:
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) -> dict[str, list[EventHook]]:
243 return self._event_hooks
245 @event_hooks.setter
246 def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> 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) -> Auth | None:
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: URL | str) -> 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: URL | str,
324 *,
325 content: RequestContent | None = None,
326 data: RequestData | None = None,
327 files: RequestFiles | None = None,
328 json: typing.Any | None = None,
329 params: QueryParamTypes | None = None,
330 headers: HeaderTypes | None = None,
331 cookies: CookieTypes | None = None,
332 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
333 extensions: RequestExtensions | None = 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/clients/#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: URL | str) -> 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(self, cookies: CookieTypes | None = None) -> CookieTypes | None:
394 """
395 Merge a cookies argument together with any cookies on the client,
396 to create the cookies used for the outgoing request.
397 """
398 if cookies or self.cookies:
399 merged_cookies = Cookies(self.cookies)
400 merged_cookies.update(cookies)
401 return merged_cookies
402 return cookies
404 def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None:
405 """
406 Merge a headers argument together with any headers on the client,
407 to create the headers used for the outgoing request.
408 """
409 merged_headers = Headers(self.headers)
410 merged_headers.update(headers)
411 return merged_headers
413 def _merge_queryparams(
414 self, params: QueryParamTypes | None = None
415 ) -> QueryParamTypes | None:
416 """
417 Merge a queryparams argument together with any queryparams on the client,
418 to create the queryparams used for the outgoing request.
419 """
420 if params or self.params:
421 merged_queryparams = QueryParams(self.params)
422 return merged_queryparams.merge(params)
423 return params
425 def _build_auth(self, auth: AuthTypes | None) -> Auth | None:
426 if auth is None:
427 return None
428 elif isinstance(auth, tuple):
429 return BasicAuth(username=auth[0], password=auth[1])
430 elif isinstance(auth, Auth):
431 return auth
432 elif callable(auth):
433 return FunctionAuth(func=auth)
434 else:
435 raise TypeError(f'Invalid "auth" argument: {auth!r}')
437 def _build_request_auth(
438 self,
439 request: Request,
440 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
441 ) -> Auth:
442 auth = (
443 self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth)
444 )
446 if auth is not None:
447 return auth
449 username, password = request.url.username, request.url.password
450 if username or password:
451 return BasicAuth(username=username, password=password)
453 return Auth()
455 def _build_redirect_request(self, request: Request, response: Response) -> Request:
456 """
457 Given a request and a redirect response, return a new request that
458 should be used to effect the redirect.
459 """
460 method = self._redirect_method(request, response)
461 url = self._redirect_url(request, response)
462 headers = self._redirect_headers(request, url, method)
463 stream = self._redirect_stream(request, method)
464 cookies = Cookies(self.cookies)
465 return Request(
466 method=method,
467 url=url,
468 headers=headers,
469 cookies=cookies,
470 stream=stream,
471 extensions=request.extensions,
472 )
474 def _redirect_method(self, request: Request, response: Response) -> str:
475 """
476 When being redirected we may want to change the method of the request
477 based on certain specs or browser behavior.
478 """
479 method = request.method
481 # https://tools.ietf.org/html/rfc7231#section-6.4.4
482 if response.status_code == codes.SEE_OTHER and method != "HEAD":
483 method = "GET"
485 # Do what the browsers do, despite standards...
486 # Turn 302s into GETs.
487 if response.status_code == codes.FOUND and method != "HEAD":
488 method = "GET"
490 # If a POST is responded to with a 301, turn it into a GET.
491 # This bizarre behaviour is explained in 'requests' issue 1704.
492 if response.status_code == codes.MOVED_PERMANENTLY and method == "POST":
493 method = "GET"
495 return method
497 def _redirect_url(self, request: Request, response: Response) -> URL:
498 """
499 Return the URL for the redirect to follow.
500 """
501 location = response.headers["Location"]
503 try:
504 url = URL(location)
505 except InvalidURL as exc:
506 raise RemoteProtocolError(
507 f"Invalid URL in location header: {exc}.", request=request
508 ) from None
510 # Handle malformed 'Location' headers that are "absolute" form, have no host.
511 # See: https://github.com/encode/httpx/issues/771
512 if url.scheme and not url.host:
513 url = url.copy_with(host=request.url.host)
515 # Facilitate relative 'Location' headers, as allowed by RFC 7231.
516 # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
517 if url.is_relative_url:
518 url = request.url.join(url)
520 # Attach previous fragment if needed (RFC 7231 7.1.2)
521 if request.url.fragment and not url.fragment:
522 url = url.copy_with(fragment=request.url.fragment)
524 return url
526 def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers:
527 """
528 Return the headers that should be used for the redirect request.
529 """
530 headers = Headers(request.headers)
532 if not same_origin(url, request.url):
533 if not is_https_redirect(request.url, url):
534 # Strip Authorization headers when responses are redirected
535 # away from the origin. (Except for direct HTTP to HTTPS redirects.)
536 headers.pop("Authorization", None)
538 # Update the Host header.
539 headers["Host"] = url.netloc.decode("ascii")
541 if method != request.method and method == "GET":
542 # If we've switch to a 'GET' request, then strip any headers which
543 # are only relevant to the request body.
544 headers.pop("Content-Length", None)
545 headers.pop("Transfer-Encoding", None)
547 # We should use the client cookie store to determine any cookie header,
548 # rather than whatever was on the original outgoing request.
549 headers.pop("Cookie", None)
551 return headers
553 def _redirect_stream(
554 self, request: Request, method: str
555 ) -> SyncByteStream | AsyncByteStream | None:
556 """
557 Return the body that should be used for the redirect request.
558 """
559 if method != request.method and method == "GET":
560 return None
562 return request.stream
564 def _set_timeout(self, request: Request) -> None:
565 if "timeout" not in request.extensions:
566 timeout = (
567 self.timeout
568 if isinstance(self.timeout, UseClientDefault)
569 else Timeout(self.timeout)
570 )
571 request.extensions = dict(**request.extensions, timeout=timeout.as_dict())
574class Client(BaseClient):
575 """
576 An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.
578 It can be shared between threads.
580 Usage:
582 ```python
583 >>> client = httpx.Client()
584 >>> response = client.get('https://example.org')
585 ```
587 **Parameters:**
589 * **auth** - *(optional)* An authentication class to use when sending
590 requests.
591 * **params** - *(optional)* Query parameters to include in request URLs, as
592 a string, dictionary, or sequence of two-tuples.
593 * **headers** - *(optional)* Dictionary of HTTP headers to include when
594 sending requests.
595 * **cookies** - *(optional)* Dictionary of Cookie items to include when
596 sending requests.
597 * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to
598 verify the identity of requested hosts. Either `True` (default CA bundle),
599 a path to an SSL certificate file, an `ssl.SSLContext`, or `False`
600 (which will disable verification).
601 * **cert** - *(optional)* An SSL certificate used by the requested host
602 to authenticate the client. Either a path to an SSL certificate file, or
603 two-tuple of (certificate file, key file), or a three-tuple of (certificate
604 file, key file, password).
605 * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
606 enabled. Defaults to `False`.
607 * **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
608 * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy
609 URLs.
610 * **timeout** - *(optional)* The timeout configuration to use when sending
611 requests.
612 * **limits** - *(optional)* The limits configuration to use.
613 * **max_redirects** - *(optional)* The maximum number of redirect responses
614 that should be followed.
615 * **base_url** - *(optional)* A URL to use as the base when building
616 request URLs.
617 * **transport** - *(optional)* A transport class to use for sending requests
618 over the network.
619 * **app** - *(optional)* An WSGI application to send requests to,
620 rather than sending actual network requests.
621 * **trust_env** - *(optional)* Enables or disables usage of environment
622 variables for configuration.
623 * **default_encoding** - *(optional)* The default encoding to use for decoding
624 response text, if no charset information is included in a response Content-Type
625 header. Set to a callable for automatic character set detection. Default: "utf-8".
626 """
628 def __init__(
629 self,
630 *,
631 auth: AuthTypes | None = None,
632 params: QueryParamTypes | None = None,
633 headers: HeaderTypes | None = None,
634 cookies: CookieTypes | None = None,
635 verify: VerifyTypes = True,
636 cert: CertTypes | None = None,
637 http1: bool = True,
638 http2: bool = False,
639 proxy: ProxyTypes | None = None,
640 proxies: ProxiesTypes | None = None,
641 mounts: None | (typing.Mapping[str, BaseTransport | None]) = None,
642 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
643 follow_redirects: bool = False,
644 limits: Limits = DEFAULT_LIMITS,
645 max_redirects: int = DEFAULT_MAX_REDIRECTS,
646 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
647 base_url: URL | str = "",
648 transport: BaseTransport | None = None,
649 app: typing.Callable[..., typing.Any] | None = None,
650 trust_env: bool = True,
651 default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
652 ) -> None:
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 if proxies:
677 message = (
678 "The 'proxies' argument is now deprecated."
679 " Use 'proxy' or 'mounts' instead."
680 )
681 warnings.warn(message, DeprecationWarning)
682 if proxy:
683 raise RuntimeError("Use either `proxy` or 'proxies', not both.")
685 if app:
686 message = (
687 "The 'app' shortcut is now deprecated."
688 " Use the explicit style 'transport=WSGITransport(app=...)' instead."
689 )
690 warnings.warn(message, DeprecationWarning)
692 allow_env_proxies = trust_env and app is None and transport is None
693 proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies)
695 self._transport = self._init_transport(
696 verify=verify,
697 cert=cert,
698 http1=http1,
699 http2=http2,
700 limits=limits,
701 transport=transport,
702 app=app,
703 trust_env=trust_env,
704 )
705 self._mounts: dict[URLPattern, BaseTransport | None] = {
706 URLPattern(key): None
707 if proxy is None
708 else self._init_proxy_transport(
709 proxy,
710 verify=verify,
711 cert=cert,
712 http1=http1,
713 http2=http2,
714 limits=limits,
715 trust_env=trust_env,
716 )
717 for key, proxy in proxy_map.items()
718 }
719 if mounts is not None:
720 self._mounts.update(
721 {URLPattern(key): transport for key, transport in mounts.items()}
722 )
724 self._mounts = dict(sorted(self._mounts.items()))
726 def _init_transport(
727 self,
728 verify: VerifyTypes = True,
729 cert: CertTypes | None = None,
730 http1: bool = True,
731 http2: bool = False,
732 limits: Limits = DEFAULT_LIMITS,
733 transport: BaseTransport | None = None,
734 app: typing.Callable[..., typing.Any] | None = None,
735 trust_env: bool = True,
736 ) -> BaseTransport:
737 if transport is not None:
738 return transport
740 if app is not None:
741 return WSGITransport(app=app)
743 return HTTPTransport(
744 verify=verify,
745 cert=cert,
746 http1=http1,
747 http2=http2,
748 limits=limits,
749 trust_env=trust_env,
750 )
752 def _init_proxy_transport(
753 self,
754 proxy: Proxy,
755 verify: VerifyTypes = True,
756 cert: CertTypes | None = None,
757 http1: bool = True,
758 http2: bool = False,
759 limits: Limits = DEFAULT_LIMITS,
760 trust_env: bool = True,
761 ) -> BaseTransport:
762 return HTTPTransport(
763 verify=verify,
764 cert=cert,
765 http1=http1,
766 http2=http2,
767 limits=limits,
768 trust_env=trust_env,
769 proxy=proxy,
770 )
772 def _transport_for_url(self, url: URL) -> BaseTransport:
773 """
774 Returns the transport instance that should be used for a given URL.
775 This will either be the standard connection pool, or a proxy.
776 """
777 for pattern, transport in self._mounts.items():
778 if pattern.matches(url):
779 return self._transport if transport is None else transport
781 return self._transport
783 def request(
784 self,
785 method: str,
786 url: URL | str,
787 *,
788 content: RequestContent | None = None,
789 data: RequestData | None = None,
790 files: RequestFiles | None = None,
791 json: typing.Any | None = None,
792 params: QueryParamTypes | None = None,
793 headers: HeaderTypes | None = None,
794 cookies: CookieTypes | None = None,
795 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
796 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
797 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
798 extensions: RequestExtensions | None = None,
799 ) -> Response:
800 """
801 Build and send a request.
803 Equivalent to:
805 ```python
806 request = client.build_request(...)
807 response = client.send(request, ...)
808 ```
810 See `Client.build_request()`, `Client.send()` and
811 [Merging of configuration][0] for how the various parameters
812 are merged with client-level configuration.
814 [0]: /advanced/clients/#merging-of-configuration
815 """
816 if cookies is not None:
817 message = (
818 "Setting per-request cookies=<...> is being deprecated, because "
819 "the expected behaviour on cookie persistence is ambiguous. Set "
820 "cookies directly on the client instance instead."
821 )
822 warnings.warn(message, DeprecationWarning)
824 request = self.build_request(
825 method=method,
826 url=url,
827 content=content,
828 data=data,
829 files=files,
830 json=json,
831 params=params,
832 headers=headers,
833 cookies=cookies,
834 timeout=timeout,
835 extensions=extensions,
836 )
837 return self.send(request, auth=auth, follow_redirects=follow_redirects)
839 @contextmanager
840 def stream(
841 self,
842 method: str,
843 url: URL | str,
844 *,
845 content: RequestContent | None = None,
846 data: RequestData | None = None,
847 files: RequestFiles | None = None,
848 json: typing.Any | None = None,
849 params: QueryParamTypes | None = None,
850 headers: HeaderTypes | None = None,
851 cookies: CookieTypes | None = None,
852 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
853 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
854 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
855 extensions: RequestExtensions | None = None,
856 ) -> typing.Iterator[Response]:
857 """
858 Alternative to `httpx.request()` that streams the response body
859 instead of loading it into memory at once.
861 **Parameters**: See `httpx.request`.
863 See also: [Streaming Responses][0]
865 [0]: /quickstart#streaming-responses
866 """
867 request = self.build_request(
868 method=method,
869 url=url,
870 content=content,
871 data=data,
872 files=files,
873 json=json,
874 params=params,
875 headers=headers,
876 cookies=cookies,
877 timeout=timeout,
878 extensions=extensions,
879 )
880 response = self.send(
881 request=request,
882 auth=auth,
883 follow_redirects=follow_redirects,
884 stream=True,
885 )
886 try:
887 yield response
888 finally:
889 response.close()
891 def send(
892 self,
893 request: Request,
894 *,
895 stream: bool = False,
896 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
897 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
898 ) -> Response:
899 """
900 Send a request.
902 The request is sent as-is, unmodified.
904 Typically you'll want to build one with `Client.build_request()`
905 so that any client-level configuration is merged into the request,
906 but passing an explicit `httpx.Request()` is supported as well.
908 See also: [Request instances][0]
910 [0]: /advanced/clients/#request-instances
911 """
912 if self._state == ClientState.CLOSED:
913 raise RuntimeError("Cannot send a request, as the client has been closed.")
915 self._state = ClientState.OPENED
916 follow_redirects = (
917 self.follow_redirects
918 if isinstance(follow_redirects, UseClientDefault)
919 else follow_redirects
920 )
922 self._set_timeout(request)
924 auth = self._build_request_auth(request, auth)
926 response = self._send_handling_auth(
927 request,
928 auth=auth,
929 follow_redirects=follow_redirects,
930 history=[],
931 )
932 try:
933 if not stream:
934 response.read()
936 return response
938 except BaseException as exc:
939 response.close()
940 raise exc
942 def _send_handling_auth(
943 self,
944 request: Request,
945 auth: Auth,
946 follow_redirects: bool,
947 history: list[Response],
948 ) -> Response:
949 auth_flow = auth.sync_auth_flow(request)
950 try:
951 request = next(auth_flow)
953 while True:
954 response = self._send_handling_redirects(
955 request,
956 follow_redirects=follow_redirects,
957 history=history,
958 )
959 try:
960 try:
961 next_request = auth_flow.send(response)
962 except StopIteration:
963 return response
965 response.history = list(history)
966 response.read()
967 request = next_request
968 history.append(response)
970 except BaseException as exc:
971 response.close()
972 raise exc
973 finally:
974 auth_flow.close()
976 def _send_handling_redirects(
977 self,
978 request: Request,
979 follow_redirects: bool,
980 history: list[Response],
981 ) -> Response:
982 while True:
983 if len(history) > self.max_redirects:
984 raise TooManyRedirects(
985 "Exceeded maximum allowed redirects.", request=request
986 )
988 for hook in self._event_hooks["request"]:
989 hook(request)
991 response = self._send_single_request(request)
992 try:
993 for hook in self._event_hooks["response"]:
994 hook(response)
995 response.history = list(history)
997 if not response.has_redirect_location:
998 return response
1000 request = self._build_redirect_request(request, response)
1001 history = history + [response]
1003 if follow_redirects:
1004 response.read()
1005 else:
1006 response.next_request = request
1007 return response
1009 except BaseException as exc:
1010 response.close()
1011 raise exc
1013 def _send_single_request(self, request: Request) -> Response:
1014 """
1015 Sends a single request, without handling any redirections.
1016 """
1017 transport = self._transport_for_url(request.url)
1018 timer = Timer()
1019 timer.sync_start()
1021 if not isinstance(request.stream, SyncByteStream):
1022 raise RuntimeError(
1023 "Attempted to send an async request with a sync Client instance."
1024 )
1026 with request_context(request=request):
1027 response = transport.handle_request(request)
1029 assert isinstance(response.stream, SyncByteStream)
1031 response.request = request
1032 response.stream = BoundSyncStream(
1033 response.stream, response=response, timer=timer
1034 )
1035 self.cookies.extract_cookies(response)
1036 response.default_encoding = self._default_encoding
1038 logger.info(
1039 'HTTP Request: %s %s "%s %d %s"',
1040 request.method,
1041 request.url,
1042 response.http_version,
1043 response.status_code,
1044 response.reason_phrase,
1045 )
1047 return response
1049 def get(
1050 self,
1051 url: URL | str,
1052 *,
1053 params: QueryParamTypes | None = None,
1054 headers: HeaderTypes | None = None,
1055 cookies: CookieTypes | None = None,
1056 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1057 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1058 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1059 extensions: RequestExtensions | None = None,
1060 ) -> Response:
1061 """
1062 Send a `GET` request.
1064 **Parameters**: See `httpx.request`.
1065 """
1066 return self.request(
1067 "GET",
1068 url,
1069 params=params,
1070 headers=headers,
1071 cookies=cookies,
1072 auth=auth,
1073 follow_redirects=follow_redirects,
1074 timeout=timeout,
1075 extensions=extensions,
1076 )
1078 def options(
1079 self,
1080 url: URL | str,
1081 *,
1082 params: QueryParamTypes | None = None,
1083 headers: HeaderTypes | None = None,
1084 cookies: CookieTypes | None = None,
1085 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1086 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1087 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1088 extensions: RequestExtensions | None = None,
1089 ) -> Response:
1090 """
1091 Send an `OPTIONS` request.
1093 **Parameters**: See `httpx.request`.
1094 """
1095 return self.request(
1096 "OPTIONS",
1097 url,
1098 params=params,
1099 headers=headers,
1100 cookies=cookies,
1101 auth=auth,
1102 follow_redirects=follow_redirects,
1103 timeout=timeout,
1104 extensions=extensions,
1105 )
1107 def head(
1108 self,
1109 url: URL | str,
1110 *,
1111 params: QueryParamTypes | None = None,
1112 headers: HeaderTypes | None = None,
1113 cookies: CookieTypes | None = None,
1114 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1115 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1116 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1117 extensions: RequestExtensions | None = None,
1118 ) -> Response:
1119 """
1120 Send a `HEAD` request.
1122 **Parameters**: See `httpx.request`.
1123 """
1124 return self.request(
1125 "HEAD",
1126 url,
1127 params=params,
1128 headers=headers,
1129 cookies=cookies,
1130 auth=auth,
1131 follow_redirects=follow_redirects,
1132 timeout=timeout,
1133 extensions=extensions,
1134 )
1136 def post(
1137 self,
1138 url: URL | str,
1139 *,
1140 content: RequestContent | None = None,
1141 data: RequestData | None = None,
1142 files: RequestFiles | None = None,
1143 json: typing.Any | None = None,
1144 params: QueryParamTypes | None = None,
1145 headers: HeaderTypes | None = None,
1146 cookies: CookieTypes | None = None,
1147 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1148 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1149 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1150 extensions: RequestExtensions | None = None,
1151 ) -> Response:
1152 """
1153 Send a `POST` request.
1155 **Parameters**: See `httpx.request`.
1156 """
1157 return self.request(
1158 "POST",
1159 url,
1160 content=content,
1161 data=data,
1162 files=files,
1163 json=json,
1164 params=params,
1165 headers=headers,
1166 cookies=cookies,
1167 auth=auth,
1168 follow_redirects=follow_redirects,
1169 timeout=timeout,
1170 extensions=extensions,
1171 )
1173 def put(
1174 self,
1175 url: URL | str,
1176 *,
1177 content: RequestContent | None = None,
1178 data: RequestData | None = None,
1179 files: RequestFiles | None = None,
1180 json: typing.Any | None = None,
1181 params: QueryParamTypes | None = None,
1182 headers: HeaderTypes | None = None,
1183 cookies: CookieTypes | None = None,
1184 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1185 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1186 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1187 extensions: RequestExtensions | None = None,
1188 ) -> Response:
1189 """
1190 Send a `PUT` request.
1192 **Parameters**: See `httpx.request`.
1193 """
1194 return self.request(
1195 "PUT",
1196 url,
1197 content=content,
1198 data=data,
1199 files=files,
1200 json=json,
1201 params=params,
1202 headers=headers,
1203 cookies=cookies,
1204 auth=auth,
1205 follow_redirects=follow_redirects,
1206 timeout=timeout,
1207 extensions=extensions,
1208 )
1210 def patch(
1211 self,
1212 url: URL | str,
1213 *,
1214 content: RequestContent | None = None,
1215 data: RequestData | None = None,
1216 files: RequestFiles | None = None,
1217 json: typing.Any | None = None,
1218 params: QueryParamTypes | None = None,
1219 headers: HeaderTypes | None = None,
1220 cookies: CookieTypes | None = None,
1221 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1222 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1223 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1224 extensions: RequestExtensions | None = None,
1225 ) -> Response:
1226 """
1227 Send a `PATCH` request.
1229 **Parameters**: See `httpx.request`.
1230 """
1231 return self.request(
1232 "PATCH",
1233 url,
1234 content=content,
1235 data=data,
1236 files=files,
1237 json=json,
1238 params=params,
1239 headers=headers,
1240 cookies=cookies,
1241 auth=auth,
1242 follow_redirects=follow_redirects,
1243 timeout=timeout,
1244 extensions=extensions,
1245 )
1247 def delete(
1248 self,
1249 url: URL | str,
1250 *,
1251 params: QueryParamTypes | None = None,
1252 headers: HeaderTypes | None = None,
1253 cookies: CookieTypes | None = None,
1254 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1255 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1256 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1257 extensions: RequestExtensions | None = None,
1258 ) -> Response:
1259 """
1260 Send a `DELETE` request.
1262 **Parameters**: See `httpx.request`.
1263 """
1264 return self.request(
1265 "DELETE",
1266 url,
1267 params=params,
1268 headers=headers,
1269 cookies=cookies,
1270 auth=auth,
1271 follow_redirects=follow_redirects,
1272 timeout=timeout,
1273 extensions=extensions,
1274 )
1276 def close(self) -> None:
1277 """
1278 Close transport and proxies.
1279 """
1280 if self._state != ClientState.CLOSED:
1281 self._state = ClientState.CLOSED
1283 self._transport.close()
1284 for transport in self._mounts.values():
1285 if transport is not None:
1286 transport.close()
1288 def __enter__(self: T) -> T:
1289 if self._state != ClientState.UNOPENED:
1290 msg = {
1291 ClientState.OPENED: "Cannot open a client instance more than once.",
1292 ClientState.CLOSED: (
1293 "Cannot reopen a client instance, once it has been closed."
1294 ),
1295 }[self._state]
1296 raise RuntimeError(msg)
1298 self._state = ClientState.OPENED
1300 self._transport.__enter__()
1301 for transport in self._mounts.values():
1302 if transport is not None:
1303 transport.__enter__()
1304 return self
1306 def __exit__(
1307 self,
1308 exc_type: type[BaseException] | None = None,
1309 exc_value: BaseException | None = None,
1310 traceback: TracebackType | None = None,
1311 ) -> None:
1312 self._state = ClientState.CLOSED
1314 self._transport.__exit__(exc_type, exc_value, traceback)
1315 for transport in self._mounts.values():
1316 if transport is not None:
1317 transport.__exit__(exc_type, exc_value, traceback)
1320class AsyncClient(BaseClient):
1321 """
1322 An asynchronous HTTP client, with connection pooling, HTTP/2, redirects,
1323 cookie persistence, etc.
1325 It can be shared between tasks.
1327 Usage:
1329 ```python
1330 >>> async with httpx.AsyncClient() as client:
1331 >>> response = await client.get('https://example.org')
1332 ```
1334 **Parameters:**
1336 * **auth** - *(optional)* An authentication class to use when sending
1337 requests.
1338 * **params** - *(optional)* Query parameters to include in request URLs, as
1339 a string, dictionary, or sequence of two-tuples.
1340 * **headers** - *(optional)* Dictionary of HTTP headers to include when
1341 sending requests.
1342 * **cookies** - *(optional)* Dictionary of Cookie items to include when
1343 sending requests.
1344 * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to
1345 verify the identity of requested hosts. Either `True` (default CA bundle),
1346 a path to an SSL certificate file, an `ssl.SSLContext`, or `False`
1347 (which will disable verification).
1348 * **cert** - *(optional)* An SSL certificate used by the requested host
1349 to authenticate the client. Either a path to an SSL certificate file, or
1350 two-tuple of (certificate file, key file), or a three-tuple of (certificate
1351 file, key file, password).
1352 * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
1353 enabled. Defaults to `False`.
1354 * **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
1355 * **proxies** - *(optional)* A dictionary mapping HTTP protocols to proxy
1356 URLs.
1357 * **timeout** - *(optional)* The timeout configuration to use when sending
1358 requests.
1359 * **limits** - *(optional)* The limits configuration to use.
1360 * **max_redirects** - *(optional)* The maximum number of redirect responses
1361 that should be followed.
1362 * **base_url** - *(optional)* A URL to use as the base when building
1363 request URLs.
1364 * **transport** - *(optional)* A transport class to use for sending requests
1365 over the network.
1366 * **app** - *(optional)* An ASGI application to send requests to,
1367 rather than sending actual network requests.
1368 * **trust_env** - *(optional)* Enables or disables usage of environment
1369 variables for configuration.
1370 * **default_encoding** - *(optional)* The default encoding to use for decoding
1371 response text, if no charset information is included in a response Content-Type
1372 header. Set to a callable for automatic character set detection. Default: "utf-8".
1373 """
1375 def __init__(
1376 self,
1377 *,
1378 auth: AuthTypes | None = None,
1379 params: QueryParamTypes | None = None,
1380 headers: HeaderTypes | None = None,
1381 cookies: CookieTypes | None = None,
1382 verify: VerifyTypes = True,
1383 cert: CertTypes | None = None,
1384 http1: bool = True,
1385 http2: bool = False,
1386 proxy: ProxyTypes | None = None,
1387 proxies: ProxiesTypes | None = None,
1388 mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None,
1389 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
1390 follow_redirects: bool = False,
1391 limits: Limits = DEFAULT_LIMITS,
1392 max_redirects: int = DEFAULT_MAX_REDIRECTS,
1393 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
1394 base_url: URL | str = "",
1395 transport: AsyncBaseTransport | None = None,
1396 app: typing.Callable[..., typing.Any] | None = None,
1397 trust_env: bool = True,
1398 default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
1399 ) -> None:
1400 super().__init__(
1401 auth=auth,
1402 params=params,
1403 headers=headers,
1404 cookies=cookies,
1405 timeout=timeout,
1406 follow_redirects=follow_redirects,
1407 max_redirects=max_redirects,
1408 event_hooks=event_hooks,
1409 base_url=base_url,
1410 trust_env=trust_env,
1411 default_encoding=default_encoding,
1412 )
1414 if http2:
1415 try:
1416 import h2 # noqa
1417 except ImportError: # pragma: no cover
1418 raise ImportError(
1419 "Using http2=True, but the 'h2' package is not installed. "
1420 "Make sure to install httpx using `pip install httpx[http2]`."
1421 ) from None
1423 if proxies:
1424 message = (
1425 "The 'proxies' argument is now deprecated."
1426 " Use 'proxy' or 'mounts' instead."
1427 )
1428 warnings.warn(message, DeprecationWarning)
1429 if proxy:
1430 raise RuntimeError("Use either `proxy` or 'proxies', not both.")
1432 if app:
1433 message = (
1434 "The 'app' shortcut is now deprecated."
1435 " Use the explicit style 'transport=ASGITransport(app=...)' instead."
1436 )
1437 warnings.warn(message, DeprecationWarning)
1439 allow_env_proxies = trust_env and app is None and transport is None
1440 proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies)
1442 self._transport = self._init_transport(
1443 verify=verify,
1444 cert=cert,
1445 http1=http1,
1446 http2=http2,
1447 limits=limits,
1448 transport=transport,
1449 app=app,
1450 trust_env=trust_env,
1451 )
1453 self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
1454 URLPattern(key): None
1455 if proxy is None
1456 else self._init_proxy_transport(
1457 proxy,
1458 verify=verify,
1459 cert=cert,
1460 http1=http1,
1461 http2=http2,
1462 limits=limits,
1463 trust_env=trust_env,
1464 )
1465 for key, proxy in proxy_map.items()
1466 }
1467 if mounts is not None:
1468 self._mounts.update(
1469 {URLPattern(key): transport for key, transport in mounts.items()}
1470 )
1471 self._mounts = dict(sorted(self._mounts.items()))
1473 def _init_transport(
1474 self,
1475 verify: VerifyTypes = True,
1476 cert: CertTypes | None = None,
1477 http1: bool = True,
1478 http2: bool = False,
1479 limits: Limits = DEFAULT_LIMITS,
1480 transport: AsyncBaseTransport | None = None,
1481 app: typing.Callable[..., typing.Any] | None = None,
1482 trust_env: bool = True,
1483 ) -> AsyncBaseTransport:
1484 if transport is not None:
1485 return transport
1487 if app is not None:
1488 return ASGITransport(app=app)
1490 return AsyncHTTPTransport(
1491 verify=verify,
1492 cert=cert,
1493 http1=http1,
1494 http2=http2,
1495 limits=limits,
1496 trust_env=trust_env,
1497 )
1499 def _init_proxy_transport(
1500 self,
1501 proxy: Proxy,
1502 verify: VerifyTypes = True,
1503 cert: CertTypes | None = None,
1504 http1: bool = True,
1505 http2: bool = False,
1506 limits: Limits = DEFAULT_LIMITS,
1507 trust_env: bool = True,
1508 ) -> AsyncBaseTransport:
1509 return AsyncHTTPTransport(
1510 verify=verify,
1511 cert=cert,
1512 http1=http1,
1513 http2=http2,
1514 limits=limits,
1515 trust_env=trust_env,
1516 proxy=proxy,
1517 )
1519 def _transport_for_url(self, url: URL) -> AsyncBaseTransport:
1520 """
1521 Returns the transport instance that should be used for a given URL.
1522 This will either be the standard connection pool, or a proxy.
1523 """
1524 for pattern, transport in self._mounts.items():
1525 if pattern.matches(url):
1526 return self._transport if transport is None else transport
1528 return self._transport
1530 async def request(
1531 self,
1532 method: str,
1533 url: URL | str,
1534 *,
1535 content: RequestContent | None = None,
1536 data: RequestData | None = None,
1537 files: RequestFiles | None = None,
1538 json: typing.Any | None = None,
1539 params: QueryParamTypes | None = None,
1540 headers: HeaderTypes | None = None,
1541 cookies: CookieTypes | None = None,
1542 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1543 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1544 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1545 extensions: RequestExtensions | None = None,
1546 ) -> Response:
1547 """
1548 Build and send a request.
1550 Equivalent to:
1552 ```python
1553 request = client.build_request(...)
1554 response = await client.send(request, ...)
1555 ```
1557 See `AsyncClient.build_request()`, `AsyncClient.send()`
1558 and [Merging of configuration][0] for how the various parameters
1559 are merged with client-level configuration.
1561 [0]: /advanced/clients/#merging-of-configuration
1562 """
1564 if cookies is not None: # pragma: no cover
1565 message = (
1566 "Setting per-request cookies=<...> is being deprecated, because "
1567 "the expected behaviour on cookie persistence is ambiguous. Set "
1568 "cookies directly on the client instance instead."
1569 )
1570 warnings.warn(message, DeprecationWarning)
1572 request = self.build_request(
1573 method=method,
1574 url=url,
1575 content=content,
1576 data=data,
1577 files=files,
1578 json=json,
1579 params=params,
1580 headers=headers,
1581 cookies=cookies,
1582 timeout=timeout,
1583 extensions=extensions,
1584 )
1585 return await self.send(request, auth=auth, follow_redirects=follow_redirects)
1587 @asynccontextmanager
1588 async def stream(
1589 self,
1590 method: str,
1591 url: URL | str,
1592 *,
1593 content: RequestContent | None = None,
1594 data: RequestData | None = None,
1595 files: RequestFiles | None = None,
1596 json: typing.Any | None = None,
1597 params: QueryParamTypes | None = None,
1598 headers: HeaderTypes | None = None,
1599 cookies: CookieTypes | None = None,
1600 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1601 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1602 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1603 extensions: RequestExtensions | None = None,
1604 ) -> typing.AsyncIterator[Response]:
1605 """
1606 Alternative to `httpx.request()` that streams the response body
1607 instead of loading it into memory at once.
1609 **Parameters**: See `httpx.request`.
1611 See also: [Streaming Responses][0]
1613 [0]: /quickstart#streaming-responses
1614 """
1615 request = self.build_request(
1616 method=method,
1617 url=url,
1618 content=content,
1619 data=data,
1620 files=files,
1621 json=json,
1622 params=params,
1623 headers=headers,
1624 cookies=cookies,
1625 timeout=timeout,
1626 extensions=extensions,
1627 )
1628 response = await self.send(
1629 request=request,
1630 auth=auth,
1631 follow_redirects=follow_redirects,
1632 stream=True,
1633 )
1634 try:
1635 yield response
1636 finally:
1637 await response.aclose()
1639 async def send(
1640 self,
1641 request: Request,
1642 *,
1643 stream: bool = False,
1644 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1645 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1646 ) -> Response:
1647 """
1648 Send a request.
1650 The request is sent as-is, unmodified.
1652 Typically you'll want to build one with `AsyncClient.build_request()`
1653 so that any client-level configuration is merged into the request,
1654 but passing an explicit `httpx.Request()` is supported as well.
1656 See also: [Request instances][0]
1658 [0]: /advanced/clients/#request-instances
1659 """
1660 if self._state == ClientState.CLOSED:
1661 raise RuntimeError("Cannot send a request, as the client has been closed.")
1663 self._state = ClientState.OPENED
1664 follow_redirects = (
1665 self.follow_redirects
1666 if isinstance(follow_redirects, UseClientDefault)
1667 else follow_redirects
1668 )
1670 self._set_timeout(request)
1672 auth = self._build_request_auth(request, auth)
1674 response = await self._send_handling_auth(
1675 request,
1676 auth=auth,
1677 follow_redirects=follow_redirects,
1678 history=[],
1679 )
1680 try:
1681 if not stream:
1682 await response.aread()
1684 return response
1686 except BaseException as exc:
1687 await response.aclose()
1688 raise exc
1690 async def _send_handling_auth(
1691 self,
1692 request: Request,
1693 auth: Auth,
1694 follow_redirects: bool,
1695 history: list[Response],
1696 ) -> Response:
1697 auth_flow = auth.async_auth_flow(request)
1698 try:
1699 request = await auth_flow.__anext__()
1701 while True:
1702 response = await self._send_handling_redirects(
1703 request,
1704 follow_redirects=follow_redirects,
1705 history=history,
1706 )
1707 try:
1708 try:
1709 next_request = await auth_flow.asend(response)
1710 except StopAsyncIteration:
1711 return response
1713 response.history = list(history)
1714 await response.aread()
1715 request = next_request
1716 history.append(response)
1718 except BaseException as exc:
1719 await response.aclose()
1720 raise exc
1721 finally:
1722 await auth_flow.aclose()
1724 async def _send_handling_redirects(
1725 self,
1726 request: Request,
1727 follow_redirects: bool,
1728 history: list[Response],
1729 ) -> Response:
1730 while True:
1731 if len(history) > self.max_redirects:
1732 raise TooManyRedirects(
1733 "Exceeded maximum allowed redirects.", request=request
1734 )
1736 for hook in self._event_hooks["request"]:
1737 await hook(request)
1739 response = await self._send_single_request(request)
1740 try:
1741 for hook in self._event_hooks["response"]:
1742 await hook(response)
1744 response.history = list(history)
1746 if not response.has_redirect_location:
1747 return response
1749 request = self._build_redirect_request(request, response)
1750 history = history + [response]
1752 if follow_redirects:
1753 await response.aread()
1754 else:
1755 response.next_request = request
1756 return response
1758 except BaseException as exc:
1759 await response.aclose()
1760 raise exc
1762 async def _send_single_request(self, request: Request) -> Response:
1763 """
1764 Sends a single request, without handling any redirections.
1765 """
1766 transport = self._transport_for_url(request.url)
1767 timer = Timer()
1768 await timer.async_start()
1770 if not isinstance(request.stream, AsyncByteStream):
1771 raise RuntimeError(
1772 "Attempted to send an sync request with an AsyncClient instance."
1773 )
1775 with request_context(request=request):
1776 response = await transport.handle_async_request(request)
1778 assert isinstance(response.stream, AsyncByteStream)
1779 response.request = request
1780 response.stream = BoundAsyncStream(
1781 response.stream, response=response, timer=timer
1782 )
1783 self.cookies.extract_cookies(response)
1784 response.default_encoding = self._default_encoding
1786 logger.info(
1787 'HTTP Request: %s %s "%s %d %s"',
1788 request.method,
1789 request.url,
1790 response.http_version,
1791 response.status_code,
1792 response.reason_phrase,
1793 )
1795 return response
1797 async def get(
1798 self,
1799 url: URL | str,
1800 *,
1801 params: QueryParamTypes | None = None,
1802 headers: HeaderTypes | None = None,
1803 cookies: CookieTypes | None = None,
1804 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
1805 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1806 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1807 extensions: RequestExtensions | None = None,
1808 ) -> Response:
1809 """
1810 Send a `GET` request.
1812 **Parameters**: See `httpx.request`.
1813 """
1814 return await self.request(
1815 "GET",
1816 url,
1817 params=params,
1818 headers=headers,
1819 cookies=cookies,
1820 auth=auth,
1821 follow_redirects=follow_redirects,
1822 timeout=timeout,
1823 extensions=extensions,
1824 )
1826 async def options(
1827 self,
1828 url: URL | str,
1829 *,
1830 params: QueryParamTypes | None = None,
1831 headers: HeaderTypes | None = None,
1832 cookies: CookieTypes | None = None,
1833 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1834 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1835 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1836 extensions: RequestExtensions | None = None,
1837 ) -> Response:
1838 """
1839 Send an `OPTIONS` request.
1841 **Parameters**: See `httpx.request`.
1842 """
1843 return await self.request(
1844 "OPTIONS",
1845 url,
1846 params=params,
1847 headers=headers,
1848 cookies=cookies,
1849 auth=auth,
1850 follow_redirects=follow_redirects,
1851 timeout=timeout,
1852 extensions=extensions,
1853 )
1855 async def head(
1856 self,
1857 url: URL | str,
1858 *,
1859 params: QueryParamTypes | None = None,
1860 headers: HeaderTypes | None = None,
1861 cookies: CookieTypes | None = None,
1862 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1863 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1864 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1865 extensions: RequestExtensions | None = None,
1866 ) -> Response:
1867 """
1868 Send a `HEAD` request.
1870 **Parameters**: See `httpx.request`.
1871 """
1872 return await self.request(
1873 "HEAD",
1874 url,
1875 params=params,
1876 headers=headers,
1877 cookies=cookies,
1878 auth=auth,
1879 follow_redirects=follow_redirects,
1880 timeout=timeout,
1881 extensions=extensions,
1882 )
1884 async def post(
1885 self,
1886 url: URL | str,
1887 *,
1888 content: RequestContent | None = None,
1889 data: RequestData | None = None,
1890 files: RequestFiles | None = None,
1891 json: typing.Any | None = None,
1892 params: QueryParamTypes | None = None,
1893 headers: HeaderTypes | None = None,
1894 cookies: CookieTypes | None = None,
1895 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1896 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1897 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1898 extensions: RequestExtensions | None = None,
1899 ) -> Response:
1900 """
1901 Send a `POST` request.
1903 **Parameters**: See `httpx.request`.
1904 """
1905 return await self.request(
1906 "POST",
1907 url,
1908 content=content,
1909 data=data,
1910 files=files,
1911 json=json,
1912 params=params,
1913 headers=headers,
1914 cookies=cookies,
1915 auth=auth,
1916 follow_redirects=follow_redirects,
1917 timeout=timeout,
1918 extensions=extensions,
1919 )
1921 async def put(
1922 self,
1923 url: URL | str,
1924 *,
1925 content: RequestContent | None = None,
1926 data: RequestData | None = None,
1927 files: RequestFiles | None = None,
1928 json: typing.Any | None = None,
1929 params: QueryParamTypes | None = None,
1930 headers: HeaderTypes | None = None,
1931 cookies: CookieTypes | None = None,
1932 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1933 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1934 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1935 extensions: RequestExtensions | None = None,
1936 ) -> Response:
1937 """
1938 Send a `PUT` request.
1940 **Parameters**: See `httpx.request`.
1941 """
1942 return await self.request(
1943 "PUT",
1944 url,
1945 content=content,
1946 data=data,
1947 files=files,
1948 json=json,
1949 params=params,
1950 headers=headers,
1951 cookies=cookies,
1952 auth=auth,
1953 follow_redirects=follow_redirects,
1954 timeout=timeout,
1955 extensions=extensions,
1956 )
1958 async def patch(
1959 self,
1960 url: URL | str,
1961 *,
1962 content: RequestContent | None = None,
1963 data: RequestData | None = None,
1964 files: RequestFiles | None = None,
1965 json: typing.Any | None = None,
1966 params: QueryParamTypes | None = None,
1967 headers: HeaderTypes | None = None,
1968 cookies: CookieTypes | None = None,
1969 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1970 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
1971 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
1972 extensions: RequestExtensions | None = None,
1973 ) -> Response:
1974 """
1975 Send a `PATCH` request.
1977 **Parameters**: See `httpx.request`.
1978 """
1979 return await self.request(
1980 "PATCH",
1981 url,
1982 content=content,
1983 data=data,
1984 files=files,
1985 json=json,
1986 params=params,
1987 headers=headers,
1988 cookies=cookies,
1989 auth=auth,
1990 follow_redirects=follow_redirects,
1991 timeout=timeout,
1992 extensions=extensions,
1993 )
1995 async def delete(
1996 self,
1997 url: URL | str,
1998 *,
1999 params: QueryParamTypes | None = None,
2000 headers: HeaderTypes | None = None,
2001 cookies: CookieTypes | None = None,
2002 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
2003 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
2004 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
2005 extensions: RequestExtensions | None = None,
2006 ) -> Response:
2007 """
2008 Send a `DELETE` request.
2010 **Parameters**: See `httpx.request`.
2011 """
2012 return await self.request(
2013 "DELETE",
2014 url,
2015 params=params,
2016 headers=headers,
2017 cookies=cookies,
2018 auth=auth,
2019 follow_redirects=follow_redirects,
2020 timeout=timeout,
2021 extensions=extensions,
2022 )
2024 async def aclose(self) -> None:
2025 """
2026 Close transport and proxies.
2027 """
2028 if self._state != ClientState.CLOSED:
2029 self._state = ClientState.CLOSED
2031 await self._transport.aclose()
2032 for proxy in self._mounts.values():
2033 if proxy is not None:
2034 await proxy.aclose()
2036 async def __aenter__(self: U) -> U:
2037 if self._state != ClientState.UNOPENED:
2038 msg = {
2039 ClientState.OPENED: "Cannot open a client instance more than once.",
2040 ClientState.CLOSED: (
2041 "Cannot reopen a client instance, once it has been closed."
2042 ),
2043 }[self._state]
2044 raise RuntimeError(msg)
2046 self._state = ClientState.OPENED
2048 await self._transport.__aenter__()
2049 for proxy in self._mounts.values():
2050 if proxy is not None:
2051 await proxy.__aenter__()
2052 return self
2054 async def __aexit__(
2055 self,
2056 exc_type: type[BaseException] | None = None,
2057 exc_value: BaseException | None = None,
2058 traceback: TracebackType | None = None,
2059 ) -> None:
2060 self._state = ClientState.CLOSED
2062 await self._transport.__aexit__(exc_type, exc_value, traceback)
2063 for proxy in self._mounts.values():
2064 if proxy is not None:
2065 await proxy.__aexit__(exc_type, exc_value, traceback)