codekingpro/portable-devtools
115k
1from __future__ import annotations2 3import datetime4import enum5import logging6import time7import typing8import warnings9from contextlib import asynccontextmanager, contextmanager10from types import TracebackType11 12from .__version__ import __version__13from ._auth import Auth, BasicAuth, FunctionAuth14from ._config import (15 DEFAULT_LIMITS,16 DEFAULT_MAX_REDIRECTS,17 DEFAULT_TIMEOUT_CONFIG,18 Limits,19 Proxy,20 Timeout,21)22from ._decoders import SUPPORTED_DECODERS23from ._exceptions import (24 InvalidURL,25 RemoteProtocolError,26 TooManyRedirects,27 request_context,28)29from ._models import Cookies, Headers, Request, Response30from ._status_codes import codes31from ._transports.base import AsyncBaseTransport, BaseTransport32from ._transports.default import AsyncHTTPTransport, HTTPTransport33from ._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)48from ._urls import URL, QueryParams49from ._utils import URLPattern, get_environment_proxies50 51if typing.TYPE_CHECKING:52 import ssl # pragma: no cover53 54__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]55 56# The type annotation for @classmethod and context managers here follows PEP 48457# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods58T = typing.TypeVar("T", bound="Client")59U = typing.TypeVar("U", bound="AsyncClient")60 61 62def _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 False68 69 return (70 url.scheme == "http"71 and _port_or_default(url) == 8072 and location.scheme == "https"73 and _port_or_default(location) == 44374 )75 76 77def _port_or_default(url: URL) -> int | None:78 if url.port is not None:79 return url.port80 return {"http": 80, "https": 443}.get(url.scheme)81 82 83def _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.scheme89 and url.host == other.host90 and _port_or_default(url) == _port_or_default(other)91 )92 93 94class UseClientDefault:95 """96 For some parameters such as `auth=...` and `timeout=...` we need to be able97 to indicate the default "unset" state, in a way that is distinctly different98 to using `None`.99 100 The default "unset" state indicates that whatever default is set on the101 client should be used. This is different to setting `None`, which102 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 default106 timeout has been configured on the client. Including `timeout=None` will107 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 114USE_CLIENT_DEFAULT = UseClientDefault()115 116 117logger = logging.getLogger("httpx")118 119USER_AGENT = f"python-httpx/{__version__}"120ACCEPT_ENCODING = ", ".join(121 [key for key in SUPPORTED_DECODERS.keys() if key != "identity"]122)123 124 125class 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 = 1130 # OPENED:131 # The client has either sent a request, or is within a `with` block.132 OPENED = 2133 # CLOSED:134 # The client has either exited the `with` block, or `close()` has135 # been called explicitly.136 CLOSED = 3137 138 139class BoundSyncStream(SyncByteStream):140 """141 A byte stream that is bound to a given response instance, and that142 ensures the `response.elapsed` is set once the response is closed.143 """144 145 def __init__(146 self, stream: SyncByteStream, response: Response, start: float147 ) -> None:148 self._stream = stream149 self._response = response150 self._start = start151 152 def __iter__(self) -> typing.Iterator[bytes]:153 for chunk in self._stream:154 yield chunk155 156 def close(self) -> None:157 elapsed = time.perf_counter() - self._start158 self._response.elapsed = datetime.timedelta(seconds=elapsed)159 self._stream.close()160 161 162class BoundAsyncStream(AsyncByteStream):163 """164 An async byte stream that is bound to a given response instance, and that165 ensures the `response.elapsed` is set once the response is closed.166 """167 168 def __init__(169 self, stream: AsyncByteStream, response: Response, start: float170 ) -> None:171 self._stream = stream172 self._response = response173 self._start = start174 175 async def __aiter__(self) -> typing.AsyncIterator[bytes]:176 async for chunk in self._stream:177 yield chunk178 179 async def aclose(self) -> None:180 elapsed = time.perf_counter() - self._start181 self._response.elapsed = datetime.timedelta(seconds=elapsed)182 await self._stream.aclose()183 184 185EventHook = typing.Callable[..., typing.Any]186 187 188class 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_hooks205 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_redirects214 self.max_redirects = max_redirects215 self._event_hooks = {216 "request": list(event_hooks.get("request", [])),217 "response": list(event_hooks.get("response", [])),218 }219 self._trust_env = trust_env220 self._default_encoding = default_encoding221 self._state = ClientState.UNOPENED222 223 @property224 def is_closed(self) -> bool:225 """226 Check if the client being closed227 """228 return self._state == ClientState.CLOSED229 230 @property231 def trust_env(self) -> bool:232 return self._trust_env233 234 def _enforce_trailing_slash(self, url: URL) -> URL:235 if url.raw_path.endswith(b"/"):236 return url237 return url.copy_with(raw_path=url.raw_path + b"/")238 239 def _get_proxy_map(240 self, proxy: ProxyTypes | None, allow_env_proxies: bool241 ) -> 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 proxy251 return {"all://": proxy}252 253 @property254 def timeout(self) -> Timeout:255 return self._timeout256 257 @timeout.setter258 def timeout(self, timeout: TimeoutTypes) -> None:259 self._timeout = Timeout(timeout)260 261 @property262 def event_hooks(self) -> dict[str, list[EventHook]]:263 return self._event_hooks264 265 @event_hooks.setter266 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 @property273 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/#authentication280 """281 return self._auth282 283 @auth.setter284 def auth(self, auth: AuthTypes) -> None:285 self._auth = self._build_auth(auth)286 287 @property288 def base_url(self) -> URL:289 """290 Base URL to use when sending requests with relative URLs.291 """292 return self._base_url293 294 @base_url.setter295 def base_url(self, url: URL | str) -> None:296 self._base_url = self._enforce_trailing_slash(URL(url))297 298 @property299 def headers(self) -> Headers:300 """301 HTTP headers to include when sending requests.302 """303 return self._headers304 305 @headers.setter306 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_headers317 318 @property319 def cookies(self) -> Cookies:320 """321 Cookie values to include when sending requests.322 """323 return self._cookies324 325 @cookies.setter326 def cookies(self, cookies: CookieTypes) -> None:327 self._cookies = Cookies(cookies)328 329 @property330 def params(self) -> QueryParams:331 """332 Query parameters to include in the URL when sending requests.333 """334 return self._params335 336 @params.setter337 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` arguments359 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-instances365 """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 extensions371 if "timeout" not in extensions:372 timeout = (373 self.timeout374 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 this399 # 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_url406 # URL('https://www.example.com/subpath/')407 # >>> client.build_request("GET", "/path").url408 # 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_url412 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_cookies422 return cookies423 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_headers432 433 def _merge_queryparams(434 self, params: QueryParamTypes | None = None435 ) -> 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 params444 445 def _build_auth(self, auth: AuthTypes | None) -> Auth | None:446 if auth is None:447 return None448 elif isinstance(auth, tuple):449 return BasicAuth(username=auth[0], password=auth[1])450 elif isinstance(auth, Auth):451 return auth452 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 auth468 469 username, password = request.url.username, request.url.password470 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 that478 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 request497 based on certain specs or browser behavior.498 """499 method = request.method500 501 # https://tools.ietf.org/html/rfc7231#section-6.4.4502 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 method516 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=request528 ) from None529 530 # Handle malformed 'Location' headers that are "absolute" form, have no host.531 # See: https://github.com/encode/httpx/issues/771532 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 url545 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 redirected555 # 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 which563 # 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 headers572 573 def _redirect_stream(574 self, request: Request, method: str575 ) -> 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 None581 582 return request.stream583 584 def _set_timeout(self, request: Request) -> None:585 if "timeout" not in request.extensions:586 timeout = (587 self.timeout588 if isinstance(self.timeout, UseClientDefault)589 else Timeout(self.timeout)590 )591 request.extensions = dict(**request.extensions, timeout=timeout.as_dict())592 593 594class 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 ```python603 >>> client = httpx.Client()604 >>> response = client.get('https://example.org')605 ```606 607 **Parameters:**608 609 * **auth** - *(optional)* An authentication class to use when sending610 requests.611 * **params** - *(optional)* Query parameters to include in request URLs, as612 a string, dictionary, or sequence of two-tuples.613 * **headers** - *(optional)* Dictionary of HTTP headers to include when614 sending requests.615 * **cookies** - *(optional)* Dictionary of Cookie items to include when616 sending requests.617 * **verify** - *(optional)* Either `True` to use an SSL context with the618 default CA bundle, `False` to disable verification, or an instance of619 `ssl.SSLContext` to use a custom context.620 * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be621 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 sending624 requests.625 * **limits** - *(optional)* The limits configuration to use.626 * **max_redirects** - *(optional)* The maximum number of redirect responses627 that should be followed.628 * **base_url** - *(optional)* A URL to use as the base when building629 request URLs.630 * **transport** - *(optional)* A transport class to use for sending requests631 over the network.632 * **trust_env** - *(optional)* Enables or disables usage of environment633 variables for configuration.634 * **default_encoding** - *(optional)* The default encoding to use for decoding635 response text, if no charset information is included in a response Content-Type636 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 # noqa679 except ImportError: # pragma: no cover680 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 None684 685 allow_env_proxies = trust_env and transport is None686 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): None699 if proxy is None700 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 transport730 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 transport768 769 return self._transport770 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 ```python794 request = client.build_request(...)795 response = client.send(request, ...)796 ```797 798 See `Client.build_request()`, `Client.send()` and799 [Merging of configuration][0] for how the various parameters800 are merged with client-level configuration.801 802 [0]: /advanced/clients/#merging-of-configuration803 """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 @contextmanager828 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 body847 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-responses854 """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 response876 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-instances899 """900 if self._state == ClientState.CLOSED:901 raise RuntimeError("Cannot send a request, as the client has been closed.")902 903 self._state = ClientState.OPENED904 follow_redirects = (905 self.follow_redirects906 if isinstance(follow_redirects, UseClientDefault)907 else follow_redirects908 )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 response925 926 except BaseException as exc:927 response.close()928 raise exc929 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 response952 953 response.history = list(history)954 response.read()955 request = next_request956 history.append(response)957 958 except BaseException as exc:959 response.close()960 raise exc961 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=request974 )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 response987 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 = request995 return response996 997 except BaseException as exc:998 response.close()999 raise exc1000 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 = request1019 response.stream = BoundSyncStream(1020 response.stream, response=response, start=start1021 )1022 self.cookies.extract_cookies(response)1023 response.default_encoding = self._default_encoding1024 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 response1035 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 *,