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