aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/httpcore/_models.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/httpcore/_models.py')
-rw-r--r--.venv/lib/python3.12/site-packages/httpcore/_models.py516
1 files changed, 516 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/httpcore/_models.py b/.venv/lib/python3.12/site-packages/httpcore/_models.py
new file mode 100644
index 00000000..8a65f133
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/httpcore/_models.py
@@ -0,0 +1,516 @@
+from __future__ import annotations
+
+import base64
+import ssl
+import typing
+import urllib.parse
+
+# Functions for typechecking...
+
+
+ByteOrStr = typing.Union[bytes, str]
+HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
+HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
+HeaderTypes = typing.Union[HeadersAsSequence, HeadersAsMapping, None]
+
+Extensions = typing.MutableMapping[str, typing.Any]
+
+
+def enforce_bytes(value: bytes | str, *, name: str) -> bytes:
+ """
+ Any arguments that are ultimately represented as bytes can be specified
+ either as bytes or as strings.
+
+ However we enforce that any string arguments must only contain characters in
+ the plain ASCII range. chr(0)...chr(127). If you need to use characters
+ outside that range then be precise, and use a byte-wise argument.
+ """
+ if isinstance(value, str):
+ try:
+ return value.encode("ascii")
+ except UnicodeEncodeError:
+ raise TypeError(f"{name} strings may not include unicode characters.")
+ elif isinstance(value, bytes):
+ return value
+
+ seen_type = type(value).__name__
+ raise TypeError(f"{name} must be bytes or str, but got {seen_type}.")
+
+
+def enforce_url(value: URL | bytes | str, *, name: str) -> URL:
+ """
+ Type check for URL parameters.
+ """
+ if isinstance(value, (bytes, str)):
+ return URL(value)
+ elif isinstance(value, URL):
+ return value
+
+ seen_type = type(value).__name__
+ raise TypeError(f"{name} must be a URL, bytes, or str, but got {seen_type}.")
+
+
+def enforce_headers(
+ value: HeadersAsMapping | HeadersAsSequence | None = None, *, name: str
+) -> list[tuple[bytes, bytes]]:
+ """
+ Convienence function that ensure all items in request or response headers
+ are either bytes or strings in the plain ASCII range.
+ """
+ if value is None:
+ return []
+ elif isinstance(value, typing.Mapping):
+ return [
+ (
+ enforce_bytes(k, name="header name"),
+ enforce_bytes(v, name="header value"),
+ )
+ for k, v in value.items()
+ ]
+ elif isinstance(value, typing.Sequence):
+ return [
+ (
+ enforce_bytes(k, name="header name"),
+ enforce_bytes(v, name="header value"),
+ )
+ for k, v in value
+ ]
+
+ seen_type = type(value).__name__
+ raise TypeError(
+ f"{name} must be a mapping or sequence of two-tuples, but got {seen_type}."
+ )
+
+
+def enforce_stream(
+ value: bytes | typing.Iterable[bytes] | typing.AsyncIterable[bytes] | None,
+ *,
+ name: str,
+) -> typing.Iterable[bytes] | typing.AsyncIterable[bytes]:
+ if value is None:
+ return ByteStream(b"")
+ elif isinstance(value, bytes):
+ return ByteStream(value)
+ return value
+
+
+# * https://tools.ietf.org/html/rfc3986#section-3.2.3
+# * https://url.spec.whatwg.org/#url-miscellaneous
+# * https://url.spec.whatwg.org/#scheme-state
+DEFAULT_PORTS = {
+ b"ftp": 21,
+ b"http": 80,
+ b"https": 443,
+ b"ws": 80,
+ b"wss": 443,
+}
+
+
+def include_request_headers(
+ headers: list[tuple[bytes, bytes]],
+ *,
+ url: "URL",
+ content: None | bytes | typing.Iterable[bytes] | typing.AsyncIterable[bytes],
+) -> list[tuple[bytes, bytes]]:
+ headers_set = set(k.lower() for k, v in headers)
+
+ if b"host" not in headers_set:
+ default_port = DEFAULT_PORTS.get(url.scheme)
+ if url.port is None or url.port == default_port:
+ header_value = url.host
+ else:
+ header_value = b"%b:%d" % (url.host, url.port)
+ headers = [(b"Host", header_value)] + headers
+
+ if (
+ content is not None
+ and b"content-length" not in headers_set
+ and b"transfer-encoding" not in headers_set
+ ):
+ if isinstance(content, bytes):
+ content_length = str(len(content)).encode("ascii")
+ headers += [(b"Content-Length", content_length)]
+ else:
+ headers += [(b"Transfer-Encoding", b"chunked")] # pragma: nocover
+
+ return headers
+
+
+# Interfaces for byte streams...
+
+
+class ByteStream:
+ """
+ A container for non-streaming content, and that supports both sync and async
+ stream iteration.
+ """
+
+ def __init__(self, content: bytes) -> None:
+ self._content = content
+
+ def __iter__(self) -> typing.Iterator[bytes]:
+ yield self._content
+
+ async def __aiter__(self) -> typing.AsyncIterator[bytes]:
+ yield self._content
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} [{len(self._content)} bytes]>"
+
+
+class Origin:
+ def __init__(self, scheme: bytes, host: bytes, port: int) -> None:
+ self.scheme = scheme
+ self.host = host
+ self.port = port
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return (
+ isinstance(other, Origin)
+ and self.scheme == other.scheme
+ and self.host == other.host
+ and self.port == other.port
+ )
+
+ def __str__(self) -> str:
+ scheme = self.scheme.decode("ascii")
+ host = self.host.decode("ascii")
+ port = str(self.port)
+ return f"{scheme}://{host}:{port}"
+
+
+class URL:
+ """
+ Represents the URL against which an HTTP request may be made.
+
+ The URL may either be specified as a plain string, for convienence:
+
+ ```python
+ url = httpcore.URL("https://www.example.com/")
+ ```
+
+ Or be constructed with explicitily pre-parsed components:
+
+ ```python
+ url = httpcore.URL(scheme=b'https', host=b'www.example.com', port=None, target=b'/')
+ ```
+
+ Using this second more explicit style allows integrations that are using
+ `httpcore` to pass through URLs that have already been parsed in order to use
+ libraries such as `rfc-3986` rather than relying on the stdlib. It also ensures
+ that URL parsing is treated identically at both the networking level and at any
+ higher layers of abstraction.
+
+ The four components are important here, as they allow the URL to be precisely
+ specified in a pre-parsed format. They also allow certain types of request to
+ be created that could not otherwise be expressed.
+
+ For example, an HTTP request to `http://www.example.com/` forwarded via a proxy
+ at `http://localhost:8080`...
+
+ ```python
+ # Constructs an HTTP request with a complete URL as the target:
+ # GET https://www.example.com/ HTTP/1.1
+ url = httpcore.URL(
+ scheme=b'http',
+ host=b'localhost',
+ port=8080,
+ target=b'https://www.example.com/'
+ )
+ request = httpcore.Request(
+ method="GET",
+ url=url
+ )
+ ```
+
+ Another example is constructing an `OPTIONS *` request...
+
+ ```python
+ # Constructs an 'OPTIONS *' HTTP request:
+ # OPTIONS * HTTP/1.1
+ url = httpcore.URL(scheme=b'https', host=b'www.example.com', target=b'*')
+ request = httpcore.Request(method="OPTIONS", url=url)
+ ```
+
+ This kind of request is not possible to formulate with a URL string,
+ because the `/` delimiter is always used to demark the target from the
+ host/port portion of the URL.
+
+ For convenience, string-like arguments may be specified either as strings or
+ as bytes. However, once a request is being issue over-the-wire, the URL
+ components are always ultimately required to be a bytewise representation.
+
+ In order to avoid any ambiguity over character encodings, when strings are used
+ as arguments, they must be strictly limited to the ASCII range `chr(0)`-`chr(127)`.
+ If you require a bytewise representation that is outside this range you must
+ handle the character encoding directly, and pass a bytes instance.
+ """
+
+ def __init__(
+ self,
+ url: bytes | str = "",
+ *,
+ scheme: bytes | str = b"",
+ host: bytes | str = b"",
+ port: int | None = None,
+ target: bytes | str = b"",
+ ) -> None:
+ """
+ Parameters:
+ url: The complete URL as a string or bytes.
+ scheme: The URL scheme as a string or bytes.
+ Typically either `"http"` or `"https"`.
+ host: The URL host as a string or bytes. Such as `"www.example.com"`.
+ port: The port to connect to. Either an integer or `None`.
+ target: The target of the HTTP request. Such as `"/items?search=red"`.
+ """
+ if url:
+ parsed = urllib.parse.urlparse(enforce_bytes(url, name="url"))
+ self.scheme = parsed.scheme
+ self.host = parsed.hostname or b""
+ self.port = parsed.port
+ self.target = (parsed.path or b"/") + (
+ b"?" + parsed.query if parsed.query else b""
+ )
+ else:
+ self.scheme = enforce_bytes(scheme, name="scheme")
+ self.host = enforce_bytes(host, name="host")
+ self.port = port
+ self.target = enforce_bytes(target, name="target")
+
+ @property
+ def origin(self) -> Origin:
+ default_port = {
+ b"http": 80,
+ b"https": 443,
+ b"ws": 80,
+ b"wss": 443,
+ b"socks5": 1080,
+ b"socks5h": 1080,
+ }[self.scheme]
+ return Origin(
+ scheme=self.scheme, host=self.host, port=self.port or default_port
+ )
+
+ def __eq__(self, other: typing.Any) -> bool:
+ return (
+ isinstance(other, URL)
+ and other.scheme == self.scheme
+ and other.host == self.host
+ and other.port == self.port
+ and other.target == self.target
+ )
+
+ def __bytes__(self) -> bytes:
+ if self.port is None:
+ return b"%b://%b%b" % (self.scheme, self.host, self.target)
+ return b"%b://%b:%d%b" % (self.scheme, self.host, self.port, self.target)
+
+ def __repr__(self) -> str:
+ return (
+ f"{self.__class__.__name__}(scheme={self.scheme!r}, "
+ f"host={self.host!r}, port={self.port!r}, target={self.target!r})"
+ )
+
+
+class Request:
+ """
+ An HTTP request.
+ """
+
+ def __init__(
+ self,
+ method: bytes | str,
+ url: URL | bytes | str,
+ *,
+ headers: HeaderTypes = None,
+ content: bytes
+ | typing.Iterable[bytes]
+ | typing.AsyncIterable[bytes]
+ | None = None,
+ extensions: Extensions | None = None,
+ ) -> None:
+ """
+ Parameters:
+ method: The HTTP request method, either as a string or bytes.
+ For example: `GET`.
+ url: The request URL, either as a `URL` instance, or as a string or bytes.
+ For example: `"https://www.example.com".`
+ headers: The HTTP request headers.
+ content: The content of the request body.
+ extensions: A dictionary of optional extra information included on
+ the request. Possible keys include `"timeout"`, and `"trace"`.
+ """
+ self.method: bytes = enforce_bytes(method, name="method")
+ self.url: URL = enforce_url(url, name="url")
+ self.headers: list[tuple[bytes, bytes]] = enforce_headers(
+ headers, name="headers"
+ )
+ self.stream: typing.Iterable[bytes] | typing.AsyncIterable[bytes] = (
+ enforce_stream(content, name="content")
+ )
+ self.extensions = {} if extensions is None else extensions
+
+ if "target" in self.extensions:
+ self.url = URL(
+ scheme=self.url.scheme,
+ host=self.url.host,
+ port=self.url.port,
+ target=self.extensions["target"],
+ )
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} [{self.method!r}]>"
+
+
+class Response:
+ """
+ An HTTP response.
+ """
+
+ def __init__(
+ self,
+ status: int,
+ *,
+ headers: HeaderTypes = None,
+ content: bytes
+ | typing.Iterable[bytes]
+ | typing.AsyncIterable[bytes]
+ | None = None,
+ extensions: Extensions | None = None,
+ ) -> None:
+ """
+ Parameters:
+ status: The HTTP status code of the response. For example `200`.
+ headers: The HTTP response headers.
+ content: The content of the response body.
+ extensions: A dictionary of optional extra information included on
+ the responseself.Possible keys include `"http_version"`,
+ `"reason_phrase"`, and `"network_stream"`.
+ """
+ self.status: int = status
+ self.headers: list[tuple[bytes, bytes]] = enforce_headers(
+ headers, name="headers"
+ )
+ self.stream: typing.Iterable[bytes] | typing.AsyncIterable[bytes] = (
+ enforce_stream(content, name="content")
+ )
+ self.extensions = {} if extensions is None else extensions
+
+ self._stream_consumed = False
+
+ @property
+ def content(self) -> bytes:
+ if not hasattr(self, "_content"):
+ if isinstance(self.stream, typing.Iterable):
+ raise RuntimeError(
+ "Attempted to access 'response.content' on a streaming response. "
+ "Call 'response.read()' first."
+ )
+ else:
+ raise RuntimeError(
+ "Attempted to access 'response.content' on a streaming response. "
+ "Call 'await response.aread()' first."
+ )
+ return self._content
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} [{self.status}]>"
+
+ # Sync interface...
+
+ def read(self) -> bytes:
+ if not isinstance(self.stream, typing.Iterable): # pragma: nocover
+ raise RuntimeError(
+ "Attempted to read an asynchronous response using 'response.read()'. "
+ "You should use 'await response.aread()' instead."
+ )
+ if not hasattr(self, "_content"):
+ self._content = b"".join([part for part in self.iter_stream()])
+ return self._content
+
+ def iter_stream(self) -> typing.Iterator[bytes]:
+ if not isinstance(self.stream, typing.Iterable): # pragma: nocover
+ raise RuntimeError(
+ "Attempted to stream an asynchronous response using 'for ... in "
+ "response.iter_stream()'. "
+ "You should use 'async for ... in response.aiter_stream()' instead."
+ )
+ if self._stream_consumed:
+ raise RuntimeError(
+ "Attempted to call 'for ... in response.iter_stream()' more than once."
+ )
+ self._stream_consumed = True
+ for chunk in self.stream:
+ yield chunk
+
+ def close(self) -> None:
+ if not isinstance(self.stream, typing.Iterable): # pragma: nocover
+ raise RuntimeError(
+ "Attempted to close an asynchronous response using 'response.close()'. "
+ "You should use 'await response.aclose()' instead."
+ )
+ if hasattr(self.stream, "close"):
+ self.stream.close()
+
+ # Async interface...
+
+ async def aread(self) -> bytes:
+ if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover
+ raise RuntimeError(
+ "Attempted to read an synchronous response using "
+ "'await response.aread()'. "
+ "You should use 'response.read()' instead."
+ )
+ if not hasattr(self, "_content"):
+ self._content = b"".join([part async for part in self.aiter_stream()])
+ return self._content
+
+ async def aiter_stream(self) -> typing.AsyncIterator[bytes]:
+ if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover
+ raise RuntimeError(
+ "Attempted to stream an synchronous response using 'async for ... in "
+ "response.aiter_stream()'. "
+ "You should use 'for ... in response.iter_stream()' instead."
+ )
+ if self._stream_consumed:
+ raise RuntimeError(
+ "Attempted to call 'async for ... in response.aiter_stream()' "
+ "more than once."
+ )
+ self._stream_consumed = True
+ async for chunk in self.stream:
+ yield chunk
+
+ async def aclose(self) -> None:
+ if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover
+ raise RuntimeError(
+ "Attempted to close a synchronous response using "
+ "'await response.aclose()'. "
+ "You should use 'response.close()' instead."
+ )
+ if hasattr(self.stream, "aclose"):
+ await self.stream.aclose()
+
+
+class Proxy:
+ def __init__(
+ self,
+ url: URL | bytes | str,
+ auth: tuple[bytes | str, bytes | str] | None = None,
+ headers: HeadersAsMapping | HeadersAsSequence | None = None,
+ ssl_context: ssl.SSLContext | None = None,
+ ):
+ self.url = enforce_url(url, name="url")
+ self.headers = enforce_headers(headers, name="headers")
+ self.ssl_context = ssl_context
+
+ if auth is not None:
+ username = enforce_bytes(auth[0], name="auth")
+ password = enforce_bytes(auth[1], name="auth")
+ userpass = username + b":" + password
+ authorization = b"Basic " + base64.b64encode(userpass)
+ self.auth: tuple[bytes, bytes] | None = (username, password)
+ self.headers = [(b"Proxy-Authorization", authorization)] + self.headers
+ else:
+ self.auth = None