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