| from __future__ import annotations |
|
|
| import datetime |
| import enum |
| import logging |
| import time |
| import typing |
| import warnings |
| from contextlib import asynccontextmanager, contextmanager |
| from types import TracebackType |
|
|
| from .__version__ import __version__ |
| from ._auth import Auth, BasicAuth, FunctionAuth |
| from ._config import ( |
| DEFAULT_LIMITS, |
| DEFAULT_MAX_REDIRECTS, |
| DEFAULT_TIMEOUT_CONFIG, |
| Limits, |
| Proxy, |
| Timeout, |
| ) |
| from ._decoders import SUPPORTED_DECODERS |
| from ._exceptions import ( |
| InvalidURL, |
| RemoteProtocolError, |
| TooManyRedirects, |
| request_context, |
| ) |
| from ._models import Cookies, Headers, Request, Response |
| from ._status_codes import codes |
| from ._transports.base import AsyncBaseTransport, BaseTransport |
| from ._transports.default import AsyncHTTPTransport, HTTPTransport |
| from ._types import ( |
| AsyncByteStream, |
| AuthTypes, |
| CertTypes, |
| CookieTypes, |
| HeaderTypes, |
| ProxyTypes, |
| QueryParamTypes, |
| RequestContent, |
| RequestData, |
| RequestExtensions, |
| RequestFiles, |
| SyncByteStream, |
| TimeoutTypes, |
| ) |
| from ._urls import URL, QueryParams |
| from ._utils import URLPattern, get_environment_proxies |
|
|
| if typing.TYPE_CHECKING: |
| import ssl |
|
|
| __all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"] |
|
|
| |
| |
| T = typing.TypeVar("T", bound="Client") |
| U = typing.TypeVar("U", bound="AsyncClient") |
|
|
|
|
| def _is_https_redirect(url: URL, location: URL) -> bool: |
| """ |
| Return 'True' if 'location' is a HTTPS upgrade of 'url' |
| """ |
| if url.host != location.host: |
| return False |
|
|
| return ( |
| url.scheme == "http" |
| and _port_or_default(url) == 80 |
| and location.scheme == "https" |
| and _port_or_default(location) == 443 |
| ) |
|
|
|
|
| def _port_or_default(url: URL) -> int | None: |
| if url.port is not None: |
| return url.port |
| return {"http": 80, "https": 443}.get(url.scheme) |
|
|
|
|
| def _same_origin(url: URL, other: URL) -> bool: |
| """ |
| Return 'True' if the given URLs share the same origin. |
| """ |
| return ( |
| url.scheme == other.scheme |
| and url.host == other.host |
| and _port_or_default(url) == _port_or_default(other) |
| ) |
|
|
|
|
| class UseClientDefault: |
| """ |
| For some parameters such as `auth=...` and `timeout=...` we need to be able |
| to indicate the default "unset" state, in a way that is distinctly different |
| to using `None`. |
| |
| The default "unset" state indicates that whatever default is set on the |
| client should be used. This is different to setting `None`, which |
| explicitly disables the parameter, possibly overriding a client default. |
| |
| For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature. |
| Omitting the `timeout` parameter will send a request using whatever default |
| timeout has been configured on the client. Including `timeout=None` will |
| ensure no timeout is used. |
| |
| Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant, |
| but it is used internally when a parameter is not included. |
| """ |
|
|
|
|
| USE_CLIENT_DEFAULT = UseClientDefault() |
|
|
|
|
| logger = logging.getLogger("httpx") |
|
|
| USER_AGENT = f"python-httpx/{__version__}" |
| ACCEPT_ENCODING = ", ".join( |
| [key for key in SUPPORTED_DECODERS.keys() if key != "identity"] |
| ) |
|
|
|
|
| class ClientState(enum.Enum): |
| |
| |
| |
| UNOPENED = 1 |
| |
| |
| OPENED = 2 |
| |
| |
| |
| CLOSED = 3 |
|
|
|
|
| class BoundSyncStream(SyncByteStream): |
| """ |
| A byte stream that is bound to a given response instance, and that |
| ensures the `response.elapsed` is set once the response is closed. |
| """ |
|
|
| def __init__( |
| self, stream: SyncByteStream, response: Response, start: float |
| ) -> None: |
| self._stream = stream |
| self._response = response |
| self._start = start |
|
|
| def __iter__(self) -> typing.Iterator[bytes]: |
| for chunk in self._stream: |
| yield chunk |
|
|
| def close(self) -> None: |
| elapsed = time.perf_counter() - self._start |
| self._response.elapsed = datetime.timedelta(seconds=elapsed) |
| self._stream.close() |
|
|
|
|
| class BoundAsyncStream(AsyncByteStream): |
| """ |
| An async byte stream that is bound to a given response instance, and that |
| ensures the `response.elapsed` is set once the response is closed. |
| """ |
|
|
| def __init__( |
| self, stream: AsyncByteStream, response: Response, start: float |
| ) -> None: |
| self._stream = stream |
| self._response = response |
| self._start = start |
|
|
| async def __aiter__(self) -> typing.AsyncIterator[bytes]: |
| async for chunk in self._stream: |
| yield chunk |
|
|
| async def aclose(self) -> None: |
| elapsed = time.perf_counter() - self._start |
| self._response.elapsed = datetime.timedelta(seconds=elapsed) |
| await self._stream.aclose() |
|
|
|
|
| EventHook = typing.Callable[..., typing.Any] |
|
|
|
|
| class BaseClient: |
| def __init__( |
| self, |
| *, |
| auth: AuthTypes | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, |
| follow_redirects: bool = False, |
| max_redirects: int = DEFAULT_MAX_REDIRECTS, |
| event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, |
| base_url: URL | str = "", |
| trust_env: bool = True, |
| default_encoding: str | typing.Callable[[bytes], str] = "utf-8", |
| ) -> None: |
| event_hooks = {} if event_hooks is None else event_hooks |
|
|
| self._base_url = self._enforce_trailing_slash(URL(base_url)) |
|
|
| self._auth = self._build_auth(auth) |
| self._params = QueryParams(params) |
| self.headers = Headers(headers) |
| self._cookies = Cookies(cookies) |
| self._timeout = Timeout(timeout) |
| self.follow_redirects = follow_redirects |
| self.max_redirects = max_redirects |
| self._event_hooks = { |
| "request": list(event_hooks.get("request", [])), |
| "response": list(event_hooks.get("response", [])), |
| } |
| self._trust_env = trust_env |
| self._default_encoding = default_encoding |
| self._state = ClientState.UNOPENED |
|
|
| @property |
| def is_closed(self) -> bool: |
| """ |
| Check if the client being closed |
| """ |
| return self._state == ClientState.CLOSED |
|
|
| @property |
| def trust_env(self) -> bool: |
| return self._trust_env |
|
|
| def _enforce_trailing_slash(self, url: URL) -> URL: |
| if url.raw_path.endswith(b"/"): |
| return url |
| return url.copy_with(raw_path=url.raw_path + b"/") |
|
|
| def _get_proxy_map( |
| self, proxy: ProxyTypes | None, allow_env_proxies: bool |
| ) -> dict[str, Proxy | None]: |
| if proxy is None: |
| if allow_env_proxies: |
| return { |
| key: None if url is None else Proxy(url=url) |
| for key, url in get_environment_proxies().items() |
| } |
| return {} |
| else: |
| proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy |
| return {"all://": proxy} |
|
|
| @property |
| def timeout(self) -> Timeout: |
| return self._timeout |
|
|
| @timeout.setter |
| def timeout(self, timeout: TimeoutTypes) -> None: |
| self._timeout = Timeout(timeout) |
|
|
| @property |
| def event_hooks(self) -> dict[str, list[EventHook]]: |
| return self._event_hooks |
|
|
| @event_hooks.setter |
| def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None: |
| self._event_hooks = { |
| "request": list(event_hooks.get("request", [])), |
| "response": list(event_hooks.get("response", [])), |
| } |
|
|
| @property |
| def auth(self) -> Auth | None: |
| """ |
| Authentication class used when none is passed at the request-level. |
| |
| See also [Authentication][0]. |
| |
| [0]: /quickstart/#authentication |
| """ |
| return self._auth |
|
|
| @auth.setter |
| def auth(self, auth: AuthTypes) -> None: |
| self._auth = self._build_auth(auth) |
|
|
| @property |
| def base_url(self) -> URL: |
| """ |
| Base URL to use when sending requests with relative URLs. |
| """ |
| return self._base_url |
|
|
| @base_url.setter |
| def base_url(self, url: URL | str) -> None: |
| self._base_url = self._enforce_trailing_slash(URL(url)) |
|
|
| @property |
| def headers(self) -> Headers: |
| """ |
| HTTP headers to include when sending requests. |
| """ |
| return self._headers |
|
|
| @headers.setter |
| def headers(self, headers: HeaderTypes) -> None: |
| client_headers = Headers( |
| { |
| b"Accept": b"*/*", |
| b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"), |
| b"Connection": b"keep-alive", |
| b"User-Agent": USER_AGENT.encode("ascii"), |
| } |
| ) |
| client_headers.update(headers) |
| self._headers = client_headers |
|
|
| @property |
| def cookies(self) -> Cookies: |
| """ |
| Cookie values to include when sending requests. |
| """ |
| return self._cookies |
|
|
| @cookies.setter |
| def cookies(self, cookies: CookieTypes) -> None: |
| self._cookies = Cookies(cookies) |
|
|
| @property |
| def params(self) -> QueryParams: |
| """ |
| Query parameters to include in the URL when sending requests. |
| """ |
| return self._params |
|
|
| @params.setter |
| def params(self, params: QueryParamTypes) -> None: |
| self._params = QueryParams(params) |
|
|
| def build_request( |
| self, |
| method: str, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Request: |
| """ |
| Build and return a request instance. |
| |
| * The `params`, `headers` and `cookies` arguments |
| are merged with any values set on the client. |
| * The `url` argument is merged with any `base_url` set on the client. |
| |
| See also: [Request instances][0] |
| |
| [0]: /advanced/clients/#request-instances |
| """ |
| url = self._merge_url(url) |
| headers = self._merge_headers(headers) |
| cookies = self._merge_cookies(cookies) |
| params = self._merge_queryparams(params) |
| extensions = {} if extensions is None else extensions |
| if "timeout" not in extensions: |
| timeout = ( |
| self.timeout |
| if isinstance(timeout, UseClientDefault) |
| else Timeout(timeout) |
| ) |
| extensions = dict(**extensions, timeout=timeout.as_dict()) |
| return Request( |
| method, |
| url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| extensions=extensions, |
| ) |
|
|
| def _merge_url(self, url: URL | str) -> URL: |
| """ |
| Merge a URL argument together with any 'base_url' on the client, |
| to create the URL used for the outgoing request. |
| """ |
| merge_url = URL(url) |
| if merge_url.is_relative_url: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") |
| return self.base_url.copy_with(raw_path=merge_raw_path) |
| return merge_url |
|
|
| def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | None: |
| """ |
| Merge a cookies argument together with any cookies on the client, |
| to create the cookies used for the outgoing request. |
| """ |
| if cookies or self.cookies: |
| merged_cookies = Cookies(self.cookies) |
| merged_cookies.update(cookies) |
| return merged_cookies |
| return cookies |
|
|
| def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None: |
| """ |
| Merge a headers argument together with any headers on the client, |
| to create the headers used for the outgoing request. |
| """ |
| merged_headers = Headers(self.headers) |
| merged_headers.update(headers) |
| return merged_headers |
|
|
| def _merge_queryparams( |
| self, params: QueryParamTypes | None = None |
| ) -> QueryParamTypes | None: |
| """ |
| Merge a queryparams argument together with any queryparams on the client, |
| to create the queryparams used for the outgoing request. |
| """ |
| if params or self.params: |
| merged_queryparams = QueryParams(self.params) |
| return merged_queryparams.merge(params) |
| return params |
|
|
| def _build_auth(self, auth: AuthTypes | None) -> Auth | None: |
| if auth is None: |
| return None |
| elif isinstance(auth, tuple): |
| return BasicAuth(username=auth[0], password=auth[1]) |
| elif isinstance(auth, Auth): |
| return auth |
| elif callable(auth): |
| return FunctionAuth(func=auth) |
| else: |
| raise TypeError(f'Invalid "auth" argument: {auth!r}') |
|
|
| def _build_request_auth( |
| self, |
| request: Request, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| ) -> Auth: |
| auth = ( |
| self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth) |
| ) |
|
|
| if auth is not None: |
| return auth |
|
|
| username, password = request.url.username, request.url.password |
| if username or password: |
| return BasicAuth(username=username, password=password) |
|
|
| return Auth() |
|
|
| def _build_redirect_request(self, request: Request, response: Response) -> Request: |
| """ |
| Given a request and a redirect response, return a new request that |
| should be used to effect the redirect. |
| """ |
| method = self._redirect_method(request, response) |
| url = self._redirect_url(request, response) |
| headers = self._redirect_headers(request, url, method) |
| stream = self._redirect_stream(request, method) |
| cookies = Cookies(self.cookies) |
| return Request( |
| method=method, |
| url=url, |
| headers=headers, |
| cookies=cookies, |
| stream=stream, |
| extensions=request.extensions, |
| ) |
|
|
| def _redirect_method(self, request: Request, response: Response) -> str: |
| """ |
| When being redirected we may want to change the method of the request |
| based on certain specs or browser behavior. |
| """ |
| method = request.method |
|
|
| |
| if response.status_code == codes.SEE_OTHER and method != "HEAD": |
| method = "GET" |
|
|
| |
| |
| if response.status_code == codes.FOUND and method != "HEAD": |
| method = "GET" |
|
|
| |
| |
| if response.status_code == codes.MOVED_PERMANENTLY and method == "POST": |
| method = "GET" |
|
|
| return method |
|
|
| def _redirect_url(self, request: Request, response: Response) -> URL: |
| """ |
| Return the URL for the redirect to follow. |
| """ |
| location = response.headers["Location"] |
|
|
| try: |
| url = URL(location) |
| except InvalidURL as exc: |
| raise RemoteProtocolError( |
| f"Invalid URL in location header: {exc}.", request=request |
| ) from None |
|
|
| |
| |
| if url.scheme and not url.host: |
| url = url.copy_with(host=request.url.host) |
|
|
| |
| |
| if url.is_relative_url: |
| url = request.url.join(url) |
|
|
| |
| if request.url.fragment and not url.fragment: |
| url = url.copy_with(fragment=request.url.fragment) |
|
|
| return url |
|
|
| def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers: |
| """ |
| Return the headers that should be used for the redirect request. |
| """ |
| headers = Headers(request.headers) |
|
|
| if not _same_origin(url, request.url): |
| if not _is_https_redirect(request.url, url): |
| |
| |
| headers.pop("Authorization", None) |
|
|
| |
| headers["Host"] = url.netloc.decode("ascii") |
|
|
| if method != request.method and method == "GET": |
| |
| |
| headers.pop("Content-Length", None) |
| headers.pop("Transfer-Encoding", None) |
|
|
| |
| |
| headers.pop("Cookie", None) |
|
|
| return headers |
|
|
| def _redirect_stream( |
| self, request: Request, method: str |
| ) -> SyncByteStream | AsyncByteStream | None: |
| """ |
| Return the body that should be used for the redirect request. |
| """ |
| if method != request.method and method == "GET": |
| return None |
|
|
| return request.stream |
|
|
| def _set_timeout(self, request: Request) -> None: |
| if "timeout" not in request.extensions: |
| timeout = ( |
| self.timeout |
| if isinstance(self.timeout, UseClientDefault) |
| else Timeout(self.timeout) |
| ) |
| request.extensions = dict(**request.extensions, timeout=timeout.as_dict()) |
|
|
|
|
| class Client(BaseClient): |
| """ |
| An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc. |
| |
| It can be shared between threads. |
| |
| Usage: |
| |
| ```python |
| >>> client = httpx.Client() |
| >>> response = client.get('https://example.org') |
| ``` |
| |
| **Parameters:** |
| |
| * **auth** - *(optional)* An authentication class to use when sending |
| requests. |
| * **params** - *(optional)* Query parameters to include in request URLs, as |
| a string, dictionary, or sequence of two-tuples. |
| * **headers** - *(optional)* Dictionary of HTTP headers to include when |
| sending requests. |
| * **cookies** - *(optional)* Dictionary of Cookie items to include when |
| sending requests. |
| * **verify** - *(optional)* Either `True` to use an SSL context with the |
| default CA bundle, `False` to disable verification, or an instance of |
| `ssl.SSLContext` to use a custom context. |
| * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be |
| enabled. Defaults to `False`. |
| * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. |
| * **timeout** - *(optional)* The timeout configuration to use when sending |
| requests. |
| * **limits** - *(optional)* The limits configuration to use. |
| * **max_redirects** - *(optional)* The maximum number of redirect responses |
| that should be followed. |
| * **base_url** - *(optional)* A URL to use as the base when building |
| request URLs. |
| * **transport** - *(optional)* A transport class to use for sending requests |
| over the network. |
| * **trust_env** - *(optional)* Enables or disables usage of environment |
| variables for configuration. |
| * **default_encoding** - *(optional)* The default encoding to use for decoding |
| response text, if no charset information is included in a response Content-Type |
| header. Set to a callable for automatic character set detection. Default: "utf-8". |
| """ |
|
|
| def __init__( |
| self, |
| *, |
| auth: AuthTypes | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| verify: ssl.SSLContext | str | bool = True, |
| cert: CertTypes | None = None, |
| trust_env: bool = True, |
| http1: bool = True, |
| http2: bool = False, |
| proxy: ProxyTypes | None = None, |
| mounts: None | (typing.Mapping[str, BaseTransport | None]) = None, |
| timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, |
| follow_redirects: bool = False, |
| limits: Limits = DEFAULT_LIMITS, |
| max_redirects: int = DEFAULT_MAX_REDIRECTS, |
| event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, |
| base_url: URL | str = "", |
| transport: BaseTransport | None = None, |
| default_encoding: str | typing.Callable[[bytes], str] = "utf-8", |
| ) -> None: |
| super().__init__( |
| auth=auth, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| timeout=timeout, |
| follow_redirects=follow_redirects, |
| max_redirects=max_redirects, |
| event_hooks=event_hooks, |
| base_url=base_url, |
| trust_env=trust_env, |
| default_encoding=default_encoding, |
| ) |
|
|
| if http2: |
| try: |
| import h2 |
| except ImportError: |
| raise ImportError( |
| "Using http2=True, but the 'h2' package is not installed. " |
| "Make sure to install httpx using `pip install httpx[http2]`." |
| ) from None |
|
|
| allow_env_proxies = trust_env and transport is None |
| proxy_map = self._get_proxy_map(proxy, allow_env_proxies) |
|
|
| self._transport = self._init_transport( |
| verify=verify, |
| cert=cert, |
| trust_env=trust_env, |
| http1=http1, |
| http2=http2, |
| limits=limits, |
| transport=transport, |
| ) |
| self._mounts: dict[URLPattern, BaseTransport | None] = { |
| URLPattern(key): None |
| if proxy is None |
| else self._init_proxy_transport( |
| proxy, |
| verify=verify, |
| cert=cert, |
| trust_env=trust_env, |
| http1=http1, |
| http2=http2, |
| limits=limits, |
| ) |
| for key, proxy in proxy_map.items() |
| } |
| if mounts is not None: |
| self._mounts.update( |
| {URLPattern(key): transport for key, transport in mounts.items()} |
| ) |
|
|
| self._mounts = dict(sorted(self._mounts.items())) |
|
|
| def _init_transport( |
| self, |
| verify: ssl.SSLContext | str | bool = True, |
| cert: CertTypes | None = None, |
| trust_env: bool = True, |
| http1: bool = True, |
| http2: bool = False, |
| limits: Limits = DEFAULT_LIMITS, |
| transport: BaseTransport | None = None, |
| ) -> BaseTransport: |
| if transport is not None: |
| return transport |
|
|
| return HTTPTransport( |
| verify=verify, |
| cert=cert, |
| trust_env=trust_env, |
| http1=http1, |
| http2=http2, |
| limits=limits, |
| ) |
|
|
| def _init_proxy_transport( |
| self, |
| proxy: Proxy, |
| verify: ssl.SSLContext | str | bool = True, |
| cert: CertTypes | None = None, |
| trust_env: bool = True, |
| http1: bool = True, |
| http2: bool = False, |
| limits: Limits = DEFAULT_LIMITS, |
| ) -> BaseTransport: |
| return HTTPTransport( |
| verify=verify, |
| cert=cert, |
| trust_env=trust_env, |
| http1=http1, |
| http2=http2, |
| limits=limits, |
| proxy=proxy, |
| ) |
|
|
| def _transport_for_url(self, url: URL) -> BaseTransport: |
| """ |
| Returns the transport instance that should be used for a given URL. |
| This will either be the standard connection pool, or a proxy. |
| """ |
| for pattern, transport in self._mounts.items(): |
| if pattern.matches(url): |
| return self._transport if transport is None else transport |
|
|
| return self._transport |
|
|
| def request( |
| self, |
| method: str, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Build and send a request. |
| |
| Equivalent to: |
| |
| ```python |
| request = client.build_request(...) |
| response = client.send(request, ...) |
| ``` |
| |
| See `Client.build_request()`, `Client.send()` and |
| [Merging of configuration][0] for how the various parameters |
| are merged with client-level configuration. |
| |
| [0]: /advanced/clients/#merging-of-configuration |
| """ |
| if cookies is not None: |
| message = ( |
| "Setting per-request cookies=<...> is being deprecated, because " |
| "the expected behaviour on cookie persistence is ambiguous. Set " |
| "cookies directly on the client instance instead." |
| ) |
| warnings.warn(message, DeprecationWarning, stacklevel=2) |
|
|
| request = self.build_request( |
| method=method, |
| url=url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
| return self.send(request, auth=auth, follow_redirects=follow_redirects) |
|
|
| @contextmanager |
| def stream( |
| self, |
| method: str, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> typing.Iterator[Response]: |
| """ |
| Alternative to `httpx.request()` that streams the response body |
| instead of loading it into memory at once. |
| |
| **Parameters**: See `httpx.request`. |
| |
| See also: [Streaming Responses][0] |
| |
| [0]: /quickstart#streaming-responses |
| """ |
| request = self.build_request( |
| method=method, |
| url=url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
| response = self.send( |
| request=request, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| stream=True, |
| ) |
| try: |
| yield response |
| finally: |
| response.close() |
|
|
| def send( |
| self, |
| request: Request, |
| *, |
| stream: bool = False, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| ) -> Response: |
| """ |
| Send a request. |
| |
| The request is sent as-is, unmodified. |
| |
| Typically you'll want to build one with `Client.build_request()` |
| so that any client-level configuration is merged into the request, |
| but passing an explicit `httpx.Request()` is supported as well. |
| |
| See also: [Request instances][0] |
| |
| [0]: /advanced/clients/#request-instances |
| """ |
| if self._state == ClientState.CLOSED: |
| raise RuntimeError("Cannot send a request, as the client has been closed.") |
|
|
| self._state = ClientState.OPENED |
| follow_redirects = ( |
| self.follow_redirects |
| if isinstance(follow_redirects, UseClientDefault) |
| else follow_redirects |
| ) |
|
|
| self._set_timeout(request) |
|
|
| auth = self._build_request_auth(request, auth) |
|
|
| response = self._send_handling_auth( |
| request, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| history=[], |
| ) |
| try: |
| if not stream: |
| response.read() |
|
|
| return response |
|
|
| except BaseException as exc: |
| response.close() |
| raise exc |
|
|
| def _send_handling_auth( |
| self, |
| request: Request, |
| auth: Auth, |
| follow_redirects: bool, |
| history: list[Response], |
| ) -> Response: |
| auth_flow = auth.sync_auth_flow(request) |
| try: |
| request = next(auth_flow) |
|
|
| while True: |
| response = self._send_handling_redirects( |
| request, |
| follow_redirects=follow_redirects, |
| history=history, |
| ) |
| try: |
| try: |
| next_request = auth_flow.send(response) |
| except StopIteration: |
| return response |
|
|
| response.history = list(history) |
| response.read() |
| request = next_request |
| history.append(response) |
|
|
| except BaseException as exc: |
| response.close() |
| raise exc |
| finally: |
| auth_flow.close() |
|
|
| def _send_handling_redirects( |
| self, |
| request: Request, |
| follow_redirects: bool, |
| history: list[Response], |
| ) -> Response: |
| while True: |
| if len(history) > self.max_redirects: |
| raise TooManyRedirects( |
| "Exceeded maximum allowed redirects.", request=request |
| ) |
|
|
| for hook in self._event_hooks["request"]: |
| hook(request) |
|
|
| response = self._send_single_request(request) |
| try: |
| for hook in self._event_hooks["response"]: |
| hook(response) |
| response.history = list(history) |
|
|
| if not response.has_redirect_location: |
| return response |
|
|
| request = self._build_redirect_request(request, response) |
| history = history + [response] |
|
|
| if follow_redirects: |
| response.read() |
| else: |
| response.next_request = request |
| return response |
|
|
| except BaseException as exc: |
| response.close() |
| raise exc |
|
|
| def _send_single_request(self, request: Request) -> Response: |
| """ |
| Sends a single request, without handling any redirections. |
| """ |
| transport = self._transport_for_url(request.url) |
| start = time.perf_counter() |
|
|
| if not isinstance(request.stream, SyncByteStream): |
| raise RuntimeError( |
| "Attempted to send an async request with a sync Client instance." |
| ) |
|
|
| with request_context(request=request): |
| response = transport.handle_request(request) |
|
|
| assert isinstance(response.stream, SyncByteStream) |
|
|
| response.request = request |
| response.stream = BoundSyncStream( |
| response.stream, response=response, start=start |
| ) |
| self.cookies.extract_cookies(response) |
| response.default_encoding = self._default_encoding |
|
|
| logger.info( |
| 'HTTP Request: %s %s "%s %d %s"', |
| request.method, |
| request.url, |
| response.http_version, |
| response.status_code, |
| response.reason_phrase, |
| ) |
|
|
| return response |
|
|
| def get( |
| self, |
| url: URL | str, |
| *, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `GET` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return self.request( |
| "GET", |
| url, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| def options( |
| self, |
| url: URL | str, |
| *, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send an `OPTIONS` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return self.request( |
| "OPTIONS", |
| url, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| def head( |
| self, |
| url: URL | str, |
| *, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `HEAD` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return self.request( |
| "HEAD", |
| url, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| def post( |
| self, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `POST` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return self.request( |
| "POST", |
| url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| def put( |
| self, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `PUT` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return self.request( |
| "PUT", |
| url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| def patch( |
| self, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `PATCH` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return self.request( |
| "PATCH", |
| url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| def delete( |
| self, |
| url: URL | str, |
| *, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `DELETE` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return self.request( |
| "DELETE", |
| url, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| def close(self) -> None: |
| """ |
| Close transport and proxies. |
| """ |
| if self._state != ClientState.CLOSED: |
| self._state = ClientState.CLOSED |
|
|
| self._transport.close() |
| for transport in self._mounts.values(): |
| if transport is not None: |
| transport.close() |
|
|
| def __enter__(self: T) -> T: |
| if self._state != ClientState.UNOPENED: |
| msg = { |
| ClientState.OPENED: "Cannot open a client instance more than once.", |
| ClientState.CLOSED: ( |
| "Cannot reopen a client instance, once it has been closed." |
| ), |
| }[self._state] |
| raise RuntimeError(msg) |
|
|
| self._state = ClientState.OPENED |
|
|
| self._transport.__enter__() |
| for transport in self._mounts.values(): |
| if transport is not None: |
| transport.__enter__() |
| return self |
|
|
| def __exit__( |
| self, |
| exc_type: type[BaseException] | None = None, |
| exc_value: BaseException | None = None, |
| traceback: TracebackType | None = None, |
| ) -> None: |
| self._state = ClientState.CLOSED |
|
|
| self._transport.__exit__(exc_type, exc_value, traceback) |
| for transport in self._mounts.values(): |
| if transport is not None: |
| transport.__exit__(exc_type, exc_value, traceback) |
|
|
|
|
| class AsyncClient(BaseClient): |
| """ |
| An asynchronous HTTP client, with connection pooling, HTTP/2, redirects, |
| cookie persistence, etc. |
| |
| It can be shared between tasks. |
| |
| Usage: |
| |
| ```python |
| >>> async with httpx.AsyncClient() as client: |
| >>> response = await client.get('https://example.org') |
| ``` |
| |
| **Parameters:** |
| |
| * **auth** - *(optional)* An authentication class to use when sending |
| requests. |
| * **params** - *(optional)* Query parameters to include in request URLs, as |
| a string, dictionary, or sequence of two-tuples. |
| * **headers** - *(optional)* Dictionary of HTTP headers to include when |
| sending requests. |
| * **cookies** - *(optional)* Dictionary of Cookie items to include when |
| sending requests. |
| * **verify** - *(optional)* Either `True` to use an SSL context with the |
| default CA bundle, `False` to disable verification, or an instance of |
| `ssl.SSLContext` to use a custom context. |
| * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be |
| enabled. Defaults to `False`. |
| * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. |
| * **timeout** - *(optional)* The timeout configuration to use when sending |
| requests. |
| * **limits** - *(optional)* The limits configuration to use. |
| * **max_redirects** - *(optional)* The maximum number of redirect responses |
| that should be followed. |
| * **base_url** - *(optional)* A URL to use as the base when building |
| request URLs. |
| * **transport** - *(optional)* A transport class to use for sending requests |
| over the network. |
| * **trust_env** - *(optional)* Enables or disables usage of environment |
| variables for configuration. |
| * **default_encoding** - *(optional)* The default encoding to use for decoding |
| response text, if no charset information is included in a response Content-Type |
| header. Set to a callable for automatic character set detection. Default: "utf-8". |
| """ |
|
|
| def __init__( |
| self, |
| *, |
| auth: AuthTypes | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| verify: ssl.SSLContext | str | bool = True, |
| cert: CertTypes | None = None, |
| http1: bool = True, |
| http2: bool = False, |
| proxy: ProxyTypes | None = None, |
| mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None, |
| timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, |
| follow_redirects: bool = False, |
| limits: Limits = DEFAULT_LIMITS, |
| max_redirects: int = DEFAULT_MAX_REDIRECTS, |
| event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, |
| base_url: URL | str = "", |
| transport: AsyncBaseTransport | None = None, |
| trust_env: bool = True, |
| default_encoding: str | typing.Callable[[bytes], str] = "utf-8", |
| ) -> None: |
| super().__init__( |
| auth=auth, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| timeout=timeout, |
| follow_redirects=follow_redirects, |
| max_redirects=max_redirects, |
| event_hooks=event_hooks, |
| base_url=base_url, |
| trust_env=trust_env, |
| default_encoding=default_encoding, |
| ) |
|
|
| if http2: |
| try: |
| import h2 |
| except ImportError: |
| raise ImportError( |
| "Using http2=True, but the 'h2' package is not installed. " |
| "Make sure to install httpx using `pip install httpx[http2]`." |
| ) from None |
|
|
| allow_env_proxies = trust_env and transport is None |
| proxy_map = self._get_proxy_map(proxy, allow_env_proxies) |
|
|
| self._transport = self._init_transport( |
| verify=verify, |
| cert=cert, |
| trust_env=trust_env, |
| http1=http1, |
| http2=http2, |
| limits=limits, |
| transport=transport, |
| ) |
|
|
| self._mounts: dict[URLPattern, AsyncBaseTransport | None] = { |
| URLPattern(key): None |
| if proxy is None |
| else self._init_proxy_transport( |
| proxy, |
| verify=verify, |
| cert=cert, |
| trust_env=trust_env, |
| http1=http1, |
| http2=http2, |
| limits=limits, |
| ) |
| for key, proxy in proxy_map.items() |
| } |
| if mounts is not None: |
| self._mounts.update( |
| {URLPattern(key): transport for key, transport in mounts.items()} |
| ) |
| self._mounts = dict(sorted(self._mounts.items())) |
|
|
| def _init_transport( |
| self, |
| verify: ssl.SSLContext | str | bool = True, |
| cert: CertTypes | None = None, |
| trust_env: bool = True, |
| http1: bool = True, |
| http2: bool = False, |
| limits: Limits = DEFAULT_LIMITS, |
| transport: AsyncBaseTransport | None = None, |
| ) -> AsyncBaseTransport: |
| if transport is not None: |
| return transport |
|
|
| return AsyncHTTPTransport( |
| verify=verify, |
| cert=cert, |
| trust_env=trust_env, |
| http1=http1, |
| http2=http2, |
| limits=limits, |
| ) |
|
|
| def _init_proxy_transport( |
| self, |
| proxy: Proxy, |
| verify: ssl.SSLContext | str | bool = True, |
| cert: CertTypes | None = None, |
| trust_env: bool = True, |
| http1: bool = True, |
| http2: bool = False, |
| limits: Limits = DEFAULT_LIMITS, |
| ) -> AsyncBaseTransport: |
| return AsyncHTTPTransport( |
| verify=verify, |
| cert=cert, |
| trust_env=trust_env, |
| http1=http1, |
| http2=http2, |
| limits=limits, |
| proxy=proxy, |
| ) |
|
|
| def _transport_for_url(self, url: URL) -> AsyncBaseTransport: |
| """ |
| Returns the transport instance that should be used for a given URL. |
| This will either be the standard connection pool, or a proxy. |
| """ |
| for pattern, transport in self._mounts.items(): |
| if pattern.matches(url): |
| return self._transport if transport is None else transport |
|
|
| return self._transport |
|
|
| async def request( |
| self, |
| method: str, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Build and send a request. |
| |
| Equivalent to: |
| |
| ```python |
| request = client.build_request(...) |
| response = await client.send(request, ...) |
| ``` |
| |
| See `AsyncClient.build_request()`, `AsyncClient.send()` |
| and [Merging of configuration][0] for how the various parameters |
| are merged with client-level configuration. |
| |
| [0]: /advanced/clients/#merging-of-configuration |
| """ |
|
|
| if cookies is not None: |
| message = ( |
| "Setting per-request cookies=<...> is being deprecated, because " |
| "the expected behaviour on cookie persistence is ambiguous. Set " |
| "cookies directly on the client instance instead." |
| ) |
| warnings.warn(message, DeprecationWarning, stacklevel=2) |
|
|
| request = self.build_request( |
| method=method, |
| url=url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
| return await self.send(request, auth=auth, follow_redirects=follow_redirects) |
|
|
| @asynccontextmanager |
| async def stream( |
| self, |
| method: str, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> typing.AsyncIterator[Response]: |
| """ |
| Alternative to `httpx.request()` that streams the response body |
| instead of loading it into memory at once. |
| |
| **Parameters**: See `httpx.request`. |
| |
| See also: [Streaming Responses][0] |
| |
| [0]: /quickstart#streaming-responses |
| """ |
| request = self.build_request( |
| method=method, |
| url=url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
| response = await self.send( |
| request=request, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| stream=True, |
| ) |
| try: |
| yield response |
| finally: |
| await response.aclose() |
|
|
| async def send( |
| self, |
| request: Request, |
| *, |
| stream: bool = False, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| ) -> Response: |
| """ |
| Send a request. |
| |
| The request is sent as-is, unmodified. |
| |
| Typically you'll want to build one with `AsyncClient.build_request()` |
| so that any client-level configuration is merged into the request, |
| but passing an explicit `httpx.Request()` is supported as well. |
| |
| See also: [Request instances][0] |
| |
| [0]: /advanced/clients/#request-instances |
| """ |
| if self._state == ClientState.CLOSED: |
| raise RuntimeError("Cannot send a request, as the client has been closed.") |
|
|
| self._state = ClientState.OPENED |
| follow_redirects = ( |
| self.follow_redirects |
| if isinstance(follow_redirects, UseClientDefault) |
| else follow_redirects |
| ) |
|
|
| self._set_timeout(request) |
|
|
| auth = self._build_request_auth(request, auth) |
|
|
| response = await self._send_handling_auth( |
| request, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| history=[], |
| ) |
| try: |
| if not stream: |
| await response.aread() |
|
|
| return response |
|
|
| except BaseException as exc: |
| await response.aclose() |
| raise exc |
|
|
| async def _send_handling_auth( |
| self, |
| request: Request, |
| auth: Auth, |
| follow_redirects: bool, |
| history: list[Response], |
| ) -> Response: |
| auth_flow = auth.async_auth_flow(request) |
| try: |
| request = await auth_flow.__anext__() |
|
|
| while True: |
| response = await self._send_handling_redirects( |
| request, |
| follow_redirects=follow_redirects, |
| history=history, |
| ) |
| try: |
| try: |
| next_request = await auth_flow.asend(response) |
| except StopAsyncIteration: |
| return response |
|
|
| response.history = list(history) |
| await response.aread() |
| request = next_request |
| history.append(response) |
|
|
| except BaseException as exc: |
| await response.aclose() |
| raise exc |
| finally: |
| await auth_flow.aclose() |
|
|
| async def _send_handling_redirects( |
| self, |
| request: Request, |
| follow_redirects: bool, |
| history: list[Response], |
| ) -> Response: |
| while True: |
| if len(history) > self.max_redirects: |
| raise TooManyRedirects( |
| "Exceeded maximum allowed redirects.", request=request |
| ) |
|
|
| for hook in self._event_hooks["request"]: |
| await hook(request) |
|
|
| response = await self._send_single_request(request) |
| try: |
| for hook in self._event_hooks["response"]: |
| await hook(response) |
|
|
| response.history = list(history) |
|
|
| if not response.has_redirect_location: |
| return response |
|
|
| request = self._build_redirect_request(request, response) |
| history = history + [response] |
|
|
| if follow_redirects: |
| await response.aread() |
| else: |
| response.next_request = request |
| return response |
|
|
| except BaseException as exc: |
| await response.aclose() |
| raise exc |
|
|
| async def _send_single_request(self, request: Request) -> Response: |
| """ |
| Sends a single request, without handling any redirections. |
| """ |
| transport = self._transport_for_url(request.url) |
| start = time.perf_counter() |
|
|
| if not isinstance(request.stream, AsyncByteStream): |
| raise RuntimeError( |
| "Attempted to send an sync request with an AsyncClient instance." |
| ) |
|
|
| with request_context(request=request): |
| response = await transport.handle_async_request(request) |
|
|
| assert isinstance(response.stream, AsyncByteStream) |
| response.request = request |
| response.stream = BoundAsyncStream( |
| response.stream, response=response, start=start |
| ) |
| self.cookies.extract_cookies(response) |
| response.default_encoding = self._default_encoding |
|
|
| logger.info( |
| 'HTTP Request: %s %s "%s %d %s"', |
| request.method, |
| request.url, |
| response.http_version, |
| response.status_code, |
| response.reason_phrase, |
| ) |
|
|
| return response |
|
|
| async def get( |
| self, |
| url: URL | str, |
| *, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `GET` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return await self.request( |
| "GET", |
| url, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| async def options( |
| self, |
| url: URL | str, |
| *, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send an `OPTIONS` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return await self.request( |
| "OPTIONS", |
| url, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| async def head( |
| self, |
| url: URL | str, |
| *, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `HEAD` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return await self.request( |
| "HEAD", |
| url, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| async def post( |
| self, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `POST` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return await self.request( |
| "POST", |
| url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| async def put( |
| self, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `PUT` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return await self.request( |
| "PUT", |
| url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| async def patch( |
| self, |
| url: URL | str, |
| *, |
| content: RequestContent | None = None, |
| data: RequestData | None = None, |
| files: RequestFiles | None = None, |
| json: typing.Any | None = None, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `PATCH` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return await self.request( |
| "PATCH", |
| url, |
| content=content, |
| data=data, |
| files=files, |
| json=json, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| async def delete( |
| self, |
| url: URL | str, |
| *, |
| params: QueryParamTypes | None = None, |
| headers: HeaderTypes | None = None, |
| cookies: CookieTypes | None = None, |
| auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, |
| timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, |
| extensions: RequestExtensions | None = None, |
| ) -> Response: |
| """ |
| Send a `DELETE` request. |
| |
| **Parameters**: See `httpx.request`. |
| """ |
| return await self.request( |
| "DELETE", |
| url, |
| params=params, |
| headers=headers, |
| cookies=cookies, |
| auth=auth, |
| follow_redirects=follow_redirects, |
| timeout=timeout, |
| extensions=extensions, |
| ) |
|
|
| async def aclose(self) -> None: |
| """ |
| Close transport and proxies. |
| """ |
| if self._state != ClientState.CLOSED: |
| self._state = ClientState.CLOSED |
|
|
| await self._transport.aclose() |
| for proxy in self._mounts.values(): |
| if proxy is not None: |
| await proxy.aclose() |
|
|
| async def __aenter__(self: U) -> U: |
| if self._state != ClientState.UNOPENED: |
| msg = { |
| ClientState.OPENED: "Cannot open a client instance more than once.", |
| ClientState.CLOSED: ( |
| "Cannot reopen a client instance, once it has been closed." |
| ), |
| }[self._state] |
| raise RuntimeError(msg) |
|
|
| self._state = ClientState.OPENED |
|
|
| await self._transport.__aenter__() |
| for proxy in self._mounts.values(): |
| if proxy is not None: |
| await proxy.__aenter__() |
| return self |
|
|
| async def __aexit__( |
| self, |
| exc_type: type[BaseException] | None = None, |
| exc_value: BaseException | None = None, |
| traceback: TracebackType | None = None, |
| ) -> None: |
| self._state = ClientState.CLOSED |
|
|
| await self._transport.__aexit__(exc_type, exc_value, traceback) |
| for proxy in self._mounts.values(): |
| if proxy is not None: |
| await proxy.__aexit__(exc_type, exc_value, traceback) |
|
|