rh2 0.0.2__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.
- rh2-0.0.2/.github/workflows/publish.yml +80 -0
- rh2-0.0.2/.gitignore +7 -0
- rh2-0.0.2/Cargo.lock +325 -0
- rh2-0.0.2/Cargo.toml +22 -0
- rh2-0.0.2/PKG-INFO +37 -0
- rh2-0.0.2/README.md +26 -0
- rh2-0.0.2/pyproject.toml +20 -0
- rh2-0.0.2/python/rh2/__init__.py +27 -0
- rh2-0.0.2/src/lib.rs +545 -0
- rh2-0.0.2/tests/test_roundtrip.py +103 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI via Trusted Publishing (OIDC) - no API token needed.
|
|
4
|
+
# Configure the publisher at https://pypi.org/manage/account/publishing/ with:
|
|
5
|
+
# project: rh2, owner: Kludex, repo: rh2, workflow: publish.yml, environment: pypi
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
name: build ${{ matrix.platform.runner }} (${{ matrix.platform.target }})
|
|
18
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
platform:
|
|
23
|
+
- runner: ubuntu-latest
|
|
24
|
+
target: x86_64
|
|
25
|
+
- runner: ubuntu-latest
|
|
26
|
+
target: aarch64
|
|
27
|
+
- runner: windows-latest
|
|
28
|
+
target: x64
|
|
29
|
+
# macOS Apple Silicon. Intel-mac (macos-13) is omitted: its runners
|
|
30
|
+
# queue for hours on this repo and block the whole release. Intel-mac
|
|
31
|
+
# users can build from the published sdist.
|
|
32
|
+
- runner: macos-14
|
|
33
|
+
target: aarch64
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.x"
|
|
39
|
+
# abi3-py39 produces a single wheel per platform that works on CPython >= 3.9.
|
|
40
|
+
- name: Build wheels
|
|
41
|
+
uses: PyO3/maturin-action@v1
|
|
42
|
+
with:
|
|
43
|
+
target: ${{ matrix.platform.target }}
|
|
44
|
+
args: --release --out dist
|
|
45
|
+
sccache: "true"
|
|
46
|
+
manylinux: auto
|
|
47
|
+
- uses: actions/upload-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: wheels-${{ matrix.platform.runner }}-${{ matrix.platform.target }}
|
|
50
|
+
path: dist
|
|
51
|
+
|
|
52
|
+
sdist:
|
|
53
|
+
name: build sdist
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
- name: Build sdist
|
|
58
|
+
uses: PyO3/maturin-action@v1
|
|
59
|
+
with:
|
|
60
|
+
command: sdist
|
|
61
|
+
args: --out dist
|
|
62
|
+
- uses: actions/upload-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: wheels-sdist
|
|
65
|
+
path: dist
|
|
66
|
+
|
|
67
|
+
release:
|
|
68
|
+
name: publish to PyPI
|
|
69
|
+
needs: [build, sdist]
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
environment: pypi
|
|
72
|
+
permissions:
|
|
73
|
+
id-token: write # required for Trusted Publishing
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/download-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
pattern: wheels-*
|
|
78
|
+
merge-multiple: true
|
|
79
|
+
path: dist
|
|
80
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
rh2-0.0.2/.gitignore
ADDED
rh2-0.0.2/Cargo.lock
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "atomic-waker"
|
|
7
|
+
version = "1.1.2"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "autocfg"
|
|
13
|
+
version = "1.5.1"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "bytes"
|
|
19
|
+
version = "1.12.0"
|
|
20
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
|
+
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "cfg-if"
|
|
25
|
+
version = "1.0.4"
|
|
26
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
27
|
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
28
|
+
|
|
29
|
+
[[package]]
|
|
30
|
+
name = "equivalent"
|
|
31
|
+
version = "1.0.2"
|
|
32
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
34
|
+
|
|
35
|
+
[[package]]
|
|
36
|
+
name = "fnv"
|
|
37
|
+
version = "1.0.7"
|
|
38
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
39
|
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
40
|
+
|
|
41
|
+
[[package]]
|
|
42
|
+
name = "futures-core"
|
|
43
|
+
version = "0.3.32"
|
|
44
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
45
|
+
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
|
46
|
+
|
|
47
|
+
[[package]]
|
|
48
|
+
name = "futures-sink"
|
|
49
|
+
version = "0.3.32"
|
|
50
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
51
|
+
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
|
52
|
+
|
|
53
|
+
[[package]]
|
|
54
|
+
name = "h2"
|
|
55
|
+
version = "0.4.15"
|
|
56
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
57
|
+
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
|
58
|
+
dependencies = [
|
|
59
|
+
"atomic-waker",
|
|
60
|
+
"bytes",
|
|
61
|
+
"fnv",
|
|
62
|
+
"futures-core",
|
|
63
|
+
"futures-sink",
|
|
64
|
+
"http",
|
|
65
|
+
"indexmap",
|
|
66
|
+
"slab",
|
|
67
|
+
"tokio",
|
|
68
|
+
"tokio-util",
|
|
69
|
+
"tracing",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[[package]]
|
|
73
|
+
name = "hashbrown"
|
|
74
|
+
version = "0.17.1"
|
|
75
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
76
|
+
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
|
77
|
+
|
|
78
|
+
[[package]]
|
|
79
|
+
name = "heck"
|
|
80
|
+
version = "0.5.0"
|
|
81
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
82
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
83
|
+
|
|
84
|
+
[[package]]
|
|
85
|
+
name = "http"
|
|
86
|
+
version = "1.4.2"
|
|
87
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
88
|
+
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
|
89
|
+
dependencies = [
|
|
90
|
+
"bytes",
|
|
91
|
+
"itoa",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[[package]]
|
|
95
|
+
name = "indexmap"
|
|
96
|
+
version = "2.14.0"
|
|
97
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
98
|
+
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
|
99
|
+
dependencies = [
|
|
100
|
+
"equivalent",
|
|
101
|
+
"hashbrown",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
[[package]]
|
|
105
|
+
name = "indoc"
|
|
106
|
+
version = "2.0.7"
|
|
107
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
108
|
+
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
|
109
|
+
dependencies = [
|
|
110
|
+
"rustversion",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
[[package]]
|
|
114
|
+
name = "itoa"
|
|
115
|
+
version = "1.0.18"
|
|
116
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
117
|
+
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
118
|
+
|
|
119
|
+
[[package]]
|
|
120
|
+
name = "libc"
|
|
121
|
+
version = "0.2.186"
|
|
122
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
123
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
124
|
+
|
|
125
|
+
[[package]]
|
|
126
|
+
name = "memoffset"
|
|
127
|
+
version = "0.9.1"
|
|
128
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
129
|
+
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
|
130
|
+
dependencies = [
|
|
131
|
+
"autocfg",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
[[package]]
|
|
135
|
+
name = "once_cell"
|
|
136
|
+
version = "1.21.4"
|
|
137
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
138
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
139
|
+
|
|
140
|
+
[[package]]
|
|
141
|
+
name = "pin-project-lite"
|
|
142
|
+
version = "0.2.17"
|
|
143
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
144
|
+
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
|
145
|
+
|
|
146
|
+
[[package]]
|
|
147
|
+
name = "portable-atomic"
|
|
148
|
+
version = "1.13.1"
|
|
149
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
150
|
+
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|
151
|
+
|
|
152
|
+
[[package]]
|
|
153
|
+
name = "proc-macro2"
|
|
154
|
+
version = "1.0.106"
|
|
155
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
156
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
157
|
+
dependencies = [
|
|
158
|
+
"unicode-ident",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
[[package]]
|
|
162
|
+
name = "pyo3"
|
|
163
|
+
version = "0.22.6"
|
|
164
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
165
|
+
checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884"
|
|
166
|
+
dependencies = [
|
|
167
|
+
"cfg-if",
|
|
168
|
+
"indoc",
|
|
169
|
+
"libc",
|
|
170
|
+
"memoffset",
|
|
171
|
+
"once_cell",
|
|
172
|
+
"portable-atomic",
|
|
173
|
+
"pyo3-build-config",
|
|
174
|
+
"pyo3-ffi",
|
|
175
|
+
"pyo3-macros",
|
|
176
|
+
"unindent",
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
[[package]]
|
|
180
|
+
name = "pyo3-build-config"
|
|
181
|
+
version = "0.22.6"
|
|
182
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
183
|
+
checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38"
|
|
184
|
+
dependencies = [
|
|
185
|
+
"once_cell",
|
|
186
|
+
"target-lexicon",
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
[[package]]
|
|
190
|
+
name = "pyo3-ffi"
|
|
191
|
+
version = "0.22.6"
|
|
192
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
193
|
+
checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636"
|
|
194
|
+
dependencies = [
|
|
195
|
+
"libc",
|
|
196
|
+
"pyo3-build-config",
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
[[package]]
|
|
200
|
+
name = "pyo3-macros"
|
|
201
|
+
version = "0.22.6"
|
|
202
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
203
|
+
checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453"
|
|
204
|
+
dependencies = [
|
|
205
|
+
"proc-macro2",
|
|
206
|
+
"pyo3-macros-backend",
|
|
207
|
+
"quote",
|
|
208
|
+
"syn",
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
[[package]]
|
|
212
|
+
name = "pyo3-macros-backend"
|
|
213
|
+
version = "0.22.6"
|
|
214
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
215
|
+
checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe"
|
|
216
|
+
dependencies = [
|
|
217
|
+
"heck",
|
|
218
|
+
"proc-macro2",
|
|
219
|
+
"pyo3-build-config",
|
|
220
|
+
"quote",
|
|
221
|
+
"syn",
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
[[package]]
|
|
225
|
+
name = "quote"
|
|
226
|
+
version = "1.0.46"
|
|
227
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
228
|
+
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
|
229
|
+
dependencies = [
|
|
230
|
+
"proc-macro2",
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
[[package]]
|
|
234
|
+
name = "rh2"
|
|
235
|
+
version = "0.0.2"
|
|
236
|
+
dependencies = [
|
|
237
|
+
"bytes",
|
|
238
|
+
"h2",
|
|
239
|
+
"http",
|
|
240
|
+
"pyo3",
|
|
241
|
+
"tokio",
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
[[package]]
|
|
245
|
+
name = "rustversion"
|
|
246
|
+
version = "1.0.22"
|
|
247
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
248
|
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
249
|
+
|
|
250
|
+
[[package]]
|
|
251
|
+
name = "slab"
|
|
252
|
+
version = "0.4.12"
|
|
253
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
254
|
+
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|
255
|
+
|
|
256
|
+
[[package]]
|
|
257
|
+
name = "syn"
|
|
258
|
+
version = "2.0.118"
|
|
259
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
260
|
+
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
|
261
|
+
dependencies = [
|
|
262
|
+
"proc-macro2",
|
|
263
|
+
"quote",
|
|
264
|
+
"unicode-ident",
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
[[package]]
|
|
268
|
+
name = "target-lexicon"
|
|
269
|
+
version = "0.12.16"
|
|
270
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
271
|
+
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|
272
|
+
|
|
273
|
+
[[package]]
|
|
274
|
+
name = "tokio"
|
|
275
|
+
version = "1.52.3"
|
|
276
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
277
|
+
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
|
278
|
+
dependencies = [
|
|
279
|
+
"bytes",
|
|
280
|
+
"pin-project-lite",
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
[[package]]
|
|
284
|
+
name = "tokio-util"
|
|
285
|
+
version = "0.7.18"
|
|
286
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
287
|
+
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
|
288
|
+
dependencies = [
|
|
289
|
+
"bytes",
|
|
290
|
+
"futures-core",
|
|
291
|
+
"futures-sink",
|
|
292
|
+
"pin-project-lite",
|
|
293
|
+
"tokio",
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
[[package]]
|
|
297
|
+
name = "tracing"
|
|
298
|
+
version = "0.1.44"
|
|
299
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
300
|
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
|
301
|
+
dependencies = [
|
|
302
|
+
"pin-project-lite",
|
|
303
|
+
"tracing-core",
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
[[package]]
|
|
307
|
+
name = "tracing-core"
|
|
308
|
+
version = "0.1.36"
|
|
309
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
310
|
+
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
|
311
|
+
dependencies = [
|
|
312
|
+
"once_cell",
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
[[package]]
|
|
316
|
+
name = "unicode-ident"
|
|
317
|
+
version = "1.0.24"
|
|
318
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
319
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
320
|
+
|
|
321
|
+
[[package]]
|
|
322
|
+
name = "unindent"
|
|
323
|
+
version = "0.2.4"
|
|
324
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
325
|
+
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
rh2-0.0.2/Cargo.toml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rh2"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Rust-backed sans-IO HTTP/2 codec for Python, wrapping the h2 crate"
|
|
6
|
+
license = "BSD-3-Clause"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
name = "rh2"
|
|
11
|
+
crate-type = ["cdylib"]
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
pyo3 = { version = "0.22", features = ["extension-module", "abi3-py39"] }
|
|
15
|
+
h2 = "0.4"
|
|
16
|
+
http = "1"
|
|
17
|
+
bytes = "1"
|
|
18
|
+
tokio = { version = "1", features = ["io-util"] }
|
|
19
|
+
|
|
20
|
+
[profile.release]
|
|
21
|
+
lto = true
|
|
22
|
+
codegen-units = 1
|
rh2-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rh2
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Summary: Rust-backed sans-IO HTTP/2 codec for Python, wrapping the h2 crate
|
|
7
|
+
License: BSD-3-Clause
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
10
|
+
|
|
11
|
+
# rh2
|
|
12
|
+
|
|
13
|
+
A Rust-backed, sans-IO HTTP/2 codec for Python, wrapping the
|
|
14
|
+
[`h2`](https://crates.io/crates/h2) crate (the HTTP/2 implementation behind
|
|
15
|
+
`hyper`/`reqwest`) via [PyO3](https://pyo3.rs).
|
|
16
|
+
|
|
17
|
+
The `h2` crate drives a connection over an async transport; `rh2` runs it
|
|
18
|
+
*sans-IO* by backing that transport with in-memory buffers and pumping the
|
|
19
|
+
state machine by hand. The Python API mirrors
|
|
20
|
+
[`hyper-h2`](https://python-hyper.org/projects/h2/):
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import rh2
|
|
24
|
+
|
|
25
|
+
conn = rh2.H2Connection()
|
|
26
|
+
events = conn.receive_data(data_from_socket)
|
|
27
|
+
for event in events:
|
|
28
|
+
if isinstance(event, rh2.RequestReceived):
|
|
29
|
+
conn.send_headers(event.stream_id, 200, [(b"content-type", b"text/plain")])
|
|
30
|
+
conn.send_data(event.stream_id, b"hello", end_stream=True)
|
|
31
|
+
socket.sendall(conn.data_to_send())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Status
|
|
35
|
+
|
|
36
|
+
Experimental. Server-side only.
|
|
37
|
+
|
rh2-0.0.2/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# rh2
|
|
2
|
+
|
|
3
|
+
A Rust-backed, sans-IO HTTP/2 codec for Python, wrapping the
|
|
4
|
+
[`h2`](https://crates.io/crates/h2) crate (the HTTP/2 implementation behind
|
|
5
|
+
`hyper`/`reqwest`) via [PyO3](https://pyo3.rs).
|
|
6
|
+
|
|
7
|
+
The `h2` crate drives a connection over an async transport; `rh2` runs it
|
|
8
|
+
*sans-IO* by backing that transport with in-memory buffers and pumping the
|
|
9
|
+
state machine by hand. The Python API mirrors
|
|
10
|
+
[`hyper-h2`](https://python-hyper.org/projects/h2/):
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import rh2
|
|
14
|
+
|
|
15
|
+
conn = rh2.H2Connection()
|
|
16
|
+
events = conn.receive_data(data_from_socket)
|
|
17
|
+
for event in events:
|
|
18
|
+
if isinstance(event, rh2.RequestReceived):
|
|
19
|
+
conn.send_headers(event.stream_id, 200, [(b"content-type", b"text/plain")])
|
|
20
|
+
conn.send_data(event.stream_id, b"hello", end_stream=True)
|
|
21
|
+
socket.sendall(conn.data_to_send())
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Status
|
|
25
|
+
|
|
26
|
+
Experimental. Server-side only.
|
rh2-0.0.2/pyproject.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.7,<2"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rh2"
|
|
7
|
+
version = "0.0.2"
|
|
8
|
+
description = "Rust-backed sans-IO HTTP/2 codec for Python, wrapping the h2 crate"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "BSD-3-Clause" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Rust",
|
|
14
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.maturin]
|
|
18
|
+
features = ["pyo3/extension-module"]
|
|
19
|
+
python-source = "python"
|
|
20
|
+
module-name = "rh2._rh2"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Rust-backed sans-IO HTTP/2 codec, wrapping the `h2` crate.
|
|
2
|
+
|
|
3
|
+
The API is deliberately close to `hyper-h2`: create a :class:`H2Connection`, feed
|
|
4
|
+
it bytes from the socket with :meth:`H2Connection.receive_data`, act on the
|
|
5
|
+
returned events, and write whatever :meth:`H2Connection.data_to_send` returns
|
|
6
|
+
back to the socket.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from rh2._rh2 import (
|
|
12
|
+
ConnectionTerminated,
|
|
13
|
+
DataReceived,
|
|
14
|
+
H2Connection,
|
|
15
|
+
RequestReceived,
|
|
16
|
+
StreamEnded,
|
|
17
|
+
StreamReset,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ConnectionTerminated",
|
|
22
|
+
"DataReceived",
|
|
23
|
+
"H2Connection",
|
|
24
|
+
"RequestReceived",
|
|
25
|
+
"StreamEnded",
|
|
26
|
+
"StreamReset",
|
|
27
|
+
]
|
rh2-0.0.2/src/lib.rs
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
//! Sans-IO HTTP/2 codec for Python, wrapping the async `h2` crate.
|
|
2
|
+
//!
|
|
3
|
+
//! The `h2` crate drives an HTTP/2 connection over a `tokio` `AsyncRead + AsyncWrite`
|
|
4
|
+
//! transport. We want a *sans-IO* codec instead: Python feeds us the bytes it read
|
|
5
|
+
//! from the socket (`receive_data`) and drains the bytes we want to write
|
|
6
|
+
//! (`data_to_send`), and we surface protocol events. To bridge the two we back the
|
|
7
|
+
//! transport with in-memory buffers and drive `h2`'s futures by hand with a no-op
|
|
8
|
+
//! waker, pumping them to `Pending` after every interaction.
|
|
9
|
+
|
|
10
|
+
use std::cell::RefCell;
|
|
11
|
+
use std::collections::{HashMap, VecDeque};
|
|
12
|
+
use std::future::Future;
|
|
13
|
+
use std::io;
|
|
14
|
+
use std::pin::Pin;
|
|
15
|
+
use std::rc::Rc;
|
|
16
|
+
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
|
17
|
+
|
|
18
|
+
use bytes::Bytes;
|
|
19
|
+
use h2::server::{self, SendResponse};
|
|
20
|
+
use h2::{RecvStream, SendStream};
|
|
21
|
+
use http::{HeaderName, HeaderValue, Response, StatusCode};
|
|
22
|
+
use pyo3::exceptions::PyValueError;
|
|
23
|
+
use pyo3::prelude::*;
|
|
24
|
+
use pyo3::types::{PyBytes, PyList};
|
|
25
|
+
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
|
26
|
+
|
|
27
|
+
// --- No-op waker -----------------------------------------------------------
|
|
28
|
+
// We poll manually and re-poll after feeding input, so wakeups are irrelevant.
|
|
29
|
+
|
|
30
|
+
fn noop_waker() -> Waker {
|
|
31
|
+
fn clone(_: *const ()) -> RawWaker {
|
|
32
|
+
RawWaker::new(std::ptr::null(), &VTABLE)
|
|
33
|
+
}
|
|
34
|
+
fn noop(_: *const ()) {}
|
|
35
|
+
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
|
|
36
|
+
unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- In-memory duplex transport -------------------------------------------
|
|
40
|
+
|
|
41
|
+
#[derive(Default)]
|
|
42
|
+
struct IoInner {
|
|
43
|
+
// Contiguous buffer + read cursor so poll_read can bulk-copy instead of
|
|
44
|
+
// popping a byte at a time.
|
|
45
|
+
inbound: Vec<u8>,
|
|
46
|
+
inbound_pos: usize,
|
|
47
|
+
outbound: Vec<u8>,
|
|
48
|
+
inbound_closed: bool,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl IoInner {
|
|
52
|
+
fn push_inbound(&mut self, data: &[u8]) {
|
|
53
|
+
// Drop already-consumed bytes before growing, keeping the buffer small.
|
|
54
|
+
if self.inbound_pos > 0 {
|
|
55
|
+
self.inbound.drain(0..self.inbound_pos);
|
|
56
|
+
self.inbound_pos = 0;
|
|
57
|
+
}
|
|
58
|
+
self.inbound.extend_from_slice(data);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[derive(Clone)]
|
|
63
|
+
struct SharedIo(Rc<RefCell<IoInner>>);
|
|
64
|
+
|
|
65
|
+
impl SharedIo {
|
|
66
|
+
fn new() -> Self {
|
|
67
|
+
SharedIo(Rc::new(RefCell::new(IoInner::default())))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl AsyncRead for SharedIo {
|
|
72
|
+
fn poll_read(
|
|
73
|
+
self: Pin<&mut Self>,
|
|
74
|
+
_cx: &mut Context<'_>,
|
|
75
|
+
buf: &mut ReadBuf<'_>,
|
|
76
|
+
) -> Poll<io::Result<()>> {
|
|
77
|
+
let mut inner = self.0.borrow_mut();
|
|
78
|
+
let avail = inner.inbound.len() - inner.inbound_pos;
|
|
79
|
+
if avail == 0 {
|
|
80
|
+
// EOF once the peer half-closed and we've drained everything.
|
|
81
|
+
if inner.inbound_closed {
|
|
82
|
+
return Poll::Ready(Ok(()));
|
|
83
|
+
}
|
|
84
|
+
return Poll::Pending;
|
|
85
|
+
}
|
|
86
|
+
let n = std::cmp::min(buf.remaining(), avail);
|
|
87
|
+
let start = inner.inbound_pos;
|
|
88
|
+
buf.put_slice(&inner.inbound[start..start + n]);
|
|
89
|
+
inner.inbound_pos += n;
|
|
90
|
+
if inner.inbound_pos == inner.inbound.len() {
|
|
91
|
+
inner.inbound.clear();
|
|
92
|
+
inner.inbound_pos = 0;
|
|
93
|
+
}
|
|
94
|
+
Poll::Ready(Ok(()))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl AsyncWrite for SharedIo {
|
|
99
|
+
fn poll_write(
|
|
100
|
+
self: Pin<&mut Self>,
|
|
101
|
+
_cx: &mut Context<'_>,
|
|
102
|
+
buf: &[u8],
|
|
103
|
+
) -> Poll<io::Result<usize>> {
|
|
104
|
+
self.0.borrow_mut().outbound.extend_from_slice(buf);
|
|
105
|
+
Poll::Ready(Ok(buf.len()))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
109
|
+
Poll::Ready(Ok(()))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
113
|
+
Poll::Ready(Ok(()))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Events ----------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
#[pyclass(module = "rh2._rh2")]
|
|
120
|
+
struct RequestReceived {
|
|
121
|
+
#[pyo3(get)]
|
|
122
|
+
stream_id: u32,
|
|
123
|
+
headers: Py<PyList>,
|
|
124
|
+
#[pyo3(get)]
|
|
125
|
+
stream_ended: bool,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[pymethods]
|
|
129
|
+
impl RequestReceived {
|
|
130
|
+
#[getter]
|
|
131
|
+
fn headers(&self, py: Python<'_>) -> Py<PyList> {
|
|
132
|
+
self.headers.clone_ref(py)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn __repr__(&self) -> String {
|
|
136
|
+
format!("RequestReceived(stream_id={}, stream_ended={})", self.stream_id, self.stream_ended)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[pyclass(module = "rh2._rh2")]
|
|
141
|
+
struct DataReceived {
|
|
142
|
+
#[pyo3(get)]
|
|
143
|
+
stream_id: u32,
|
|
144
|
+
data: Py<PyBytes>,
|
|
145
|
+
#[pyo3(get)]
|
|
146
|
+
stream_ended: bool,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[pymethods]
|
|
150
|
+
impl DataReceived {
|
|
151
|
+
#[getter]
|
|
152
|
+
fn data(&self, py: Python<'_>) -> Py<PyBytes> {
|
|
153
|
+
self.data.clone_ref(py)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn __repr__(&self) -> String {
|
|
157
|
+
format!("DataReceived(stream_id={})", self.stream_id)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[pyclass(module = "rh2._rh2")]
|
|
162
|
+
struct StreamEnded {
|
|
163
|
+
#[pyo3(get)]
|
|
164
|
+
stream_id: u32,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[pymethods]
|
|
168
|
+
impl StreamEnded {
|
|
169
|
+
fn __repr__(&self) -> String {
|
|
170
|
+
format!("StreamEnded(stream_id={})", self.stream_id)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[pyclass(module = "rh2._rh2")]
|
|
175
|
+
struct StreamReset {
|
|
176
|
+
#[pyo3(get)]
|
|
177
|
+
stream_id: u32,
|
|
178
|
+
#[pyo3(get)]
|
|
179
|
+
error_code: u32,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[pymethods]
|
|
183
|
+
impl StreamReset {
|
|
184
|
+
fn __repr__(&self) -> String {
|
|
185
|
+
format!("StreamReset(stream_id={}, error_code={})", self.stream_id, self.error_code)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[pyclass(module = "rh2._rh2")]
|
|
190
|
+
struct ConnectionTerminated {}
|
|
191
|
+
|
|
192
|
+
#[pymethods]
|
|
193
|
+
impl ConnectionTerminated {
|
|
194
|
+
fn __repr__(&self) -> String {
|
|
195
|
+
"ConnectionTerminated()".to_string()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// --- Connection ------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
type HandshakeFut =
|
|
202
|
+
Pin<Box<dyn Future<Output = Result<server::Connection<SharedIo, Bytes>, h2::Error>>>>;
|
|
203
|
+
|
|
204
|
+
enum State {
|
|
205
|
+
Handshaking(HandshakeFut),
|
|
206
|
+
Ready(server::Connection<SharedIo, Bytes>),
|
|
207
|
+
Closed,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[pyclass(module = "rh2._rh2", unsendable)]
|
|
211
|
+
struct H2Connection {
|
|
212
|
+
io: SharedIo,
|
|
213
|
+
state: State,
|
|
214
|
+
recv_streams: HashMap<u32, RecvStream>,
|
|
215
|
+
responders: HashMap<u32, SendResponse<Bytes>>,
|
|
216
|
+
send_streams: HashMap<u32, SendStream<Bytes>>,
|
|
217
|
+
// Body bytes queued by the app, flushed as the peer's flow-control window allows.
|
|
218
|
+
pending_send: HashMap<u32, VecDeque<(Bytes, bool)>>,
|
|
219
|
+
events: VecDeque<PyObject>,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
impl H2Connection {
|
|
223
|
+
fn pump(&mut self, py: Python<'_>) -> PyResult<()> {
|
|
224
|
+
let waker = noop_waker();
|
|
225
|
+
let mut cx = Context::from_waker(&waker);
|
|
226
|
+
|
|
227
|
+
// 1. Complete the handshake if we're still waiting on the client preface.
|
|
228
|
+
if let State::Handshaking(fut) = &mut self.state {
|
|
229
|
+
match fut.as_mut().poll(&mut cx) {
|
|
230
|
+
Poll::Ready(Ok(conn)) => self.state = State::Ready(conn),
|
|
231
|
+
Poll::Ready(Err(e)) => {
|
|
232
|
+
self.state = State::Closed;
|
|
233
|
+
return Err(PyValueError::new_err(format!("handshake failed: {e}")));
|
|
234
|
+
}
|
|
235
|
+
Poll::Pending => return Ok(()),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if !matches!(self.state, State::Ready(_)) {
|
|
240
|
+
return Ok(());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Drive the connection until it stops making progress. `h2` only writes
|
|
244
|
+
// buffered frames to the transport while the connection is being polled,
|
|
245
|
+
// so a final no-progress pass is what flushes the last response bytes.
|
|
246
|
+
for _ in 0..64 {
|
|
247
|
+
let mut progress = self.drive(py, &mut cx)?;
|
|
248
|
+
if self.flush_pending(&mut cx) {
|
|
249
|
+
progress = true;
|
|
250
|
+
}
|
|
251
|
+
if !progress {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
Ok(())
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn drive(&mut self, py: Python<'_>, cx: &mut Context<'_>) -> PyResult<bool> {
|
|
259
|
+
// Progress is measured by whether the drive surfaced any new events;
|
|
260
|
+
// capacity-only advances are handled by `flush_pending` in the caller.
|
|
261
|
+
let before = self.events.len();
|
|
262
|
+
let conn = match &mut self.state {
|
|
263
|
+
State::Ready(conn) => conn,
|
|
264
|
+
_ => return Ok(false),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// 2. Accept any newly-arrived streams.
|
|
268
|
+
loop {
|
|
269
|
+
match conn.poll_accept(cx) {
|
|
270
|
+
Poll::Ready(Some(Ok((request, respond)))) => {
|
|
271
|
+
let stream_id = respond.stream_id().as_u32();
|
|
272
|
+
let (parts, recv) = request.into_parts();
|
|
273
|
+
|
|
274
|
+
// Reconstruct the HTTP/2 pseudo-headers the app expects, then
|
|
275
|
+
// the regular headers, as a list of (name, value) byte tuples.
|
|
276
|
+
let headers = PyList::empty_bound(py);
|
|
277
|
+
let mut push = |name: &[u8], value: &[u8]| {
|
|
278
|
+
let pair = (PyBytes::new_bound(py, name), PyBytes::new_bound(py, value));
|
|
279
|
+
headers.append(pair).unwrap();
|
|
280
|
+
};
|
|
281
|
+
push(b":method", parts.method.as_str().as_bytes());
|
|
282
|
+
if let Some(scheme) = parts.uri.scheme_str() {
|
|
283
|
+
push(b":scheme", scheme.as_bytes());
|
|
284
|
+
}
|
|
285
|
+
if let Some(authority) = parts.uri.authority() {
|
|
286
|
+
push(b":authority", authority.as_str().as_bytes());
|
|
287
|
+
}
|
|
288
|
+
let path = parts
|
|
289
|
+
.uri
|
|
290
|
+
.path_and_query()
|
|
291
|
+
.map(|pq| pq.as_str())
|
|
292
|
+
.unwrap_or("/");
|
|
293
|
+
push(b":path", path.as_bytes());
|
|
294
|
+
for (name, value) in parts.headers.iter() {
|
|
295
|
+
push(name.as_str().as_bytes(), value.as_bytes());
|
|
296
|
+
}
|
|
297
|
+
let headers: Py<PyList> = headers.into();
|
|
298
|
+
|
|
299
|
+
let stream_ended = recv.is_end_stream();
|
|
300
|
+
self.recv_streams.insert(stream_id, recv);
|
|
301
|
+
self.responders.insert(stream_id, respond);
|
|
302
|
+
let event = RequestReceived { stream_id, headers, stream_ended };
|
|
303
|
+
self.events.push_back(event.into_py(py));
|
|
304
|
+
}
|
|
305
|
+
Poll::Ready(Some(Err(e))) => {
|
|
306
|
+
self.state = State::Closed;
|
|
307
|
+
let event = ConnectionTerminated {};
|
|
308
|
+
self.events.push_back(event.into_py(py));
|
|
309
|
+
let _ = e;
|
|
310
|
+
return Ok(self.events.len() != before);
|
|
311
|
+
}
|
|
312
|
+
Poll::Ready(None) => {
|
|
313
|
+
let event = ConnectionTerminated {};
|
|
314
|
+
self.events.push_back(event.into_py(py));
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
Poll::Pending => break,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 3. Read body data from each open receive stream.
|
|
322
|
+
let ids: Vec<u32> = self.recv_streams.keys().copied().collect();
|
|
323
|
+
for stream_id in ids {
|
|
324
|
+
loop {
|
|
325
|
+
let recv = match self.recv_streams.get_mut(&stream_id) {
|
|
326
|
+
Some(r) => r,
|
|
327
|
+
None => break,
|
|
328
|
+
};
|
|
329
|
+
match recv.poll_data(cx) {
|
|
330
|
+
Poll::Ready(Some(Ok(chunk))) => {
|
|
331
|
+
// Immediately release the connection-level window; the app
|
|
332
|
+
// acknowledges stream-level capacity via `acknowledge_data`.
|
|
333
|
+
let _ = recv.flow_control().release_capacity(chunk.len());
|
|
334
|
+
let stream_ended = recv.is_end_stream();
|
|
335
|
+
let event = DataReceived {
|
|
336
|
+
stream_id,
|
|
337
|
+
data: PyBytes::new_bound(py, &chunk).into(),
|
|
338
|
+
stream_ended,
|
|
339
|
+
};
|
|
340
|
+
self.events.push_back(event.into_py(py));
|
|
341
|
+
}
|
|
342
|
+
Poll::Ready(Some(Err(e))) => {
|
|
343
|
+
let error_code =
|
|
344
|
+
e.reason().map(|r| u32::from(r)).unwrap_or(0);
|
|
345
|
+
self.recv_streams.remove(&stream_id);
|
|
346
|
+
let event = StreamReset { stream_id, error_code };
|
|
347
|
+
self.events.push_back(event.into_py(py));
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
Poll::Ready(None) => {
|
|
351
|
+
self.recv_streams.remove(&stream_id);
|
|
352
|
+
let event = StreamEnded { stream_id };
|
|
353
|
+
self.events.push_back(event.into_py(py));
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
Poll::Pending => break,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
Ok(self.events.len() != before)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn flush_pending(&mut self, cx: &mut Context<'_>) -> bool {
|
|
365
|
+
let mut sent = false;
|
|
366
|
+
let ids: Vec<u32> = self.pending_send.keys().copied().collect();
|
|
367
|
+
for stream_id in ids {
|
|
368
|
+
loop {
|
|
369
|
+
let queue = match self.pending_send.get_mut(&stream_id) {
|
|
370
|
+
Some(q) if !q.is_empty() => q,
|
|
371
|
+
_ => break,
|
|
372
|
+
};
|
|
373
|
+
let send = match self.send_streams.get_mut(&stream_id) {
|
|
374
|
+
Some(s) => s,
|
|
375
|
+
None => break,
|
|
376
|
+
};
|
|
377
|
+
let (chunk, end_stream) = queue.front().cloned().unwrap();
|
|
378
|
+
if chunk.is_empty() {
|
|
379
|
+
let _ = send.send_data(Bytes::new(), end_stream);
|
|
380
|
+
queue.pop_front();
|
|
381
|
+
sent = true;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
send.reserve_capacity(chunk.len());
|
|
385
|
+
match send.poll_capacity(cx) {
|
|
386
|
+
Poll::Ready(Some(Ok(cap))) if cap > 0 => {
|
|
387
|
+
let take = std::cmp::min(cap, chunk.len());
|
|
388
|
+
let to_send = chunk.slice(0..take);
|
|
389
|
+
let last = take == chunk.len();
|
|
390
|
+
let _ = send.send_data(to_send, last && end_stream);
|
|
391
|
+
if last {
|
|
392
|
+
queue.pop_front();
|
|
393
|
+
} else {
|
|
394
|
+
queue[0] = (chunk.slice(take..), end_stream);
|
|
395
|
+
}
|
|
396
|
+
sent = true;
|
|
397
|
+
}
|
|
398
|
+
Poll::Ready(Some(Ok(_))) => break,
|
|
399
|
+
Poll::Ready(Some(Err(_))) | Poll::Ready(None) => {
|
|
400
|
+
queue.clear();
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
Poll::Pending => break,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
sent
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#[pymethods]
|
|
412
|
+
impl H2Connection {
|
|
413
|
+
#[new]
|
|
414
|
+
fn new() -> Self {
|
|
415
|
+
let io = SharedIo::new();
|
|
416
|
+
let fut: HandshakeFut = Box::pin(server::handshake(io.clone()));
|
|
417
|
+
H2Connection {
|
|
418
|
+
io,
|
|
419
|
+
state: State::Handshaking(fut),
|
|
420
|
+
recv_streams: HashMap::new(),
|
|
421
|
+
responders: HashMap::new(),
|
|
422
|
+
send_streams: HashMap::new(),
|
|
423
|
+
pending_send: HashMap::new(),
|
|
424
|
+
events: VecDeque::new(),
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// Feed bytes received from the peer; returns the events they produced.
|
|
429
|
+
fn receive_data(&mut self, py: Python<'_>, data: &[u8]) -> PyResult<Vec<PyObject>> {
|
|
430
|
+
self.io.0.borrow_mut().push_inbound(data);
|
|
431
|
+
self.pump(py)?;
|
|
432
|
+
Ok(self.events.drain(..).collect())
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// Signal that the peer closed its side of the connection (EOF).
|
|
436
|
+
fn receive_eof(&mut self, py: Python<'_>) -> PyResult<Vec<PyObject>> {
|
|
437
|
+
self.io.0.borrow_mut().inbound_closed = true;
|
|
438
|
+
self.pump(py)?;
|
|
439
|
+
Ok(self.events.drain(..).collect())
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/// Drain bytes that must be written to the peer.
|
|
443
|
+
///
|
|
444
|
+
/// The state-changing calls (`send_headers`, `send_data`, ...) only mutate
|
|
445
|
+
/// local state; the connection is driven here, so a burst of them coalesces
|
|
446
|
+
/// into a single pump instead of re-driving the whole machine per call.
|
|
447
|
+
fn data_to_send<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
|
|
448
|
+
self.pump(py)?;
|
|
449
|
+
let mut inner = self.io.0.borrow_mut();
|
|
450
|
+
let out = std::mem::take(&mut inner.outbound);
|
|
451
|
+
Ok(PyBytes::new_bound(py, &out))
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/// Send response headers on a stream. `headers` is a list of (name, value) byte pairs;
|
|
455
|
+
/// the `:status` pseudo-header supplies the status code.
|
|
456
|
+
#[pyo3(signature = (stream_id, status, headers, end_stream=false))]
|
|
457
|
+
fn send_headers(
|
|
458
|
+
&mut self,
|
|
459
|
+
stream_id: u32,
|
|
460
|
+
status: u16,
|
|
461
|
+
headers: Vec<(Vec<u8>, Vec<u8>)>,
|
|
462
|
+
end_stream: bool,
|
|
463
|
+
) -> PyResult<()> {
|
|
464
|
+
let respond = self
|
|
465
|
+
.responders
|
|
466
|
+
.remove(&stream_id)
|
|
467
|
+
.ok_or_else(|| PyValueError::new_err(format!("unknown stream {stream_id}")))?;
|
|
468
|
+
let mut response = Response::builder()
|
|
469
|
+
.status(StatusCode::from_u16(status).map_err(|e| PyValueError::new_err(e.to_string()))?);
|
|
470
|
+
{
|
|
471
|
+
let hdrs = response.headers_mut().unwrap();
|
|
472
|
+
for (name, value) in headers {
|
|
473
|
+
if name.first() == Some(&b':') {
|
|
474
|
+
continue; // pseudo-headers are set from `status`
|
|
475
|
+
}
|
|
476
|
+
let hn = HeaderName::from_bytes(&name)
|
|
477
|
+
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
478
|
+
let hv = HeaderValue::from_bytes(&value)
|
|
479
|
+
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
480
|
+
hdrs.append(hn, hv);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
let response = response
|
|
484
|
+
.body(())
|
|
485
|
+
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
486
|
+
let mut respond = respond;
|
|
487
|
+
let send = respond
|
|
488
|
+
.send_response(response, end_stream)
|
|
489
|
+
.map_err(|e| PyValueError::new_err(format!("send_response failed: {e}")))?;
|
|
490
|
+
if !end_stream {
|
|
491
|
+
self.send_streams.insert(stream_id, send);
|
|
492
|
+
}
|
|
493
|
+
Ok(())
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
#[pyo3(signature = (stream_id, data, end_stream=false))]
|
|
497
|
+
fn send_data(&mut self, stream_id: u32, data: &[u8], end_stream: bool) -> PyResult<()> {
|
|
498
|
+
// Take `&[u8]` (buffer protocol, zero-copy view) rather than `Vec<u8>`,
|
|
499
|
+
// which PyO3 would fill element-by-element - O(n) Python int conversions.
|
|
500
|
+
self.pending_send
|
|
501
|
+
.entry(stream_id)
|
|
502
|
+
.or_default()
|
|
503
|
+
.push_back((Bytes::copy_from_slice(data), end_stream));
|
|
504
|
+
Ok(())
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
#[pyo3(signature = (stream_id, error_code=8))]
|
|
508
|
+
fn reset_stream(&mut self, stream_id: u32, error_code: u32) -> PyResult<()> {
|
|
509
|
+
if let Some(mut send) = self.send_streams.remove(&stream_id) {
|
|
510
|
+
send.send_reset(h2::Reason::from(error_code));
|
|
511
|
+
}
|
|
512
|
+
self.responders.remove(&stream_id);
|
|
513
|
+
self.recv_streams.remove(&stream_id);
|
|
514
|
+
self.pending_send.remove(&stream_id);
|
|
515
|
+
Ok(())
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/// Acknowledge that the app consumed `size` bytes of a request body, refilling
|
|
519
|
+
/// the stream-level flow-control window.
|
|
520
|
+
fn acknowledge_data(&mut self, stream_id: u32, size: usize) -> PyResult<()> {
|
|
521
|
+
if let Some(recv) = self.recv_streams.get_mut(&stream_id) {
|
|
522
|
+
let _ = recv.flow_control().release_capacity(size);
|
|
523
|
+
}
|
|
524
|
+
Ok(())
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/// Begin a graceful shutdown (send GOAWAY).
|
|
528
|
+
fn close_connection(&mut self) -> PyResult<()> {
|
|
529
|
+
if let State::Ready(conn) = &mut self.state {
|
|
530
|
+
conn.graceful_shutdown();
|
|
531
|
+
}
|
|
532
|
+
Ok(())
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#[pymodule]
|
|
537
|
+
fn _rh2(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
538
|
+
m.add_class::<H2Connection>()?;
|
|
539
|
+
m.add_class::<RequestReceived>()?;
|
|
540
|
+
m.add_class::<DataReceived>()?;
|
|
541
|
+
m.add_class::<StreamEnded>()?;
|
|
542
|
+
m.add_class::<StreamReset>()?;
|
|
543
|
+
m.add_class::<ConnectionTerminated>()?;
|
|
544
|
+
Ok(())
|
|
545
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Drive the rh2 server codec with hyper-h2 acting as the client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import h2.config
|
|
6
|
+
import h2.connection
|
|
7
|
+
import h2.events
|
|
8
|
+
|
|
9
|
+
import rh2
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _pump(server: rh2.H2Connection, client: h2.connection.H2Connection) -> list:
|
|
13
|
+
"""Ferry bytes both ways until neither side has more to send."""
|
|
14
|
+
events: list = []
|
|
15
|
+
for _ in range(20):
|
|
16
|
+
moved = False
|
|
17
|
+
client_out = client.data_to_send()
|
|
18
|
+
if client_out:
|
|
19
|
+
events += server.receive_data(client_out)
|
|
20
|
+
moved = True
|
|
21
|
+
server_out = server.data_to_send()
|
|
22
|
+
if server_out:
|
|
23
|
+
events += client.receive_data(server_out)
|
|
24
|
+
moved = True
|
|
25
|
+
if not moved:
|
|
26
|
+
break
|
|
27
|
+
return events
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_request_response_roundtrip() -> None:
|
|
31
|
+
server = rh2.H2Connection()
|
|
32
|
+
client = h2.connection.H2Connection(config=h2.config.H2Configuration(client_side=True))
|
|
33
|
+
client.initiate_connection()
|
|
34
|
+
|
|
35
|
+
stream_id = client.get_next_available_stream_id()
|
|
36
|
+
client.send_headers(
|
|
37
|
+
stream_id,
|
|
38
|
+
[
|
|
39
|
+
(b":method", b"GET"),
|
|
40
|
+
(b":scheme", b"http"),
|
|
41
|
+
(b":authority", b"example.com"),
|
|
42
|
+
(b":path", b"/hello"),
|
|
43
|
+
],
|
|
44
|
+
end_stream=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
events = _pump(server, client)
|
|
48
|
+
reqs = [e for e in events if isinstance(e, rh2.RequestReceived)]
|
|
49
|
+
assert len(reqs) == 1
|
|
50
|
+
req = reqs[0]
|
|
51
|
+
assert req.stream_id == stream_id
|
|
52
|
+
assert req.stream_ended is True
|
|
53
|
+
header_map = {bytes(k): bytes(v) for k, v in req.headers}
|
|
54
|
+
assert header_map[b":method"] == b"GET"
|
|
55
|
+
assert header_map[b":path"] == b"/hello"
|
|
56
|
+
|
|
57
|
+
server.send_headers(stream_id, 200, [(b"content-type", b"text/plain")], end_stream=False)
|
|
58
|
+
server.send_data(stream_id, b"hello world", end_stream=True)
|
|
59
|
+
|
|
60
|
+
client_events = _pump(server, client)
|
|
61
|
+
got_response = any(isinstance(e, h2.events.ResponseReceived) for e in client_events)
|
|
62
|
+
got_data = b"".join(
|
|
63
|
+
e.data for e in client_events if isinstance(e, h2.events.DataReceived)
|
|
64
|
+
)
|
|
65
|
+
assert got_response
|
|
66
|
+
assert got_data == b"hello world"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_request_body_flow_control() -> None:
|
|
70
|
+
server = rh2.H2Connection()
|
|
71
|
+
client = h2.connection.H2Connection(config=h2.config.H2Configuration(client_side=True))
|
|
72
|
+
client.initiate_connection()
|
|
73
|
+
|
|
74
|
+
stream_id = client.get_next_available_stream_id()
|
|
75
|
+
client.send_headers(
|
|
76
|
+
stream_id,
|
|
77
|
+
[
|
|
78
|
+
(b":method", b"POST"),
|
|
79
|
+
(b":scheme", b"http"),
|
|
80
|
+
(b":authority", b"example.com"),
|
|
81
|
+
(b":path", b"/upload"),
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
body = b"x" * (100 * 1024) # larger than the default 64 KiB window
|
|
85
|
+
# Send within the client's available window, then rely on WINDOW_UPDATE.
|
|
86
|
+
offset = 0
|
|
87
|
+
events: list = []
|
|
88
|
+
while offset < len(body):
|
|
89
|
+
window = client.local_flow_control_window(stream_id)
|
|
90
|
+
if window == 0:
|
|
91
|
+
events += _pump(server, client)
|
|
92
|
+
continue
|
|
93
|
+
chunk = body[offset : offset + min(window, 16384)]
|
|
94
|
+
client.send_data(stream_id, chunk)
|
|
95
|
+
offset += len(chunk)
|
|
96
|
+
client.end_stream(stream_id)
|
|
97
|
+
events += _pump(server, client)
|
|
98
|
+
|
|
99
|
+
received = b"".join(
|
|
100
|
+
bytes(e.data) for e in events if isinstance(e, rh2.DataReceived)
|
|
101
|
+
)
|
|
102
|
+
assert received == body
|
|
103
|
+
assert any(isinstance(e, rh2.StreamEnded) for e in events)
|