syshttp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
syshttp/autopatch.py ADDED
@@ -0,0 +1,382 @@
1
+ """Patch HTTP client libraries as they are imported, so they route and verify
2
+ the way syshttp does: routing by :func:`syshttp.proxy.proxy_map` through
3
+ proxylib's client integrations (``proxylib.patch``), and TLS by the system's
4
+ context (:func:`syshttp.tls.os_ssl_context`).
5
+
6
+ ``SYSHTTP_IMPORTPATCH`` chooses the libraries -- read under the prefixes as
7
+ every setting is (``SYSHTTP_PREFIX=AGENTS_``: ``AGENTS_SYSHTTP_IMPORTPATCH``): ``1``/``true``/``yes``/``on`` for all of
8
+ :data:`TARGETS`, ``0``/``false``/``no``/``off``, empty or unset for none, else
9
+ their names separated by ``:`` (``requests:httpx``; a module name such as
10
+ ``urllib.request`` works too). It is read when the library is imported:
11
+ importpatch (a dependency) calls :func:`importpatch` for each target, through
12
+ this distribution's ``importpatch`` entry points. :func:`patch` applies the
13
+ same patches from code, whatever the setting says.
14
+
15
+ What the caller chose always wins: a proxy given to the client, a context, a
16
+ CA bundle or ``verify=False``.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import functools
22
+ import importlib
23
+ import logging
24
+ import ssl
25
+ import sys
26
+ import threading
27
+ import time
28
+ from types import ModuleType
29
+ from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Tuple
30
+
31
+ # Neither proxylib nor syshttp.proxy is imported here: proxylib's own import
32
+ # of urllib.request runs this module's hook while proxylib is half built.
33
+ from .env import HTTPEnv
34
+
35
+ __all__ = ["SETTING", "TARGETS", "enabled", "importpatch", "patch", "patched", "setting_name", "unpatch"]
36
+
37
+ #: The setting, read under every prefix as any other is (:func:`setting_name`).
38
+ SETTING = "SYSHTTP_IMPORTPATCH"
39
+ #: The libraries that can be patched: name -> the module whose import patches it.
40
+ TARGETS: Dict[str, str] = {
41
+ "requests": "requests",
42
+ "urllib": "urllib.request",
43
+ "httpx": "httpx",
44
+ "aiohttp": "aiohttp",
45
+ "pip": "pip._vendor.requests",
46
+ }
47
+
48
+ _log = logging.getLogger(__name__)
49
+ _TRUE = ("1", "true", "yes", "on")
50
+ _FALSE = ("", "0", "false", "no", "off")
51
+ _BY_MODULE = {module: name for name, module in TARGETS.items()}
52
+ _lock = threading.RLock()
53
+ # name -> the functions that undo its patch, in the order applied.
54
+ _undo: Dict[str, List[Callable[[], None]]] = {}
55
+
56
+
57
+ def setting_name(*, env: Optional[HTTPEnv] = None) -> str:
58
+ """The variable that chooses the libraries: ``<PREFIX>SYSHTTP_IMPORTPATCH``
59
+ for the first prefix that sets it (``AGENTS_SYSHTTP_IMPORTPATCH`` under
60
+ ``SYSHTTP_PREFIX=AGENTS_``), else the name under the first prefix."""
61
+ env = HTTPEnv() if env is None else env
62
+ found = env.find(SETTING)
63
+ return found[0] if found else env.name(SETTING)
64
+
65
+
66
+ def enabled(*, env: Optional[HTTPEnv] = None) -> FrozenSet[str]:
67
+ """The names of :data:`TARGETS` that :func:`setting_name` enables, ``:``
68
+ separated. A name that is not a target is logged at WARNING and skipped."""
69
+ env = HTTPEnv() if env is None else env
70
+ name = setting_name(env=env)
71
+ raw = env.get(SETTING) or ""
72
+ if raw.lower() in _FALSE:
73
+ return frozenset()
74
+ if raw.lower() in _TRUE:
75
+ return frozenset(TARGETS)
76
+ names = set()
77
+ for part in raw.split(":"):
78
+ part = part.strip()
79
+ if not part:
80
+ continue
81
+ target = _BY_MODULE.get(part, part)
82
+ if target in TARGETS:
83
+ names.add(target)
84
+ else:
85
+ _log.warning("%s names %r, which is not one of %s", name, part, ", ".join(TARGETS))
86
+ return frozenset(names)
87
+
88
+
89
+ def importpatch(name: str, module: ModuleType) -> None:
90
+ """importpatch's entry point: patch ``module``, just imported as ``name``,
91
+ when :func:`setting_name` enables it."""
92
+ _flush()
93
+ target = _BY_MODULE.get(name)
94
+ if target is not None and target in enabled():
95
+ _apply(target, module)
96
+
97
+
98
+ def patch(targets: Optional[Iterable[str]] = None) -> Tuple[str, ...]:
99
+ """Patch ``targets`` now (default: every target whose library is
100
+ installed), importing each library. A name that is not a target is a
101
+ ``ValueError``; a named library that is not installed raises its
102
+ ``ImportError``. Returns the names patched; patching twice does nothing."""
103
+ names = list(TARGETS) if targets is None else [_BY_MODULE.get(t, t) for t in targets]
104
+ for name in names:
105
+ if name not in TARGETS:
106
+ raise ValueError("%r is not one of %s" % (name, ", ".join(TARGETS)))
107
+ _flush()
108
+ done = []
109
+ for name in names:
110
+ try:
111
+ module = importlib.import_module(TARGETS[name])
112
+ except ImportError:
113
+ if targets is not None:
114
+ raise
115
+ continue
116
+ _apply(name, module)
117
+ done.append(name)
118
+ return tuple(done)
119
+
120
+
121
+ def unpatch() -> None:
122
+ """Undo every patch, the last applied first. A client built while patched
123
+ keeps what it was given; proxylib's integrations stay installed, back on
124
+ proxylib's default map."""
125
+ with _lock:
126
+ for name in reversed(list(_undo)):
127
+ for step in reversed(_undo.pop(name)):
128
+ step()
129
+
130
+
131
+ def patched() -> Tuple[str, ...]:
132
+ """The names patched now, in the order applied."""
133
+ with _lock:
134
+ return tuple(_undo)
135
+
136
+
137
+ def _apply(name: str, module: ModuleType) -> None:
138
+ with _lock:
139
+ if name in _undo:
140
+ return
141
+ steps: List[Callable[[], None]] = []
142
+ if _initializing("proxylib") or _initializing("syshttp.proxy"):
143
+ steps.append(_defer(name, module))
144
+ else:
145
+ # Before the TLS patch: syshttp's wrappers then run first and
146
+ # hand proxylib's transports the system context.
147
+ steps.extend(_route(name))
148
+ tls = _TLS[name]
149
+ if tls is not None:
150
+ steps.append(tls(module))
151
+ _undo[name] = steps
152
+
153
+
154
+ def _initializing(name: str) -> bool:
155
+ """Whether module ``name`` is being imported right now (half built)."""
156
+ module = sys.modules.get(name)
157
+ return module is not None and bool(getattr(getattr(module, "__spec__", None), "_initializing", False))
158
+
159
+
160
+ def _context(certificate: bool = False) -> ssl.SSLContext:
161
+ """The system's context; a fresh one offering no store certificate when
162
+ the caller loads a certificate of its own into it."""
163
+ from . import tls as _tls
164
+
165
+ if certificate:
166
+ return _tls.new_os_context(client_certificates=False)
167
+ return _tls.os_ssl_context()
168
+
169
+
170
+ # ---- routing -----------------------------------------------------------------
171
+
172
+
173
+ def _route(name: str) -> List[Callable[[], None]]:
174
+ """``proxylib.patch`` with syshttp's map for target ``name``; nothing
175
+ when this proxylib has no such target."""
176
+ import proxylib
177
+
178
+ from . import proxy as _proxy
179
+
180
+ if name not in proxylib.patching.targets():
181
+ _log.debug("proxylib %s has no %r patch target: %s is not routed", proxylib.__version__, name, name)
182
+ return []
183
+ proxymap = _URLErrorMap(_proxy.proxy_map()) if name == "urllib" else _proxy.proxy_map()
184
+ return [proxylib.patch(proxymap, targets=[name]).undo]
185
+
186
+
187
+ # Targets whose routing waits for proxylib to finish importing -> their undo
188
+ # steps once routed (None until then).
189
+ _pending: Dict[str, Optional[List[Callable[[], None]]]] = {}
190
+
191
+
192
+ def _defer(name: str, module: ModuleType) -> Callable[[], None]:
193
+ """Route ``name`` once proxylib is importable: at the next patch, or for
194
+ urllib (the only client proxylib imports itself) at the first
195
+ ``urlopen``, the call proxylib's urllib integration serves."""
196
+ _pending[name] = None
197
+ undo_opener: Callable[[], None] = lambda: None
198
+ if name == "urllib":
199
+ original = getattr(module, "urlopen")
200
+
201
+ @functools.wraps(original)
202
+ def urlopen(*args: Any, **kwargs: Any) -> Any:
203
+ _flush()
204
+ return original(*args, **kwargs)
205
+
206
+ setattr(module, "urlopen", urlopen)
207
+ undo_opener = lambda: setattr(module, "urlopen", original) # noqa: E731
208
+
209
+ def undo() -> None:
210
+ undo_opener()
211
+ for step in reversed(_pending.pop(name, None) or []):
212
+ step()
213
+
214
+ return undo
215
+
216
+
217
+ def _flush() -> None:
218
+ """Route every deferred target, once proxylib can be used."""
219
+ if not _pending or _initializing("proxylib") or _initializing("syshttp.proxy"):
220
+ return
221
+ with _lock:
222
+ for name, steps in list(_pending.items()):
223
+ if steps is None:
224
+ _pending[name] = _route(name)
225
+
226
+
227
+ class _URLErrorMap:
228
+ """urllib sends through one proxy: the first reachable one (``Probed``),
229
+ with none reachable reported as urllib reports a connection failure."""
230
+
231
+ def __init__(self, inner: Any) -> None:
232
+ import proxylib
233
+
234
+ self.inner = proxylib.Probed(inner)
235
+
236
+ def __getitem__(self, url: str) -> Any:
237
+ from urllib.error import URLError
238
+
239
+ import proxylib
240
+
241
+ try:
242
+ return proxylib.lookup(self.inner, url)
243
+ except proxylib.NoReachableProxyError as exc:
244
+ raise URLError(exc) from exc
245
+
246
+
247
+ # ---- TLS: requests -----------------------------------------------------------
248
+
249
+
250
+ def _tls_requests(requests: ModuleType) -> Callable[[], None]:
251
+ """Every ``HTTPAdapter`` -- proxylib's included -- verifies an https
252
+ origin with the system's context when ``verify`` is ``True`` and the
253
+ adapter's pool manager was given no context of its own."""
254
+ adapter_class = requests.adapters.HTTPAdapter
255
+ original_key = getattr(adapter_class, "build_connection_pool_key_attributes", None)
256
+ if original_key is None:
257
+ _log.debug("requests %s has no pool-key hook: its own verification stays", requests.__version__)
258
+ return lambda: None
259
+ original_verify = adapter_class.cert_verify
260
+
261
+ def ours(adapter: Any, url: str, verify: Any) -> bool:
262
+ pool = getattr(adapter, "poolmanager", None)
263
+ own = getattr(pool, "connection_pool_kw", {}).get("ssl_context")
264
+ return verify is True and own is None and url.lower().startswith("https")
265
+
266
+ @functools.wraps(original_key)
267
+ def build_connection_pool_key_attributes(self: Any, request: Any, verify: Any, cert: Any = None) -> Any:
268
+ host_params, pool_kwargs = original_key(self, request, verify, cert)
269
+ # requests 2.32.3 puts its own default context (certifi) into the pool
270
+ # for verify=True; later versions leave the key out.
271
+ requests_default = getattr(requests.adapters, "_preloaded_ssl_context", None)
272
+ given = pool_kwargs.get("ssl_context")
273
+ if ours(self, request.url or "", verify) and (given is None or given is requests_default):
274
+ pool_kwargs["ssl_context"] = _context()
275
+ return host_params, pool_kwargs
276
+
277
+ @functools.wraps(original_verify)
278
+ def cert_verify(self: Any, conn: Any, url: str, verify: Any, cert: Any) -> None:
279
+ if ours(self, url, verify):
280
+ # No CA file: urllib3 would load it into the shared system context.
281
+ original_verify(self, conn, url, False, cert)
282
+ conn.cert_reqs = "CERT_REQUIRED"
283
+ return
284
+ original_verify(self, conn, url, verify, cert)
285
+
286
+ setattr(adapter_class, "build_connection_pool_key_attributes", build_connection_pool_key_attributes)
287
+ setattr(adapter_class, "cert_verify", cert_verify)
288
+
289
+ def undo() -> None:
290
+ setattr(adapter_class, "build_connection_pool_key_attributes", original_key)
291
+ setattr(adapter_class, "cert_verify", original_verify)
292
+
293
+ return undo
294
+
295
+
296
+ # ---- TLS: urllib -------------------------------------------------------------
297
+
298
+ # One context for http.client's default: it sets ALPN on the context it is
299
+ # handed, so it does not share the requests session's.
300
+ _https_context: "Optional[ssl.SSLContext]" = None
301
+ _https_context_at = 0.0
302
+
303
+
304
+ def _default_https_context() -> ssl.SSLContext:
305
+ global _https_context, _https_context_at
306
+ from . import tls as _tls
307
+
308
+ with _lock:
309
+ now = time.monotonic()
310
+ if _https_context is None or now - _https_context_at > _tls.OS_CONTEXT_MAX_AGE:
311
+ _https_context = _tls.new_os_context()
312
+ _https_context_at = now
313
+ return _https_context
314
+
315
+
316
+ def _tls_urllib(request: ModuleType) -> Callable[[], None]:
317
+ """``http.client``'s default HTTPS context (``ssl._create_default_https_context``)
318
+ becomes the system's, for urllib and every other caller that gives none."""
319
+ original = ssl._create_default_https_context # type: ignore[attr-defined]
320
+ setattr(ssl, "_create_default_https_context", _default_https_context)
321
+ return lambda: setattr(ssl, "_create_default_https_context", original)
322
+
323
+
324
+ # ---- TLS: httpx --------------------------------------------------------------
325
+
326
+
327
+ def _tls_httpx(httpx: ModuleType) -> Callable[[], None]:
328
+ """``httpx.Client`` / ``AsyncClient`` built with ``verify`` left at
329
+ ``True`` verify with the system's context (a fresh one offering no store
330
+ certificate when ``cert=`` is given)."""
331
+
332
+ def wrap(client_class: Any) -> Callable[[], None]:
333
+ original = client_class.__init__
334
+
335
+ @functools.wraps(original)
336
+ def __init__(self: Any, *args: Any, **kwargs: Any) -> None:
337
+ if kwargs.get("verify", True) is True:
338
+ kwargs["verify"] = _context(certificate=kwargs.get("cert") is not None)
339
+ original(self, *args, **kwargs)
340
+
341
+ client_class.__init__ = __init__
342
+ return lambda: setattr(client_class, "__init__", original)
343
+
344
+ steps = [wrap(httpx.Client), wrap(httpx.AsyncClient)]
345
+
346
+ def undo() -> None:
347
+ for step in reversed(steps):
348
+ step()
349
+
350
+ return undo
351
+
352
+
353
+ # ---- TLS: aiohttp ------------------------------------------------------------
354
+
355
+
356
+ def _tls_aiohttp(aiohttp: ModuleType) -> Callable[[], None]:
357
+ """``ClientSession`` requests with ``ssl`` left at ``True`` and no
358
+ ``ssl_context`` / ``verify_ssl`` / ``fingerprint`` verify with the
359
+ system's context."""
360
+ session_class = aiohttp.ClientSession
361
+ original = session_class._request
362
+
363
+ @functools.wraps(original)
364
+ async def _request(self: Any, method: str, str_or_url: Any, **kwargs: Any) -> Any:
365
+ if kwargs.get("ssl", True) is True and all(
366
+ kwargs.get(k) is None for k in ("ssl_context", "verify_ssl", "fingerprint")
367
+ ):
368
+ kwargs["ssl"] = _context()
369
+ return await original(self, method, str_or_url, **kwargs)
370
+
371
+ session_class._request = _request
372
+ return lambda: setattr(session_class, "_request", original)
373
+
374
+
375
+ _TLS: Dict[str, Optional[Callable[[ModuleType], Callable[[], None]]]] = {
376
+ "requests": _tls_requests,
377
+ "urllib": _tls_urllib,
378
+ "httpx": _tls_httpx,
379
+ "aiohttp": _tls_aiohttp,
380
+ # pip verifies with its own truststore context, which is kept.
381
+ "pip": None,
382
+ }
@@ -0,0 +1,70 @@
1
+ """An HTTP client on the standard library, with a replaceable transport.
2
+
3
+ `HTTPClient` sends a request through a transport (an object with `send()` and
4
+ `close()`: the default `HTTPTransport` on `http.client`, or one written
5
+ against the `Transport` protocol) and follows redirects under one rule for
6
+ credentials: `Authorization`, `Cookie` and `Proxy-Authorization` go to the
7
+ origin they were given for and to no other scheme, host or port. A reply comes
8
+ back as an `HTTPResponse`.
9
+
10
+ Importing this package loads `http.client` and `ssl`. Nothing here needs a
11
+ third-party library; `requests` in particular is not used.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys as _sys
17
+ import types as _types
18
+
19
+ from . import _settings
20
+ from ._client import DEFAULT_TIMEOUT, HTTPClient
21
+ from ._context import CA_BUNDLE
22
+ from ._default import DEFAULT_TRANSPORT
23
+ from ._fetch import fetch
24
+ from ._follow import is_redirect, split_userinfo
25
+ from ._headers import Headers, peer_text
26
+ from ._prepare import PreparedRequest, prepare_request
27
+ from ._response import HTTPResponse
28
+ from ._state import COOKIE_HOSTS
29
+ from ._status import HTTPStatusError, drop_body, json_body, raise_for_status, read_limited
30
+ from ._transport import HTTPTransport, Transport
31
+
32
+ __all__ = [
33
+ "CA_BUNDLE",
34
+ "COOKIE_HOSTS",
35
+ "DEFAULT_TIMEOUT",
36
+ "DEFAULT_TRANSPORT",
37
+ "HTTPClient",
38
+ "HTTPResponse",
39
+ "HTTPStatusError",
40
+ "HTTPTransport",
41
+ "Headers",
42
+ "MAX_REPLY_BYTES",
43
+ "PreparedRequest",
44
+ "Transport",
45
+ "drop_body",
46
+ "fetch",
47
+ "is_redirect",
48
+ "json_body",
49
+ "peer_text",
50
+ "prepare_request",
51
+ "raise_for_status",
52
+ "read_limited",
53
+ "split_userinfo",
54
+ ]
55
+
56
+
57
+ class _Module(_types.ModuleType):
58
+ """The module, with `MAX_REPLY_BYTES` read from and written to where it
59
+ takes effect."""
60
+
61
+ @property
62
+ def MAX_REPLY_BYTES(self) -> int:
63
+ return _settings.MAX_REPLY_BYTES
64
+
65
+ @MAX_REPLY_BYTES.setter
66
+ def MAX_REPLY_BYTES(self, value: int) -> None:
67
+ _settings.MAX_REPLY_BYTES = value
68
+
69
+
70
+ _sys.modules[__name__].__class__ = _Module
@@ -0,0 +1,156 @@
1
+ """How a request body is framed: the length it is sent under, and a body that
2
+ sends exactly that many bytes."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import errno as _errno
7
+ import os as _os
8
+ import stat as _stat
9
+ import typing as _ty
10
+
11
+ _STEP = 65536
12
+
13
+
14
+ def body_length(body) -> "int | None":
15
+ """The bytes `body` will send from now on, when that is known without
16
+ reading it, else `None`: a bytes-like object, a list or tuple of bytes, a
17
+ file that is seekable (from `tell()` to its end, the position restored) or
18
+ is a regular file (its size less `tell()`). An iterator, a generator and a
19
+ stream that cannot report its size have no length."""
20
+ if body is None:
21
+ return 0
22
+ if isinstance(body, (bytes, bytearray, memoryview)):
23
+ return memoryview(body).nbytes
24
+ if isinstance(body, (list, tuple)):
25
+ if all(isinstance(piece, (bytes, bytearray)) for piece in body):
26
+ return sum(len(piece) for piece in body)
27
+ return None
28
+ if hasattr(body, "read"):
29
+ return _file_length(body)
30
+ return None
31
+
32
+
33
+ def _file_length(file) -> "int | None":
34
+ try:
35
+ seekable = file.seekable()
36
+ except (AttributeError, ValueError, OSError):
37
+ seekable = False
38
+ if seekable:
39
+ here = file.tell()
40
+ try:
41
+ end = file.seek(0, 2)
42
+ if end is None:
43
+ end = file.tell()
44
+ finally:
45
+ file.seek(here)
46
+ return max(0, end - here)
47
+ try:
48
+ info = _os.fstat(file.fileno())
49
+ if _stat.S_ISREG(info.st_mode):
50
+ return max(0, info.st_size - file.tell())
51
+ except (AttributeError, ValueError, OSError):
52
+ pass
53
+ return None
54
+
55
+
56
+ def declared_length(pairs: "_ty.Iterable[tuple[str, str]]") -> "int | None":
57
+ """The `Content-Length` the caller set, or `None`; `ValueError` for one
58
+ that is repeated or is not a count of bytes."""
59
+ found = [value for name, value in pairs if name.lower() == "content-length"]
60
+ if not found:
61
+ return None
62
+ value = found[0].strip()
63
+ if len(found) > 1 or not (value.isascii() and value.isdigit()):
64
+ raise ValueError("the Content-Length header is not one count of bytes")
65
+ return int(value)
66
+
67
+
68
+ class Sized:
69
+ """A body sent under a `Content-Length`: exactly `length` bytes of `source`
70
+ (a binary file or an iterable of bytes). It never reads or sends more, so a
71
+ file that grew since it was measured cannot overrun the length, and a
72
+ source that ends first is `OSError(EIO)`, not a shorter upload."""
73
+
74
+ def __init__(self, source, length: int):
75
+ self._length = length
76
+ self._left = length
77
+ if hasattr(source, "read"):
78
+ self._read = source.read
79
+ else:
80
+ self._pieces = iter(source)
81
+ self._pending = b""
82
+ self._read = self._read_pieces
83
+
84
+ def __len__(self) -> int:
85
+ return self._length
86
+
87
+ def __iter__(self) -> "_ty.Iterator[bytes]":
88
+ while True:
89
+ chunk = self.read(_STEP)
90
+ if not chunk:
91
+ return
92
+ yield chunk
93
+
94
+ def read(self, amount: "int | None" = -1) -> bytes:
95
+ if self._left <= 0:
96
+ return b""
97
+ want = self._left if amount is None or amount < 0 else min(amount, self._left)
98
+ chunk = self._read(want)
99
+ if isinstance(chunk, str):
100
+ raise TypeError("the request body is bytes, not text")
101
+ if not chunk:
102
+ raise OSError(_errno.EIO, "the request body ended before its Content-Length")
103
+ chunk = bytes(chunk[:want])
104
+ self._left -= len(chunk)
105
+ return chunk
106
+
107
+ def _read_pieces(self, want: int) -> bytes:
108
+ while not self._pending:
109
+ try:
110
+ self._pending = bytes(next(self._pieces))
111
+ except StopIteration:
112
+ return b""
113
+ chunk, self._pending = self._pending[:want], self._pending[want:]
114
+ return chunk
115
+
116
+
117
+ def _chunks(file) -> "_ty.Iterator[bytes]":
118
+ while True:
119
+ chunk = file.read(_STEP)
120
+ if not chunk:
121
+ return
122
+ yield chunk
123
+
124
+
125
+ def prepare_body(body, pairs: "list[tuple[str, str]]"):
126
+ """`(value, length)`: what to hand the HTTP layer as the body, and the
127
+ `Content-Length` to add (`None` when the request already names one, the
128
+ layer adds its own, or none is needed). A length the caller set is
129
+ trusted, so nothing is chunked and exactly that many bytes are sent;
130
+ otherwise a body whose length is known is sent under it and any other is
131
+ left for the layer to send chunked."""
132
+ if body is None:
133
+ return None, None
134
+ declared = declared_length(pairs)
135
+ known = body_length(body)
136
+ if isinstance(body, (bytes, bytearray, memoryview)):
137
+ if declared is None or declared == known:
138
+ return body, None
139
+ if known is not None and declared > known:
140
+ raise ValueError("the Content-Length header is larger than the body")
141
+ return bytes(memoryview(body)[:declared]), None
142
+ length: "int | None"
143
+ add: "int | None"
144
+ if declared is not None:
145
+ if known is not None and declared > known:
146
+ raise ValueError("the Content-Length header is larger than the body")
147
+ length, add = declared, None
148
+ elif any(name.lower() == "transfer-encoding" for name, _ in pairs):
149
+ return body, None # the caller frames it
150
+ else:
151
+ length, add = known, known
152
+ if length is None:
153
+ return (_chunks(body) if hasattr(body, "read") else body), None
154
+ if length == 0:
155
+ return b"", add
156
+ return Sized(body, length), add