without-http 0.0.4__tar.gz → 0.0.5__tar.gz
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.
- {without_http-0.0.4 → without_http-0.0.5}/PKG-INFO +4 -4
- {without_http-0.0.4 → without_http-0.0.5}/README.md +1 -1
- {without_http-0.0.4 → without_http-0.0.5}/pyproject.toml +3 -3
- {without_http-0.0.4 → without_http-0.0.5}/pyproject.toml.orig +3 -3
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/__init__.py +10 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/client.py +58 -58
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/h11_wire.py +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/h2_wire.py +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/lifespan.py +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/py.typed +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/server.py +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/socket_options.py +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/testing.py +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/timeouts.py +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/tls.py +0 -0
- {without_http-0.0.4 → without_http-0.0.5}/src/without_http/ws_wire.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: without-http
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: A sans-IO-backed ASGI server and HTTP client for without: h11/h2/wsproto over asyncio sockets.
|
|
5
5
|
Author: Josh Karpel
|
|
6
6
|
Author-email: Josh Karpel <josh.karpel@gmail.com>
|
|
@@ -16,8 +16,8 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
16
16
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
|
-
Requires-Dist: without-core==0.0.
|
|
20
|
-
Requires-Dist: without-asgi==0.0.
|
|
19
|
+
Requires-Dist: without-core==0.0.5
|
|
20
|
+
Requires-Dist: without-asgi==0.0.5
|
|
21
21
|
Requires-Dist: h11>=0.16
|
|
22
22
|
Requires-Dist: h2>=4.1
|
|
23
23
|
Requires-Dist: wsproto>=1.2
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
# without-http
|
|
30
30
|
|
|
31
31
|
A sans-IO-backed ASGI **server** and **HTTP client** for `without`. Where
|
|
32
|
-
[`without-asgi`](../
|
|
32
|
+
[`without-asgi`](../without_asgi) is the *app* side of the ASGI boundary (it turns
|
|
33
33
|
a server's `receive`/`send` into typed streams), `without-http` is the *server*
|
|
34
34
|
side: it owns the socket and the HTTP wire protocol, and drives any ASGI app via
|
|
35
35
|
`app(scope, receive, send)`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# without-http
|
|
2
2
|
|
|
3
3
|
A sans-IO-backed ASGI **server** and **HTTP client** for `without`. Where
|
|
4
|
-
[`without-asgi`](../
|
|
4
|
+
[`without-asgi`](../without_asgi) is the *app* side of the ASGI boundary (it turns
|
|
5
5
|
a server's `receive`/`send` into typed streams), `without-http` is the *server*
|
|
6
6
|
side: it owns the socket and the HTTP wire protocol, and drives any ASGI app via
|
|
7
7
|
`app(scope, receive, send)`.
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "without-http"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.5"
|
|
8
8
|
description = "A sans-IO-backed ASGI server and HTTP client for without: h11/h2/wsproto over asyncio sockets."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -23,8 +23,8 @@ classifiers = [
|
|
|
23
23
|
"Typing :: Typed",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"without-core==0.0.
|
|
27
|
-
"without-asgi==0.0.
|
|
26
|
+
"without-core==0.0.5",
|
|
27
|
+
"without-asgi==0.0.5",
|
|
28
28
|
"h11>=0.16",
|
|
29
29
|
"h2>=4.1",
|
|
30
30
|
"wsproto>=1.2",
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "without-http"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.5"
|
|
8
8
|
description = "A sans-IO-backed ASGI server and HTTP client for without: h11/h2/wsproto over asyncio sockets."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -26,8 +26,8 @@ classifiers = [
|
|
|
26
26
|
"Typing :: Typed",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"without-core==0.0.
|
|
30
|
-
"without-asgi==0.0.
|
|
29
|
+
"without-core==0.0.5",
|
|
30
|
+
"without-asgi==0.0.5",
|
|
31
31
|
"h11>=0.16",
|
|
32
32
|
"h2>=4.1",
|
|
33
33
|
"wsproto>=1.2",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from without_http.client import DEFAULT_DECOMPRESSORS
|
|
2
|
+
from without_http.client import GZIP_CONTAINER
|
|
2
3
|
from without_http.client import USER_AGENT
|
|
3
4
|
from without_http.client import Client
|
|
4
5
|
from without_http.client import ClientMiddleware
|
|
@@ -13,10 +14,12 @@ from without_http.client import Resolve
|
|
|
13
14
|
from without_http.client import ResponseBody
|
|
14
15
|
from without_http.client import ResponseHead
|
|
15
16
|
from without_http.client import ResponseTrailers
|
|
17
|
+
from without_http.client import StreamingCompressor
|
|
16
18
|
from without_http.client import add_headers
|
|
17
19
|
from without_http.client import basic_auth
|
|
18
20
|
from without_http.client import bearer_auth
|
|
19
21
|
from without_http.client import brotli_compress
|
|
22
|
+
from without_http.client import brotli_compressor
|
|
20
23
|
from without_http.client import compressing
|
|
21
24
|
from without_http.client import cookies
|
|
22
25
|
from without_http.client import deadline
|
|
@@ -24,12 +27,14 @@ from without_http.client import decompress
|
|
|
24
27
|
from without_http.client import default_headers
|
|
25
28
|
from without_http.client import follow_redirects
|
|
26
29
|
from without_http.client import gzip_compress
|
|
30
|
+
from without_http.client import gzip_compressor
|
|
27
31
|
from without_http.client import request
|
|
28
32
|
from without_http.client import stack
|
|
29
33
|
from without_http.client import tcp_connect
|
|
30
34
|
from without_http.client import user_agent
|
|
31
35
|
from without_http.client import wrap
|
|
32
36
|
from without_http.client import zstd_compress
|
|
37
|
+
from without_http.client import zstd_compressor
|
|
33
38
|
from without_http.h2_wire import early_hint_headers
|
|
34
39
|
from without_http.h2_wire import request_headers
|
|
35
40
|
from without_http.h2_wire import response_headers
|
|
@@ -64,6 +69,7 @@ from without_http.ws_wire import ws_events_from_outbound
|
|
|
64
69
|
__all__ = [
|
|
65
70
|
"ALPN_PROTOCOLS",
|
|
66
71
|
"DEFAULT_DECOMPRESSORS",
|
|
72
|
+
"GZIP_CONTAINER",
|
|
67
73
|
"USER_AGENT",
|
|
68
74
|
"Client",
|
|
69
75
|
"ClientMiddleware",
|
|
@@ -85,12 +91,14 @@ __all__ = [
|
|
|
85
91
|
"ResponseTrailers",
|
|
86
92
|
"Server",
|
|
87
93
|
"SocketOptions",
|
|
94
|
+
"StreamingCompressor",
|
|
88
95
|
"Timeout",
|
|
89
96
|
"WriteTimeout",
|
|
90
97
|
"add_headers",
|
|
91
98
|
"basic_auth",
|
|
92
99
|
"bearer_auth",
|
|
93
100
|
"brotli_compress",
|
|
101
|
+
"brotli_compressor",
|
|
94
102
|
"compressing",
|
|
95
103
|
"cookies",
|
|
96
104
|
"deadline",
|
|
@@ -101,6 +109,7 @@ __all__ = [
|
|
|
101
109
|
"extensions_with_tls",
|
|
102
110
|
"follow_redirects",
|
|
103
111
|
"gzip_compress",
|
|
112
|
+
"gzip_compressor",
|
|
104
113
|
"h11_events_from_outbound",
|
|
105
114
|
"inbound_from_event",
|
|
106
115
|
"is_websocket_upgrade",
|
|
@@ -124,4 +133,5 @@ __all__ = [
|
|
|
124
133
|
"wrap",
|
|
125
134
|
"ws_events_from_outbound",
|
|
126
135
|
"zstd_compress",
|
|
136
|
+
"zstd_compressor",
|
|
127
137
|
]
|
|
@@ -23,6 +23,7 @@ from datetime import datetime
|
|
|
23
23
|
from datetime import timedelta
|
|
24
24
|
from email.utils import parsedate_to_datetime
|
|
25
25
|
from importlib import metadata
|
|
26
|
+
from types import MappingProxyType
|
|
26
27
|
from typing import NamedTuple
|
|
27
28
|
from typing import Protocol
|
|
28
29
|
from typing import Self
|
|
@@ -45,6 +46,12 @@ from without import stack
|
|
|
45
46
|
from without_asgi import Content
|
|
46
47
|
from without_asgi import RawHeaders
|
|
47
48
|
from without_asgi import StreamingContent
|
|
49
|
+
from without_asgi.compression import GZIP_CONTAINER
|
|
50
|
+
from without_asgi.compression import Compressor
|
|
51
|
+
from without_asgi.compression import StreamingCompressor
|
|
52
|
+
from without_asgi.compression import brotli_compressor
|
|
53
|
+
from without_asgi.compression import gzip_compressor
|
|
54
|
+
from without_asgi.compression import zstd_compressor
|
|
48
55
|
from without_asgi.headers import merge
|
|
49
56
|
|
|
50
57
|
from without_http.h2_wire import request_headers
|
|
@@ -1615,26 +1622,28 @@ def follow_redirects(max_hops: int = 5) -> ClientMiddleware:
|
|
|
1615
1622
|
|
|
1616
1623
|
|
|
1617
1624
|
_REQUEST_FRAMING_HEADERS = frozenset({b"content-length", b"transfer-encoding"})
|
|
1618
|
-
_GZIP_CONTAINER = zlib.MAX_WBITS | 16 # the wbits offset that selects the gzip wrapper around DEFLATE
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
class Compressor(Protocol):
|
|
1622
|
-
"""
|
|
1623
|
-
The incremental compressor shape `compressing` drives: feed chunks through
|
|
1624
|
-
`compress`, then `flush` ends the stream. `zlib.compressobj` and
|
|
1625
|
-
`zstd.ZstdCompressor` satisfy it as-is; a third-party codec plugs in with
|
|
1626
|
-
whatever thin adapter its own surface needs.
|
|
1627
|
-
"""
|
|
1628
|
-
|
|
1629
|
-
def compress(self, data: bytes, /) -> bytes: ...
|
|
1630
|
-
def flush(self) -> bytes: ...
|
|
1631
1625
|
|
|
1632
1626
|
|
|
1633
1627
|
async def _compressed(body: Stream[bytes], compressor: Compressor) -> AsyncIterator[bytes]:
|
|
1628
|
+
# A codec decides for itself what any one `compress` call returns, and fed the
|
|
1629
|
+
# pieces a streamed body arrives in it usually returns nothing, holding the whole
|
|
1630
|
+
# upload until the stream ends. Ending a block per chunk is what releases it; a
|
|
1631
|
+
# codec with no way to do that (a plain `Compressor`) has to hold it, since unlike
|
|
1632
|
+
# a negotiated response there is no unencoded answer available to a caller who
|
|
1633
|
+
# asked for this coding by name.
|
|
1634
|
+
flush_block: Callable[[], bytes] = (
|
|
1635
|
+
compressor.flush_block if isinstance(compressor, StreamingCompressor) else lambda: b""
|
|
1636
|
+
)
|
|
1637
|
+
# One chunk of lookahead, so the last one is known to be last and rides out on the
|
|
1638
|
+
# flush that ends the stream rather than paying for a block of its own. A body that
|
|
1639
|
+
# arrives whole is one chunk, so it still encodes to exactly the bytes it would
|
|
1640
|
+
# have without any of this.
|
|
1641
|
+
held: bytes | None = None
|
|
1634
1642
|
async for chunk in body:
|
|
1635
|
-
if compressed := compressor.compress(
|
|
1643
|
+
if held and (compressed := compressor.compress(held) + flush_block()):
|
|
1636
1644
|
yield compressed
|
|
1637
|
-
|
|
1645
|
+
held = chunk
|
|
1646
|
+
yield compressor.compress(held or b"") + compressor.flush()
|
|
1638
1647
|
|
|
1639
1648
|
|
|
1640
1649
|
def compressing(coding: bytes, make_compressor: Callable[[], Compressor]) -> ClientMiddleware:
|
|
@@ -1648,11 +1657,21 @@ def compressing(coding: bytes, make_compressor: Callable[[], Compressor]) -> Cli
|
|
|
1648
1657
|
reimplemented.
|
|
1649
1658
|
|
|
1650
1659
|
That rest: the lazy body `Stream[bytes]` is wrapped in the incremental compressor,
|
|
1651
|
-
so a streamed upload compresses chunk by chunk and
|
|
1652
|
-
framing follows the rewrite, `content-length` no longer describes the body,
|
|
1653
|
-
is dropped and the compressed stream goes out `transfer-encoding: chunked`
|
|
1654
|
-
HTTP/2 the framing header is dropped with the other hop-by-hop headers and
|
|
1655
|
-
body rides DATA frames as usual).
|
|
1660
|
+
so a streamed upload compresses chunk by chunk and holds no more than one of them;
|
|
1661
|
+
and the framing follows the rewrite, `content-length` no longer describes the body,
|
|
1662
|
+
so it is dropped and the compressed stream goes out `transfer-encoding: chunked`
|
|
1663
|
+
(over HTTP/2 the framing header is dropped with the other hop-by-hop headers and
|
|
1664
|
+
the body rides DATA frames as usual).
|
|
1665
|
+
|
|
1666
|
+
Holding no more than a chunk is a demand on the codec, not just on the loop: what
|
|
1667
|
+
`compress` returns is the codec's choice, and fed a chunk at a time zlib and zstd
|
|
1668
|
+
return almost nothing until the stream ends, which would buffer the whole upload
|
|
1669
|
+
inside the codec while looking like it streamed. `make_compressor` should therefore
|
|
1670
|
+
produce a `StreamingCompressor`, as `gzip_compressor`, `zstd_compressor`, and
|
|
1671
|
+
`brotli_compressor` all do; a plain `Compressor` still encodes correctly and still
|
|
1672
|
+
buffers, because a coding named by the caller has no unencoded answer to fall back
|
|
1673
|
+
on the way a negotiated response does. A body that arrives whole is one chunk
|
|
1674
|
+
either way, and encodes to the same bytes under both.
|
|
1656
1675
|
|
|
1657
1676
|
Two kinds of request pass through untouched: one already carrying a
|
|
1658
1677
|
`content-encoding` (the body is already encoded; re-compressing would corrupt it),
|
|
@@ -1694,7 +1713,7 @@ def gzip_compress(level: int = zlib.Z_DEFAULT_COMPRESSION) -> ClientMiddleware:
|
|
|
1694
1713
|
codings, and `compressing` is the shared mechanism for any coding beyond those;
|
|
1695
1714
|
the response-side counterpart to all of them is `decompress`.
|
|
1696
1715
|
"""
|
|
1697
|
-
return compressing(b"gzip", lambda:
|
|
1716
|
+
return compressing(b"gzip", lambda: gzip_compressor(level))
|
|
1698
1717
|
|
|
1699
1718
|
|
|
1700
1719
|
def zstd_compress(level: int | None = None) -> ClientMiddleware:
|
|
@@ -1708,33 +1727,7 @@ def zstd_compress(level: int | None = None) -> ClientMiddleware:
|
|
|
1708
1727
|
|
|
1709
1728
|
`level` is zstd's compression level, defaulting to the library's own default.
|
|
1710
1729
|
"""
|
|
1711
|
-
return compressing(b"zstd", lambda:
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
class _RawBrotliCompressor(Protocol):
|
|
1715
|
-
"""The slice of `brotli.Compressor` the adapter drives (the bindings ship no types)."""
|
|
1716
|
-
|
|
1717
|
-
def process(self, data: bytes, /) -> bytes: ...
|
|
1718
|
-
def finish(self) -> bytes: ...
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
@dataclass(slots=True, eq=False)
|
|
1722
|
-
class _BrotliCompressor:
|
|
1723
|
-
"""
|
|
1724
|
-
Adapt `brotli.Compressor` to the `Compressor` shape.
|
|
1725
|
-
|
|
1726
|
-
Brotli's bindings spell the incremental surface `process`/`finish` (their `flush`
|
|
1727
|
-
is a mid-stream flush that keeps the stream open), so the adapter maps `compress`
|
|
1728
|
-
to `process` and `flush` to `finish`.
|
|
1729
|
-
"""
|
|
1730
|
-
|
|
1731
|
-
_raw: _RawBrotliCompressor
|
|
1732
|
-
|
|
1733
|
-
def compress(self, data: bytes, /) -> bytes:
|
|
1734
|
-
return self._raw.process(data)
|
|
1735
|
-
|
|
1736
|
-
def flush(self) -> bytes:
|
|
1737
|
-
return self._raw.finish()
|
|
1730
|
+
return compressing(b"zstd", lambda: zstd_compressor(level))
|
|
1738
1731
|
|
|
1739
1732
|
|
|
1740
1733
|
def brotli_compress(quality: int = 11) -> ClientMiddleware:
|
|
@@ -1744,9 +1737,12 @@ def brotli_compress(quality: int = 11) -> ClientMiddleware:
|
|
|
1744
1737
|
`gzip_compress`'s sibling over brotli ([Google's own bindings](https://github.com/google/brotli),
|
|
1745
1738
|
a bundled dependency since the stdlib has no brotli): everything there holds here,
|
|
1746
1739
|
and only the coding differs. `quality` is brotli's compression quality (0-11),
|
|
1747
|
-
defaulting to the
|
|
1740
|
+
defaulting to the bindings' own default of 11, the maximum, because a client
|
|
1741
|
+
compressing an upload it holds whole is the case that ratio is worth paying for.
|
|
1742
|
+
The server-side `compress` table defaults lower, since it encodes per response;
|
|
1743
|
+
see `without_asgi.compression.brotli_compressor`, the shared codec behind both.
|
|
1748
1744
|
"""
|
|
1749
|
-
return compressing(b"br", lambda:
|
|
1745
|
+
return compressing(b"br", lambda: brotli_compressor(quality))
|
|
1750
1746
|
|
|
1751
1747
|
|
|
1752
1748
|
class Decompressor(Protocol):
|
|
@@ -1767,7 +1763,7 @@ class Decompressor(Protocol):
|
|
|
1767
1763
|
|
|
1768
1764
|
|
|
1769
1765
|
def _gzip_decompressor() -> Decompressor:
|
|
1770
|
-
return zlib.decompressobj(wbits=
|
|
1766
|
+
return zlib.decompressobj(wbits=GZIP_CONTAINER)
|
|
1771
1767
|
|
|
1772
1768
|
|
|
1773
1769
|
class _RawBrotliDecompressor(Protocol):
|
|
@@ -1802,12 +1798,16 @@ def _brotli_decompressor() -> Decompressor:
|
|
|
1802
1798
|
|
|
1803
1799
|
|
|
1804
1800
|
# The codings decoded out of the box: gzip and zstd from the stdlib, brotli from the
|
|
1805
|
-
# bundled bindings. `decompress`'s default table
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1801
|
+
# bundled bindings. `decompress`'s default table, and the mirror of the server-side
|
|
1802
|
+
# `DEFAULT_COMPRESSORS`, a proxy for the same two reasons: nothing can mutate the table
|
|
1803
|
+
# every other caller starts from, and `|` extends it into a fresh `dict`.
|
|
1804
|
+
DEFAULT_DECOMPRESSORS: MappingProxyType[bytes, Callable[[], Decompressor]] = MappingProxyType(
|
|
1805
|
+
{
|
|
1806
|
+
b"br": _brotli_decompressor,
|
|
1807
|
+
b"gzip": _gzip_decompressor,
|
|
1808
|
+
b"zstd": zstd.ZstdDecompressor,
|
|
1809
|
+
}
|
|
1810
|
+
)
|
|
1811
1811
|
|
|
1812
1812
|
_CONTENT_CODING_HEADERS = frozenset({b"content-encoding", b"content-length"})
|
|
1813
1813
|
|
|
@@ -1859,7 +1859,7 @@ def decompress(
|
|
|
1859
1859
|
the bundled bindings). The offer is *derived from its keys*, so what is
|
|
1860
1860
|
advertised and what is decoded cannot disagree; register a coding this package
|
|
1861
1861
|
does not ship by extending the table
|
|
1862
|
-
(`decompress(
|
|
1862
|
+
(`decompress(DEFAULT_DECOMPRESSORS | {b"lzma": make_lzma})`) with any factory
|
|
1863
1863
|
whose product satisfies `Decompressor`. The mapping is snapshotted, keys
|
|
1864
1864
|
lowercased, when the middleware is built.
|
|
1865
1865
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|