tigrbl-kernel 0.4.2.dev4__py3-none-any.whl → 0.4.3.dev4__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.
- tigrbl_kernel/__init__.py +1 -19
- tigrbl_kernel/_build.py +1 -1
- tigrbl_kernel/_compile.py +100 -1
- tigrbl_kernel/channel_taxonomy.py +142 -0
- tigrbl_kernel/cross_transport.py +167 -0
- tigrbl_kernel/dispatch_taxonomy.py +47 -0
- tigrbl_kernel/events.py +10 -0
- tigrbl_kernel/helpers.py +2 -2
- tigrbl_kernel/loop_modes.py +19 -1
- tigrbl_kernel/measure.py +1 -1
- tigrbl_kernel/ordering.py +1 -0
- tigrbl_kernel/packed_access.py +131 -0
- tigrbl_kernel/packed_selectors.py +239 -0
- tigrbl_kernel/protocol_anchors.py +49 -0
- tigrbl_kernel/protocol_bindings.py +26 -7
- tigrbl_kernel/protocol_chains/http_stream.py +35 -2
- tigrbl_kernel/protocol_streams.py +470 -0
- tigrbl_kernel/resume_policy.py +78 -0
- tigrbl_kernel/runtime_contracts.py +254 -0
- tigrbl_kernel/rust_compile.py +11 -27
- tigrbl_kernel/rust_plan.py +9 -3
- tigrbl_kernel/rust_spec.py +15 -398
- tigrbl_kernel/transport_events.py +5 -2
- tigrbl_kernel/types.py +1 -0
- tigrbl_kernel/webtransport_events.py +65 -3
- {tigrbl_kernel-0.4.2.dev4.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/METADATA +74 -5
- {tigrbl_kernel-0.4.2.dev4.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/RECORD +30 -21
- {tigrbl_kernel-0.4.2.dev4.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/WHEEL +0 -0
- {tigrbl_kernel-0.4.2.dev4.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
- {tigrbl_kernel-0.4.2.dev4.dist-info → tigrbl_kernel-0.4.3.dev4.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
_INITIATORS = {"client", "server"}
|
|
6
|
+
_DIRECTIONS = {"client_to_server", "server_to_client", "bidirectional", "control"}
|
|
7
|
+
_EXTENSION_CONTEXTS = {"connect", "tunnel", "websocket", "extension"}
|
|
8
|
+
|
|
9
|
+
_H11_REQUEST_BODY = {"request_body", "http_request_body", "http_body_request"}
|
|
10
|
+
_H11_RESPONSE_BODY = {"response_body", "http_response_body", "http_body_response"}
|
|
11
|
+
_H11_BODY = _H11_REQUEST_BODY | _H11_RESPONSE_BODY | {"http_body"}
|
|
12
|
+
|
|
13
|
+
_H2_REQUEST = {"request_stream", "h2_request_stream", "http2_request_stream"}
|
|
14
|
+
_H2_PUSH = {"push_stream", "h2_push_stream", "http2_push_stream"}
|
|
15
|
+
|
|
16
|
+
_H3_REQUEST = {"request_stream", "h3_request_stream", "http3_request_stream"}
|
|
17
|
+
_H3_PUSH = {"push_stream", "h3_push_stream", "http3_push_stream"}
|
|
18
|
+
_H3_CONTROL = {
|
|
19
|
+
"control_stream",
|
|
20
|
+
"qpack_encoder_stream",
|
|
21
|
+
"qpack_decoder_stream",
|
|
22
|
+
"h3_control_stream",
|
|
23
|
+
"h3_qpack_encoder_stream",
|
|
24
|
+
"h3_qpack_decoder_stream",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_WT_STREAM = {"webtransport_stream", "wt_stream"}
|
|
28
|
+
_WT_BIDI = {"bidi_stream", "webtransport_bidi_stream", "wt_bidi_stream"}
|
|
29
|
+
_WT_UNIDI_CLIENT = {
|
|
30
|
+
"unidi_client_stream",
|
|
31
|
+
"webtransport_unidi_client_stream",
|
|
32
|
+
"wt_unidi_client_stream",
|
|
33
|
+
}
|
|
34
|
+
_WT_UNIDI_SERVER = {
|
|
35
|
+
"unidi_server_stream",
|
|
36
|
+
"webtransport_unidi_server_stream",
|
|
37
|
+
"wt_unidi_server_stream",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_protocol_stream_shape(
|
|
42
|
+
*,
|
|
43
|
+
protocol: str,
|
|
44
|
+
carrier_kind: str,
|
|
45
|
+
exchange: str,
|
|
46
|
+
stream_initiator: str | None = None,
|
|
47
|
+
direction: str | None = None,
|
|
48
|
+
extension_context: str | None = None,
|
|
49
|
+
) -> dict[str, object]:
|
|
50
|
+
"""Validate protocol stream legality before app frame-codec dispatch."""
|
|
51
|
+
|
|
52
|
+
selected_protocol = _normalize_protocol(protocol)
|
|
53
|
+
selected_carrier = str(carrier_kind)
|
|
54
|
+
selected_exchange = _normalize_exchange(exchange)
|
|
55
|
+
selected_extension = _normalize_optional(extension_context)
|
|
56
|
+
|
|
57
|
+
if selected_protocol == "h11":
|
|
58
|
+
return _validate_h11(
|
|
59
|
+
carrier_kind=selected_carrier,
|
|
60
|
+
exchange=selected_exchange,
|
|
61
|
+
stream_initiator=stream_initiator,
|
|
62
|
+
direction=direction,
|
|
63
|
+
)
|
|
64
|
+
if selected_protocol == "h2":
|
|
65
|
+
return _validate_h2(
|
|
66
|
+
carrier_kind=selected_carrier,
|
|
67
|
+
exchange=selected_exchange,
|
|
68
|
+
stream_initiator=stream_initiator,
|
|
69
|
+
direction=direction,
|
|
70
|
+
extension_context=selected_extension,
|
|
71
|
+
)
|
|
72
|
+
if selected_protocol == "h3":
|
|
73
|
+
return _validate_h3(
|
|
74
|
+
carrier_kind=selected_carrier,
|
|
75
|
+
exchange=selected_exchange,
|
|
76
|
+
stream_initiator=stream_initiator,
|
|
77
|
+
direction=direction,
|
|
78
|
+
extension_context=selected_extension,
|
|
79
|
+
)
|
|
80
|
+
if selected_protocol == "webtransport":
|
|
81
|
+
return _validate_webtransport(
|
|
82
|
+
carrier_kind=selected_carrier,
|
|
83
|
+
exchange=selected_exchange,
|
|
84
|
+
stream_initiator=stream_initiator,
|
|
85
|
+
direction=direction,
|
|
86
|
+
)
|
|
87
|
+
raise ValueError(f"unsupported stream protocol {protocol!r}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def classify_plain_h3_stream(
|
|
91
|
+
*,
|
|
92
|
+
stream_type: str,
|
|
93
|
+
initiator: str | None = None,
|
|
94
|
+
extension_context: str | None = None,
|
|
95
|
+
) -> dict[str, object]:
|
|
96
|
+
"""Classify HTTP/3 streams without borrowing WebTransport lane semantics."""
|
|
97
|
+
|
|
98
|
+
selected = str(stream_type)
|
|
99
|
+
selected_extension = _normalize_optional(extension_context)
|
|
100
|
+
if selected in _H3_REQUEST:
|
|
101
|
+
return _projection(
|
|
102
|
+
protocol="h3",
|
|
103
|
+
carrier_kind="h3_request_stream",
|
|
104
|
+
exchange="request_response",
|
|
105
|
+
stream_initiator=_expect("stream_initiator", initiator, "client"),
|
|
106
|
+
direction="bidirectional",
|
|
107
|
+
app_payload=True,
|
|
108
|
+
transport_stream=True,
|
|
109
|
+
response_only=False,
|
|
110
|
+
extension_context=selected_extension,
|
|
111
|
+
)
|
|
112
|
+
if selected in _H3_PUSH:
|
|
113
|
+
return _projection(
|
|
114
|
+
protocol="h3",
|
|
115
|
+
carrier_kind="h3_push_stream",
|
|
116
|
+
exchange="server_stream",
|
|
117
|
+
stream_initiator=_expect("stream_initiator", initiator, "server"),
|
|
118
|
+
direction="server_to_client",
|
|
119
|
+
app_payload=True,
|
|
120
|
+
transport_stream=True,
|
|
121
|
+
response_only=True,
|
|
122
|
+
extension_context=None,
|
|
123
|
+
)
|
|
124
|
+
if selected in _H3_CONTROL:
|
|
125
|
+
if initiator is not None and str(initiator) not in _INITIATORS:
|
|
126
|
+
raise ValueError("stream_initiator must be client or server")
|
|
127
|
+
return _projection(
|
|
128
|
+
protocol="h3",
|
|
129
|
+
carrier_kind=selected,
|
|
130
|
+
exchange="fire_and_forget",
|
|
131
|
+
stream_initiator=str(initiator) if initiator is not None else None,
|
|
132
|
+
direction="control",
|
|
133
|
+
app_payload=False,
|
|
134
|
+
transport_stream=True,
|
|
135
|
+
response_only=False,
|
|
136
|
+
extension_context=None,
|
|
137
|
+
)
|
|
138
|
+
if selected == "server_bidi_stream":
|
|
139
|
+
if selected_extension is None:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"plain h3 server-initiated bidirectional streams require an extension context"
|
|
142
|
+
)
|
|
143
|
+
if selected_extension == "webtransport":
|
|
144
|
+
raise ValueError("WebTransport-over-h3 is governed by WebTransport lanes")
|
|
145
|
+
return _projection(
|
|
146
|
+
protocol="h3",
|
|
147
|
+
carrier_kind="extension_bidi_stream",
|
|
148
|
+
exchange="bidirectional_stream",
|
|
149
|
+
stream_initiator=_expect("stream_initiator", initiator, "server"),
|
|
150
|
+
direction="bidirectional",
|
|
151
|
+
app_payload=True,
|
|
152
|
+
transport_stream=True,
|
|
153
|
+
response_only=False,
|
|
154
|
+
extension_context=selected_extension,
|
|
155
|
+
)
|
|
156
|
+
raise ValueError(f"unsupported plain h3 stream type {stream_type!r}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate_h11(
|
|
160
|
+
*,
|
|
161
|
+
carrier_kind: str,
|
|
162
|
+
exchange: str,
|
|
163
|
+
stream_initiator: str | None,
|
|
164
|
+
direction: str | None,
|
|
165
|
+
) -> dict[str, object]:
|
|
166
|
+
if carrier_kind not in _H11_BODY:
|
|
167
|
+
raise ValueError(f"unsupported h11 stream carrier {carrier_kind!r}")
|
|
168
|
+
if exchange == "client_stream" or carrier_kind in _H11_REQUEST_BODY:
|
|
169
|
+
if carrier_kind in _H11_RESPONSE_BODY:
|
|
170
|
+
raise ValueError("h11 response-body carrier cannot use client_stream")
|
|
171
|
+
return _projection(
|
|
172
|
+
protocol="h11",
|
|
173
|
+
carrier_kind="http_request_body",
|
|
174
|
+
exchange="client_stream",
|
|
175
|
+
stream_initiator=_expect("stream_initiator", stream_initiator, "client"),
|
|
176
|
+
direction=_expect("direction", direction, "client_to_server"),
|
|
177
|
+
app_payload=True,
|
|
178
|
+
transport_stream=False,
|
|
179
|
+
response_only=False,
|
|
180
|
+
extension_context=None,
|
|
181
|
+
)
|
|
182
|
+
if exchange == "server_stream" or carrier_kind in _H11_RESPONSE_BODY:
|
|
183
|
+
if carrier_kind in _H11_REQUEST_BODY:
|
|
184
|
+
raise ValueError("h11 request-body carrier cannot use server_stream")
|
|
185
|
+
return _projection(
|
|
186
|
+
protocol="h11",
|
|
187
|
+
carrier_kind="http_response_body",
|
|
188
|
+
exchange="server_stream",
|
|
189
|
+
stream_initiator=_expect("stream_initiator", stream_initiator, "server"),
|
|
190
|
+
direction=_expect("direction", direction, "server_to_client"),
|
|
191
|
+
app_payload=True,
|
|
192
|
+
transport_stream=False,
|
|
193
|
+
response_only=False,
|
|
194
|
+
extension_context=None,
|
|
195
|
+
)
|
|
196
|
+
raise ValueError("h11 does not provide native bidirectional application streams")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _validate_h2(
|
|
200
|
+
*,
|
|
201
|
+
carrier_kind: str,
|
|
202
|
+
exchange: str,
|
|
203
|
+
stream_initiator: str | None,
|
|
204
|
+
direction: str | None,
|
|
205
|
+
extension_context: str | None,
|
|
206
|
+
) -> dict[str, object]:
|
|
207
|
+
if carrier_kind in _H2_REQUEST:
|
|
208
|
+
if exchange == "bidirectional_stream" and extension_context is None:
|
|
209
|
+
raise ValueError("h2 bidirectional app streams require an extension context")
|
|
210
|
+
if exchange not in {
|
|
211
|
+
"request_response",
|
|
212
|
+
"client_stream",
|
|
213
|
+
"server_stream",
|
|
214
|
+
"bidirectional_stream",
|
|
215
|
+
}:
|
|
216
|
+
raise ValueError(f"unsupported h2 request stream exchange {exchange!r}")
|
|
217
|
+
return _projection(
|
|
218
|
+
protocol="h2",
|
|
219
|
+
carrier_kind="h2_request_stream",
|
|
220
|
+
exchange=exchange,
|
|
221
|
+
stream_initiator=_expect("stream_initiator", stream_initiator, "client"),
|
|
222
|
+
direction=_expect("direction", direction, _h2_direction(exchange)),
|
|
223
|
+
app_payload=True,
|
|
224
|
+
transport_stream=True,
|
|
225
|
+
response_only=False,
|
|
226
|
+
extension_context=extension_context,
|
|
227
|
+
)
|
|
228
|
+
if carrier_kind in _H2_PUSH:
|
|
229
|
+
if exchange != "server_stream":
|
|
230
|
+
raise ValueError("h2 push streams are response-only server streams")
|
|
231
|
+
return _projection(
|
|
232
|
+
protocol="h2",
|
|
233
|
+
carrier_kind="h2_push_stream",
|
|
234
|
+
exchange="server_stream",
|
|
235
|
+
stream_initiator=_expect("stream_initiator", stream_initiator, "server"),
|
|
236
|
+
direction=_expect("direction", direction, "server_to_client"),
|
|
237
|
+
app_payload=True,
|
|
238
|
+
transport_stream=True,
|
|
239
|
+
response_only=True,
|
|
240
|
+
extension_context=None,
|
|
241
|
+
)
|
|
242
|
+
if stream_initiator == "server" and exchange == "bidirectional_stream":
|
|
243
|
+
raise ValueError("plain h2 server-initiated bidirectional streams are invalid")
|
|
244
|
+
raise ValueError(f"unsupported h2 stream carrier {carrier_kind!r}")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _validate_h3(
|
|
248
|
+
*,
|
|
249
|
+
carrier_kind: str,
|
|
250
|
+
exchange: str,
|
|
251
|
+
stream_initiator: str | None,
|
|
252
|
+
direction: str | None,
|
|
253
|
+
extension_context: str | None,
|
|
254
|
+
) -> dict[str, object]:
|
|
255
|
+
if carrier_kind in _H3_CONTROL:
|
|
256
|
+
classified = classify_plain_h3_stream(
|
|
257
|
+
stream_type=carrier_kind,
|
|
258
|
+
initiator=stream_initiator,
|
|
259
|
+
)
|
|
260
|
+
if exchange != "fire_and_forget":
|
|
261
|
+
raise ValueError("h3 control and qpack streams are not app exchange carriers")
|
|
262
|
+
return classified
|
|
263
|
+
if carrier_kind in _H3_PUSH:
|
|
264
|
+
if exchange != "server_stream":
|
|
265
|
+
raise ValueError("h3 push streams are response-only server streams")
|
|
266
|
+
classified = classify_plain_h3_stream(
|
|
267
|
+
stream_type=carrier_kind,
|
|
268
|
+
initiator=stream_initiator,
|
|
269
|
+
)
|
|
270
|
+
if direction is not None and direction != classified["direction"]:
|
|
271
|
+
raise ValueError("h3 push stream direction mismatch")
|
|
272
|
+
return classified
|
|
273
|
+
if carrier_kind in _H3_REQUEST:
|
|
274
|
+
if exchange == "bidirectional_stream" and extension_context is None:
|
|
275
|
+
raise ValueError("plain h3 bidirectional app streams require an extension context")
|
|
276
|
+
if exchange not in {
|
|
277
|
+
"request_response",
|
|
278
|
+
"client_stream",
|
|
279
|
+
"server_stream",
|
|
280
|
+
"bidirectional_stream",
|
|
281
|
+
}:
|
|
282
|
+
raise ValueError(f"unsupported h3 request stream exchange {exchange!r}")
|
|
283
|
+
return _projection(
|
|
284
|
+
protocol="h3",
|
|
285
|
+
carrier_kind="h3_request_stream",
|
|
286
|
+
exchange=exchange,
|
|
287
|
+
stream_initiator=_expect("stream_initiator", stream_initiator, "client"),
|
|
288
|
+
direction=_expect("direction", direction, _h2_direction(exchange)),
|
|
289
|
+
app_payload=True,
|
|
290
|
+
transport_stream=True,
|
|
291
|
+
response_only=False,
|
|
292
|
+
extension_context=extension_context,
|
|
293
|
+
)
|
|
294
|
+
if carrier_kind == "server_bidi_stream":
|
|
295
|
+
if extension_context is None:
|
|
296
|
+
raise ValueError(
|
|
297
|
+
"plain h3 server-initiated bidirectional streams require an extension context"
|
|
298
|
+
)
|
|
299
|
+
if extension_context == "webtransport":
|
|
300
|
+
raise ValueError("WebTransport-over-h3 is governed by WebTransport lanes")
|
|
301
|
+
return _projection(
|
|
302
|
+
protocol="h3",
|
|
303
|
+
carrier_kind="extension_bidi_stream",
|
|
304
|
+
exchange="bidirectional_stream",
|
|
305
|
+
stream_initiator=_expect("stream_initiator", stream_initiator, "server"),
|
|
306
|
+
direction=_expect("direction", direction, "bidirectional"),
|
|
307
|
+
app_payload=True,
|
|
308
|
+
transport_stream=True,
|
|
309
|
+
response_only=False,
|
|
310
|
+
extension_context=extension_context,
|
|
311
|
+
)
|
|
312
|
+
raise ValueError(f"unsupported plain h3 stream carrier {carrier_kind!r}")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _validate_webtransport(
|
|
316
|
+
*,
|
|
317
|
+
carrier_kind: str,
|
|
318
|
+
exchange: str,
|
|
319
|
+
stream_initiator: str | None,
|
|
320
|
+
direction: str | None,
|
|
321
|
+
) -> dict[str, object]:
|
|
322
|
+
if carrier_kind in _WT_STREAM:
|
|
323
|
+
if exchange == "bidirectional_stream":
|
|
324
|
+
carrier_kind = "bidi_stream"
|
|
325
|
+
elif exchange == "client_stream":
|
|
326
|
+
carrier_kind = "unidi_client_stream"
|
|
327
|
+
elif exchange == "server_stream":
|
|
328
|
+
carrier_kind = "unidi_server_stream"
|
|
329
|
+
if carrier_kind in _WT_BIDI:
|
|
330
|
+
initiator = _normalize_initiator(stream_initiator)
|
|
331
|
+
if initiator is None:
|
|
332
|
+
raise ValueError("WebTransport bidirectional streams require stream_initiator")
|
|
333
|
+
return _projection(
|
|
334
|
+
protocol="webtransport",
|
|
335
|
+
carrier_kind="bidi_stream",
|
|
336
|
+
exchange=_expect("exchange", exchange, "bidirectional_stream"),
|
|
337
|
+
stream_initiator=initiator,
|
|
338
|
+
direction=_expect("direction", direction, "bidirectional"),
|
|
339
|
+
app_payload=True,
|
|
340
|
+
transport_stream=True,
|
|
341
|
+
response_only=False,
|
|
342
|
+
extension_context=None,
|
|
343
|
+
)
|
|
344
|
+
if carrier_kind in _WT_UNIDI_CLIENT:
|
|
345
|
+
return _projection(
|
|
346
|
+
protocol="webtransport",
|
|
347
|
+
carrier_kind="unidi_client_stream",
|
|
348
|
+
exchange=_expect("exchange", exchange, "client_stream"),
|
|
349
|
+
stream_initiator=_expect("stream_initiator", stream_initiator, "client"),
|
|
350
|
+
direction=_expect("direction", direction, "client_to_server"),
|
|
351
|
+
app_payload=True,
|
|
352
|
+
transport_stream=True,
|
|
353
|
+
response_only=False,
|
|
354
|
+
extension_context=None,
|
|
355
|
+
)
|
|
356
|
+
if carrier_kind in _WT_UNIDI_SERVER:
|
|
357
|
+
return _projection(
|
|
358
|
+
protocol="webtransport",
|
|
359
|
+
carrier_kind="unidi_server_stream",
|
|
360
|
+
exchange=_expect("exchange", exchange, "server_stream"),
|
|
361
|
+
stream_initiator=_expect("stream_initiator", stream_initiator, "server"),
|
|
362
|
+
direction=_expect("direction", direction, "server_to_client"),
|
|
363
|
+
app_payload=True,
|
|
364
|
+
transport_stream=True,
|
|
365
|
+
response_only=False,
|
|
366
|
+
extension_context=None,
|
|
367
|
+
)
|
|
368
|
+
raise ValueError(f"unsupported WebTransport stream carrier {carrier_kind!r}")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _normalize_protocol(protocol: str) -> str:
|
|
372
|
+
token = str(protocol).lower().replace("_", "-")
|
|
373
|
+
aliases = {
|
|
374
|
+
"http/1.1": "h11",
|
|
375
|
+
"http1.1": "h11",
|
|
376
|
+
"http-1.1": "h11",
|
|
377
|
+
"h11": "h11",
|
|
378
|
+
"http/2": "h2",
|
|
379
|
+
"http2": "h2",
|
|
380
|
+
"h2": "h2",
|
|
381
|
+
"http/3": "h3",
|
|
382
|
+
"http3": "h3",
|
|
383
|
+
"h3": "h3",
|
|
384
|
+
"webtransport": "webtransport",
|
|
385
|
+
"wt": "webtransport",
|
|
386
|
+
}
|
|
387
|
+
return aliases.get(token, token)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _normalize_exchange(exchange: str) -> str:
|
|
391
|
+
token = str(exchange)
|
|
392
|
+
aliases = {
|
|
393
|
+
"unary": "request_response",
|
|
394
|
+
"duplex": "bidirectional_stream",
|
|
395
|
+
"bidirectional": "bidirectional_stream",
|
|
396
|
+
}
|
|
397
|
+
return aliases.get(token, token)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _normalize_optional(value: str | None) -> str | None:
|
|
401
|
+
if value is None or value == "":
|
|
402
|
+
return None
|
|
403
|
+
token = str(value)
|
|
404
|
+
if token not in _EXTENSION_CONTEXTS and token != "webtransport":
|
|
405
|
+
raise ValueError(f"unsupported stream extension context {value!r}")
|
|
406
|
+
return token
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _normalize_initiator(value: str | None) -> str | None:
|
|
410
|
+
if value is None:
|
|
411
|
+
return None
|
|
412
|
+
token = str(value)
|
|
413
|
+
if token not in _INITIATORS:
|
|
414
|
+
raise ValueError("stream_initiator must be client or server")
|
|
415
|
+
return token
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _expect(field: str, value: str | None, expected: str) -> str:
|
|
419
|
+
if value is None:
|
|
420
|
+
return expected
|
|
421
|
+
token = str(value)
|
|
422
|
+
if token != expected:
|
|
423
|
+
raise ValueError(f"{field} must be {expected}")
|
|
424
|
+
return token
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _h2_direction(exchange: str) -> str:
|
|
428
|
+
if exchange == "client_stream":
|
|
429
|
+
return "client_to_server"
|
|
430
|
+
if exchange == "server_stream":
|
|
431
|
+
return "server_to_client"
|
|
432
|
+
return "bidirectional"
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _projection(
|
|
436
|
+
*,
|
|
437
|
+
protocol: str,
|
|
438
|
+
carrier_kind: str,
|
|
439
|
+
exchange: str,
|
|
440
|
+
stream_initiator: str | None,
|
|
441
|
+
direction: str,
|
|
442
|
+
app_payload: bool,
|
|
443
|
+
transport_stream: bool,
|
|
444
|
+
response_only: bool,
|
|
445
|
+
extension_context: str | None,
|
|
446
|
+
) -> dict[str, object]:
|
|
447
|
+
if stream_initiator is not None and stream_initiator not in _INITIATORS:
|
|
448
|
+
raise ValueError("stream_initiator must be client or server")
|
|
449
|
+
if direction not in _DIRECTIONS:
|
|
450
|
+
raise ValueError("direction must be client_to_server, server_to_client, bidirectional, or control")
|
|
451
|
+
out: dict[str, object] = {
|
|
452
|
+
"protocol": protocol,
|
|
453
|
+
"carrier_kind": carrier_kind,
|
|
454
|
+
"exchange": exchange,
|
|
455
|
+
"stream_initiator": stream_initiator,
|
|
456
|
+
"direction": direction,
|
|
457
|
+
"family": "stream" if direction != "control" else "control",
|
|
458
|
+
"app_payload": app_payload,
|
|
459
|
+
"transport_stream": transport_stream,
|
|
460
|
+
"response_only": response_only,
|
|
461
|
+
}
|
|
462
|
+
if extension_context is not None:
|
|
463
|
+
out["extension_context"] = extension_context
|
|
464
|
+
return out
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
__all__ = [
|
|
468
|
+
"classify_plain_h3_stream",
|
|
469
|
+
"validate_protocol_stream_shape",
|
|
470
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RESUME_CAPABLE_BINDINGS = frozenset(
|
|
9
|
+
{"http.stream", "https.stream", "http.sse", "https.sse", "ws", "wss", "webtransport"}
|
|
10
|
+
)
|
|
11
|
+
RESUME_MODES = frozenset({"disabled", "cursor", "stateful", "replay"})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ResumePolicy:
|
|
16
|
+
mode: str = "disabled"
|
|
17
|
+
token_field: str = "resume_token"
|
|
18
|
+
offset_field: str = "requested_offset"
|
|
19
|
+
replay_window: int = 0
|
|
20
|
+
ttl_seconds: int | None = None
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
if self.mode not in RESUME_MODES:
|
|
24
|
+
raise ValueError(f"unsupported resume mode {self.mode!r}")
|
|
25
|
+
if not self.token_field:
|
|
26
|
+
raise ValueError("resume token_field is required")
|
|
27
|
+
if not self.offset_field:
|
|
28
|
+
raise ValueError("resume offset_field is required")
|
|
29
|
+
if self.replay_window < 0:
|
|
30
|
+
raise ValueError("resume replay_window must be non-negative")
|
|
31
|
+
if self.ttl_seconds is not None and self.ttl_seconds <= 0:
|
|
32
|
+
raise ValueError("resume ttl_seconds must be positive")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def enabled(self) -> bool:
|
|
36
|
+
return self.mode != "disabled"
|
|
37
|
+
|
|
38
|
+
def as_dict(self) -> dict[str, object]:
|
|
39
|
+
return {
|
|
40
|
+
"mode": self.mode,
|
|
41
|
+
"token_field": self.token_field,
|
|
42
|
+
"offset_field": self.offset_field,
|
|
43
|
+
"replay_window": self.replay_window,
|
|
44
|
+
"ttl_seconds": self.ttl_seconds,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compile_resume_policy(binding_kind: str, binding: Mapping[str, Any]) -> ResumePolicy:
|
|
49
|
+
raw = binding.get("resume") or binding.get("resume_policy")
|
|
50
|
+
if raw in (None, False):
|
|
51
|
+
return ResumePolicy()
|
|
52
|
+
if raw is True:
|
|
53
|
+
raw = {"mode": "stateful"}
|
|
54
|
+
if not isinstance(raw, Mapping):
|
|
55
|
+
raise ValueError("resume policy must be a mapping or boolean")
|
|
56
|
+
if binding_kind not in RESUME_CAPABLE_BINDINGS:
|
|
57
|
+
raise ValueError(f"{binding_kind} does not support stream resume policy")
|
|
58
|
+
|
|
59
|
+
mode = str(raw.get("mode", "stateful"))
|
|
60
|
+
policy = ResumePolicy(
|
|
61
|
+
mode=mode,
|
|
62
|
+
token_field=str(raw.get("token_field", "resume_token")),
|
|
63
|
+
offset_field=str(raw.get("offset_field", "requested_offset")),
|
|
64
|
+
replay_window=int(raw.get("replay_window", 0)),
|
|
65
|
+
ttl_seconds=(
|
|
66
|
+
int(raw["ttl_seconds"])
|
|
67
|
+
if raw.get("ttl_seconds") is not None
|
|
68
|
+
else None
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
if policy.enabled and binding_kind in {"http.stream", "https.stream"} and mode == "cursor":
|
|
72
|
+
raise ValueError("HTTP stream resume cursor mode is reserved for SSE")
|
|
73
|
+
if policy.enabled and mode in {"cursor", "replay"} and policy.replay_window < 1:
|
|
74
|
+
raise ValueError("resume replay_window must be positive for cursor or replay mode")
|
|
75
|
+
return policy
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = ["RESUME_CAPABLE_BINDINGS", "RESUME_MODES", "ResumePolicy", "compile_resume_policy"]
|