tigrcorn-protocols 0.3.16.dev12__tar.gz → 0.3.17__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.
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/PKG-INFO +6 -6
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/pyproject.toml +6 -6
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/__init__.py +8 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/client_session_coverage/__init__.py +9 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/client_session_coverage/matrix.py +36 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/client_session_coverage/models.py +104 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/client_session_coverage/robustness.py +136 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/client_session_coverage/topology.py +130 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/client_session_coverage/validation.py +76 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http1/parser/__init__.py +14 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http1/parser/body.py +79 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http1/parser/core.py +43 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http1/parser/head.py +189 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http1/parser/models.py +39 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http1/parser/target.py +33 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http1/parser/validation.py +21 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/__init__.py +5 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/connect.py +176 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/core.py +88 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/flow_control.py +153 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/frames.py +149 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/headers.py +273 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/imports.py +69 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/io.py +77 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/requests.py +203 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/responses.py +233 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/streams.py +157 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http2/handler/websocket_support.py +46 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http3/__init__.py +12 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http3/codec.py +69 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/connect.py +269 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/core.py +59 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/custom.py +53 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/imports.py +58 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/lifecycle.py +126 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/outbound.py +171 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/packet.py +400 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/requests.py +292 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/responses.py +86 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/session.py +38 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/websocket_runtime.py +206 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http3/handler/webtransport.py +7 -1
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/webtransport_datagrams.py +111 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/webtransport_streams.py +296 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/handler/webtransport_support.py +101 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/qpack/__init__.py +71 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/qpack/decoder.py +249 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/qpack/encoder.py +253 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/qpack/model.py +197 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/qpack/static_table.py +118 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/qpack/wire.py +60 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/streams/__init__.py +29 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/streams/connection.py +284 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/streams/constants.py +17 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/http3/streams/request.py +354 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/resume.py +202 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/websocket/handler/__init__.py +7 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/websocket/handler/app_send.py +183 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/websocket/handler/core.py +314 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/websocket/handler/errors.py +8 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/webtransport/__init__.py +61 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/webtransport/governance.py +257 -0
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols/webtransport/wire.py +851 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols.egg-info/PKG-INFO +6 -6
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols.egg-info/SOURCES.txt +57 -7
- tigrcorn_protocols-0.3.17/src/tigrcorn_protocols.egg-info/requires.txt +5 -0
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols/__init__.py +0 -1
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols/http1/parser.py +0 -481
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols/http2/handler.py +0 -1499
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols/http3/handler/core.py +0 -1958
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols/http3/handler.py +0 -3
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols/http3/qpack.py +0 -843
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols/http3/streams.py +0 -657
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols/websocket/handler.py +0 -462
- tigrcorn_protocols-0.3.16.dev12/src/tigrcorn_protocols.egg-info/requires.txt +0 -5
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/LICENSE +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/README.md +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/setup.cfg +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/_compression.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/connect.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/content_coding.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/custom/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/custom/adapters.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/custom/registry.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/flow/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/flow/backpressure.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/flow/buffers.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/flow/credits.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/flow/keepalive.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/flow/timeouts.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/flow/watermarks.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http1/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http1/keepalive.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http1/serializer.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http1/state.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http2/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http2/codec.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http2/flow.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http2/hpack.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http2/state.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http2/streams.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http2/websocket.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http3/handler/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http3/state.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/http3/websocket.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/lifespan/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/lifespan/driver.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/py.typed +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/rawframed/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/rawframed/codec.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/rawframed/frames.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/rawframed/handler.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/rawframed/state.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/registry.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/cancellation.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/dispatch.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/fairness.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/policy.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/priorities.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/quotas.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/runtime.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/scheduler/tasks.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/sessions/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/sessions/base.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/sessions/connection.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/sessions/limits.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/sessions/manager.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/sessions/metadata.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/sessions/quic.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/streams/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/streams/base.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/streams/ids.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/streams/multiplex.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/streams/registry.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/streams/singleplex.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/websocket/__init__.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/websocket/codec.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/websocket/extensions.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/websocket/frames.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/websocket/handshake.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols/websocket/state.py +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols.egg-info/dependency_links.txt +0 -0
- {tigrcorn_protocols-0.3.16.dev12 → tigrcorn_protocols-0.3.17}/src/tigrcorn_protocols.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrcorn-protocols
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.17
|
|
4
4
|
Summary: Protocol handlers for Tigrcorn HTTP/1.1, HTTP/2, HTTP/3, QUIC, WebSocket, WebTransport, lifespan, and ASGI3 traffic.
|
|
5
5
|
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
6
6
|
License: Apache License
|
|
@@ -187,11 +187,11 @@ Classifier: Typing :: Typed
|
|
|
187
187
|
Requires-Python: <3.15,>=3.10
|
|
188
188
|
Description-Content-Type: text/markdown
|
|
189
189
|
License-File: LICENSE
|
|
190
|
-
Requires-Dist: tigrcorn-core==0.3.
|
|
191
|
-
Requires-Dist: tigrcorn-config==0.3.
|
|
192
|
-
Requires-Dist: tigrcorn-asgi==0.3.
|
|
193
|
-
Requires-Dist: tigrcorn-http==0.3.
|
|
194
|
-
Requires-Dist: tigrcorn-transports==0.3.
|
|
190
|
+
Requires-Dist: tigrcorn-core==0.3.17
|
|
191
|
+
Requires-Dist: tigrcorn-config==0.3.17
|
|
192
|
+
Requires-Dist: tigrcorn-asgi==0.3.17
|
|
193
|
+
Requires-Dist: tigrcorn-http==0.3.17
|
|
194
|
+
Requires-Dist: tigrcorn-transports==0.3.17
|
|
195
195
|
Dynamic: license-file
|
|
196
196
|
|
|
197
197
|
<div align="center">
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tigrcorn-protocols"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.17"
|
|
8
8
|
description = "Protocol handlers for Tigrcorn HTTP/1.1, HTTP/2, HTTP/3, QUIC, WebSocket, WebTransport, lifespan, and ASGI3 traffic."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<3.15"
|
|
@@ -30,11 +30,11 @@ classifiers = [
|
|
|
30
30
|
"Typing :: Typed",
|
|
31
31
|
]
|
|
32
32
|
dependencies = [
|
|
33
|
-
"tigrcorn-core==0.3.
|
|
34
|
-
"tigrcorn-config==0.3.
|
|
35
|
-
"tigrcorn-asgi==0.3.
|
|
36
|
-
"tigrcorn-http==0.3.
|
|
37
|
-
"tigrcorn-transports==0.3.
|
|
33
|
+
"tigrcorn-core==0.3.17",
|
|
34
|
+
"tigrcorn-config==0.3.17",
|
|
35
|
+
"tigrcorn-asgi==0.3.17",
|
|
36
|
+
"tigrcorn-http==0.3.17",
|
|
37
|
+
"tigrcorn-transports==0.3.17",
|
|
38
38
|
]
|
|
39
39
|
|
|
40
40
|
[tool.setuptools]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .models import ClientTopology, CoverageDisposition, ProtocolCarrier, SessionScope
|
|
6
|
+
from .validation import _enum_value, _normalized, classify_default_session_scope, validate_matrix_row
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_matrix_row(
|
|
10
|
+
*,
|
|
11
|
+
protocol_carrier: ProtocolCarrier | str,
|
|
12
|
+
client_topology: ClientTopology | str,
|
|
13
|
+
session_scope: SessionScope | str | None = None,
|
|
14
|
+
disposition: CoverageDisposition | str = CoverageDisposition.REQUIRED,
|
|
15
|
+
lifecycle_behavior: CoverageDisposition | str = CoverageDisposition.REQUIRED,
|
|
16
|
+
identity_isolation: CoverageDisposition | str = CoverageDisposition.REQUIRED,
|
|
17
|
+
ordering_behavior: CoverageDisposition | str = CoverageDisposition.REQUIRED,
|
|
18
|
+
pressure_mode: CoverageDisposition | str = CoverageDisposition.REQUIRED,
|
|
19
|
+
fault_mode: CoverageDisposition | str = CoverageDisposition.REQUIRED,
|
|
20
|
+
**identifiers: Any,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
carrier = _normalized(_enum_value(protocol_carrier))
|
|
23
|
+
row: dict[str, Any] = {
|
|
24
|
+
"protocol_carrier": carrier,
|
|
25
|
+
"client_topology": _enum_value(client_topology),
|
|
26
|
+
"session_scope": _enum_value(session_scope or classify_default_session_scope(carrier)),
|
|
27
|
+
"lifecycle_behavior": _enum_value(lifecycle_behavior),
|
|
28
|
+
"identity_isolation": _enum_value(identity_isolation),
|
|
29
|
+
"ordering_behavior": _enum_value(ordering_behavior),
|
|
30
|
+
"pressure_mode": _enum_value(pressure_mode),
|
|
31
|
+
"fault_mode": _enum_value(fault_mode),
|
|
32
|
+
"disposition": _enum_value(disposition),
|
|
33
|
+
}
|
|
34
|
+
row.update(identifiers)
|
|
35
|
+
validate_matrix_row(row)
|
|
36
|
+
return row
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProtocolCarrier(str, Enum):
|
|
8
|
+
HTTP1 = "http1"
|
|
9
|
+
HTTP2 = "http2"
|
|
10
|
+
HTTP3_QUIC = "http3_quic"
|
|
11
|
+
WEBSOCKET_H1 = "websocket_h1"
|
|
12
|
+
WEBSOCKET_H2 = "websocket_h2"
|
|
13
|
+
WEBSOCKET_H3 = "websocket_h3"
|
|
14
|
+
WEBTRANSPORT_H3_QUIC = "webtransport_h3_quic"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClientTopology(str, Enum):
|
|
18
|
+
SINGLE_CLIENT = "single_client"
|
|
19
|
+
SEQUENTIAL_CLIENTS = "sequential_clients"
|
|
20
|
+
BOUNDED_INTERLEAVED_CLIENTS = "bounded_interleaved_clients"
|
|
21
|
+
CONCURRENT_CLIENTS = "concurrent_clients"
|
|
22
|
+
CHURN_CLIENTS = "churn_clients"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SessionScope(str, Enum):
|
|
26
|
+
REQUEST_SCOPED = "request_scoped"
|
|
27
|
+
TCP_CONNECTION_SCOPED = "tcp_connection_scoped"
|
|
28
|
+
H2_CONNECTION_SCOPED = "h2_connection_scoped"
|
|
29
|
+
H2_STREAM_SCOPED = "h2_stream_scoped"
|
|
30
|
+
QUIC_CONNECTION_SCOPED = "quic_connection_scoped"
|
|
31
|
+
H3_STREAM_SCOPED = "h3_stream_scoped"
|
|
32
|
+
WEBSOCKET_CONNECTION_SCOPED = "websocket_connection_scoped"
|
|
33
|
+
WEBTRANSPORT_SESSION_SCOPED = "webtransport_session_scoped"
|
|
34
|
+
WEBTRANSPORT_STREAM_SCOPED = "webtransport_stream_scoped"
|
|
35
|
+
WEBTRANSPORT_DATAGRAM_SCOPED = "webtransport_datagram_scoped"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CoverageDisposition(str, Enum):
|
|
39
|
+
COVERED = "covered"
|
|
40
|
+
REQUIRED = "required"
|
|
41
|
+
PLANNED = "planned"
|
|
42
|
+
NOT_APPLICABLE = "not_applicable"
|
|
43
|
+
FAIL_CLOSED = "fail_closed"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BehaviorAxis(str, Enum):
|
|
47
|
+
LIFECYCLE = "lifecycle_behavior"
|
|
48
|
+
IDENTITY_ISOLATION = "identity_isolation"
|
|
49
|
+
ORDERING = "ordering_behavior"
|
|
50
|
+
PRESSURE = "pressure_mode"
|
|
51
|
+
FAULT = "fault_mode"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
PROTOCOL_CARRIER_VALUES = frozenset(item.value for item in ProtocolCarrier)
|
|
55
|
+
CLIENT_TOPOLOGY_VALUES = frozenset(item.value for item in ClientTopology)
|
|
56
|
+
SESSION_SCOPE_VALUES = frozenset(item.value for item in SessionScope)
|
|
57
|
+
DISPOSITION_VALUES = frozenset(item.value for item in CoverageDisposition)
|
|
58
|
+
BEHAVIOR_AXIS_VALUES = frozenset(item.value for item in BehaviorAxis)
|
|
59
|
+
|
|
60
|
+
GOVERNED_IDENTIFIER_FIELDS = frozenset(
|
|
61
|
+
{
|
|
62
|
+
"client_id",
|
|
63
|
+
"connection_id",
|
|
64
|
+
"session_id",
|
|
65
|
+
"stream_id",
|
|
66
|
+
"datagram_id",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
INTERNAL_ONLY_FIELDS = frozenset({"lane"})
|
|
70
|
+
|
|
71
|
+
REQUIRED_MATRIX_AXES = frozenset(
|
|
72
|
+
{
|
|
73
|
+
"protocol_carrier",
|
|
74
|
+
"client_topology",
|
|
75
|
+
"session_scope",
|
|
76
|
+
"lifecycle_behavior",
|
|
77
|
+
"identity_isolation",
|
|
78
|
+
"ordering_behavior",
|
|
79
|
+
"pressure_mode",
|
|
80
|
+
"fault_mode",
|
|
81
|
+
"disposition",
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
PROTOCOL_CARRIER_DEFAULT_SCOPES: dict[str, SessionScope] = {
|
|
86
|
+
ProtocolCarrier.HTTP1.value: SessionScope.REQUEST_SCOPED,
|
|
87
|
+
ProtocolCarrier.HTTP2.value: SessionScope.H2_STREAM_SCOPED,
|
|
88
|
+
ProtocolCarrier.HTTP3_QUIC.value: SessionScope.H3_STREAM_SCOPED,
|
|
89
|
+
ProtocolCarrier.WEBSOCKET_H1.value: SessionScope.WEBSOCKET_CONNECTION_SCOPED,
|
|
90
|
+
ProtocolCarrier.WEBSOCKET_H2.value: SessionScope.WEBSOCKET_CONNECTION_SCOPED,
|
|
91
|
+
ProtocolCarrier.WEBSOCKET_H3.value: SessionScope.WEBSOCKET_CONNECTION_SCOPED,
|
|
92
|
+
ProtocolCarrier.WEBTRANSPORT_H3_QUIC.value: SessionScope.WEBTRANSPORT_SESSION_SCOPED,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ClientSession:
|
|
98
|
+
client_id: str
|
|
99
|
+
connection_id: str
|
|
100
|
+
session_id: str
|
|
101
|
+
closed: bool = False
|
|
102
|
+
payloads: list[str] = field(default_factory=list)
|
|
103
|
+
streams_seen: set[str] = field(default_factory=set)
|
|
104
|
+
datagrams_seen: set[str] = field(default_factory=set)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .matrix import build_matrix_row
|
|
6
|
+
from .models import ClientSession, ClientTopology, CoverageDisposition, ProtocolCarrier, SessionScope
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ClientSessionRobustnessHarness:
|
|
10
|
+
"""Carrier-aware runtime harness for T2 pressure, fault, and cleanup proof."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
carrier: ProtocolCarrier,
|
|
15
|
+
scope: SessionScope | None = None,
|
|
16
|
+
*,
|
|
17
|
+
max_sessions: int = 32,
|
|
18
|
+
max_streams: int = 32,
|
|
19
|
+
max_queue: int = 32,
|
|
20
|
+
max_message_size: int = 65536,
|
|
21
|
+
max_datagram_size: int = 1200,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.carrier = carrier
|
|
24
|
+
self.scope = scope
|
|
25
|
+
self.max_sessions = max_sessions
|
|
26
|
+
self.max_streams = max_streams
|
|
27
|
+
self.max_queue = max_queue
|
|
28
|
+
self.max_message_size = max_message_size
|
|
29
|
+
self.max_datagram_size = max_datagram_size
|
|
30
|
+
self.sessions: dict[str, ClientSession] = {}
|
|
31
|
+
self.errors: list[dict[str, Any]] = []
|
|
32
|
+
|
|
33
|
+
def open(self, client_id: str, connection_id: str, session_id: str) -> None:
|
|
34
|
+
if len([session for session in self.sessions.values() if not session.closed]) >= self.max_sessions:
|
|
35
|
+
self.fail_closed(client_id, connection_id, session_id, ClientTopology.CONCURRENT_CLIENTS, "max_sessions_exceeded")
|
|
36
|
+
raise BufferError("session pressure budget exceeded")
|
|
37
|
+
if session_id in self.sessions and not self.sessions[session_id].closed:
|
|
38
|
+
self.fail_closed(client_id, connection_id, session_id, ClientTopology.CONCURRENT_CLIENTS, "duplicate_session")
|
|
39
|
+
raise ValueError("duplicate session rejected")
|
|
40
|
+
self.sessions[session_id] = ClientSession(client_id=client_id, connection_id=connection_id, session_id=session_id)
|
|
41
|
+
|
|
42
|
+
def send(
|
|
43
|
+
self,
|
|
44
|
+
client_id: str,
|
|
45
|
+
connection_id: str,
|
|
46
|
+
session_id: str,
|
|
47
|
+
topology: ClientTopology,
|
|
48
|
+
payload: object,
|
|
49
|
+
*,
|
|
50
|
+
stream_id: str | int | None = None,
|
|
51
|
+
datagram_id: str | int | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
session = self.session_for(client_id, connection_id, session_id, topology)
|
|
54
|
+
identifiers = {"stream_id": stream_id, "datagram_id": datagram_id}
|
|
55
|
+
if session.closed:
|
|
56
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "post_close_send", **identifiers)
|
|
57
|
+
raise RuntimeError("post-close send rejected")
|
|
58
|
+
if not isinstance(payload, (str, bytes)) or not payload:
|
|
59
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "malformed_payload", **identifiers)
|
|
60
|
+
raise ValueError("malformed payload rejected")
|
|
61
|
+
payload_size = len(payload)
|
|
62
|
+
if payload_size > self.max_message_size:
|
|
63
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "message_pressure_budget_exceeded", **identifiers)
|
|
64
|
+
raise BufferError("message pressure budget exceeded")
|
|
65
|
+
if datagram_id is not None and payload_size > self.max_datagram_size:
|
|
66
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "datagram_pressure_budget_exceeded", **identifiers)
|
|
67
|
+
raise BufferError("datagram pressure budget exceeded")
|
|
68
|
+
if len(session.payloads) >= self.max_queue:
|
|
69
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "queue_pressure_budget_exceeded", **identifiers)
|
|
70
|
+
raise BufferError("queue pressure budget exceeded")
|
|
71
|
+
if stream_id is not None:
|
|
72
|
+
session.streams_seen.add(str(stream_id))
|
|
73
|
+
if len(session.streams_seen) > self.max_streams:
|
|
74
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "max_streams_exceeded", **identifiers)
|
|
75
|
+
session.streams_seen.remove(str(stream_id))
|
|
76
|
+
raise BufferError("stream pressure budget exceeded")
|
|
77
|
+
if datagram_id is not None:
|
|
78
|
+
session.datagrams_seen.add(str(datagram_id))
|
|
79
|
+
session.payloads.append(payload if isinstance(payload, str) else payload.decode("latin1"))
|
|
80
|
+
|
|
81
|
+
def close(self, client_id: str, connection_id: str, session_id: str, topology: ClientTopology) -> None:
|
|
82
|
+
self.session_for(client_id, connection_id, session_id, topology).closed = True
|
|
83
|
+
|
|
84
|
+
def cancel(self, client_id: str, connection_id: str, session_id: str, topology: ClientTopology) -> None:
|
|
85
|
+
self.session_for(client_id, connection_id, session_id, topology).closed = True
|
|
86
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "cancelled")
|
|
87
|
+
|
|
88
|
+
def timeout(self, client_id: str, connection_id: str, session_id: str, topology: ClientTopology) -> None:
|
|
89
|
+
self.session_for(client_id, connection_id, session_id, topology).closed = True
|
|
90
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "timeout")
|
|
91
|
+
|
|
92
|
+
def session_for(
|
|
93
|
+
self,
|
|
94
|
+
client_id: str,
|
|
95
|
+
connection_id: str,
|
|
96
|
+
session_id: str,
|
|
97
|
+
topology: ClientTopology,
|
|
98
|
+
) -> ClientSession:
|
|
99
|
+
try:
|
|
100
|
+
session = self.sessions[session_id]
|
|
101
|
+
except KeyError as exc:
|
|
102
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "unknown_session")
|
|
103
|
+
raise KeyError("unknown session rejected") from exc
|
|
104
|
+
if session.client_id != client_id or session.connection_id != connection_id:
|
|
105
|
+
self.fail_closed(client_id, connection_id, session_id, topology, "cross_client_session_access")
|
|
106
|
+
raise PermissionError("cross-client or cross-connection session access rejected")
|
|
107
|
+
return session
|
|
108
|
+
|
|
109
|
+
def fail_closed(
|
|
110
|
+
self,
|
|
111
|
+
client_id: str,
|
|
112
|
+
connection_id: str,
|
|
113
|
+
session_id: str,
|
|
114
|
+
topology: ClientTopology,
|
|
115
|
+
error_kind: str,
|
|
116
|
+
**identifiers: Any,
|
|
117
|
+
) -> dict[str, Any]:
|
|
118
|
+
identifiers = {key: value for key, value in identifiers.items() if value is not None}
|
|
119
|
+
row = build_matrix_row(
|
|
120
|
+
protocol_carrier=self.carrier,
|
|
121
|
+
client_topology=topology,
|
|
122
|
+
session_scope=self.scope,
|
|
123
|
+
disposition=CoverageDisposition.FAIL_CLOSED,
|
|
124
|
+
lifecycle_behavior=CoverageDisposition.COVERED,
|
|
125
|
+
identity_isolation=CoverageDisposition.COVERED,
|
|
126
|
+
ordering_behavior=CoverageDisposition.COVERED,
|
|
127
|
+
pressure_mode=CoverageDisposition.COVERED,
|
|
128
|
+
fault_mode=CoverageDisposition.COVERED,
|
|
129
|
+
client_id=client_id,
|
|
130
|
+
connection_id=connection_id,
|
|
131
|
+
session_id=session_id,
|
|
132
|
+
error_kind=error_kind,
|
|
133
|
+
**identifiers,
|
|
134
|
+
)
|
|
135
|
+
self.errors.append(row)
|
|
136
|
+
return row
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .matrix import build_matrix_row
|
|
7
|
+
from .models import ClientSession, ClientTopology, CoverageDisposition, ProtocolCarrier, SessionScope
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClientSessionTopologyHarness:
|
|
11
|
+
"""Runtime-facing carrier topology recorder for governed proof rows."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, carrier: ProtocolCarrier, scope: SessionScope | None = None) -> None:
|
|
14
|
+
self.carrier = carrier
|
|
15
|
+
self.scope = scope
|
|
16
|
+
self.sessions: dict[str, ClientSession] = {}
|
|
17
|
+
self.events: list[dict[str, Any]] = []
|
|
18
|
+
|
|
19
|
+
def open(self, client_id: str, connection_id: str, session_id: str, topology: ClientTopology) -> None:
|
|
20
|
+
self.sessions[session_id] = ClientSession(
|
|
21
|
+
client_id=client_id,
|
|
22
|
+
connection_id=connection_id,
|
|
23
|
+
session_id=session_id,
|
|
24
|
+
)
|
|
25
|
+
self.events.append(self.record("open", client_id, connection_id, session_id, topology))
|
|
26
|
+
|
|
27
|
+
def send(
|
|
28
|
+
self,
|
|
29
|
+
client_id: str,
|
|
30
|
+
connection_id: str,
|
|
31
|
+
session_id: str,
|
|
32
|
+
topology: ClientTopology,
|
|
33
|
+
payload: str,
|
|
34
|
+
**identifiers: Any,
|
|
35
|
+
) -> None:
|
|
36
|
+
session = self.session_for(client_id, connection_id, session_id)
|
|
37
|
+
session.payloads.append(payload)
|
|
38
|
+
self.events.append(
|
|
39
|
+
self.record(
|
|
40
|
+
"send",
|
|
41
|
+
client_id,
|
|
42
|
+
connection_id,
|
|
43
|
+
session_id,
|
|
44
|
+
topology,
|
|
45
|
+
payload=payload,
|
|
46
|
+
**identifiers,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def send_async(
|
|
51
|
+
self,
|
|
52
|
+
client_id: str,
|
|
53
|
+
connection_id: str,
|
|
54
|
+
session_id: str,
|
|
55
|
+
topology: ClientTopology,
|
|
56
|
+
payload: str,
|
|
57
|
+
delay: float = 0.0,
|
|
58
|
+
**identifiers: Any,
|
|
59
|
+
) -> None:
|
|
60
|
+
await asyncio.sleep(delay)
|
|
61
|
+
self.send(client_id, connection_id, session_id, topology, payload, **identifiers)
|
|
62
|
+
|
|
63
|
+
def close(self, client_id: str, connection_id: str, session_id: str, topology: ClientTopology) -> None:
|
|
64
|
+
session = self.session_for(client_id, connection_id, session_id)
|
|
65
|
+
session.closed = True
|
|
66
|
+
self.events.append(self.record("close", client_id, connection_id, session_id, topology))
|
|
67
|
+
|
|
68
|
+
def record(
|
|
69
|
+
self,
|
|
70
|
+
subevent: str,
|
|
71
|
+
client_id: str,
|
|
72
|
+
connection_id: str,
|
|
73
|
+
session_id: str,
|
|
74
|
+
topology: ClientTopology,
|
|
75
|
+
**extra: Any,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
return build_matrix_row(
|
|
78
|
+
protocol_carrier=self.carrier,
|
|
79
|
+
client_topology=topology,
|
|
80
|
+
session_scope=self.scope,
|
|
81
|
+
disposition=CoverageDisposition.COVERED,
|
|
82
|
+
lifecycle_behavior=CoverageDisposition.COVERED,
|
|
83
|
+
identity_isolation=CoverageDisposition.COVERED,
|
|
84
|
+
ordering_behavior=CoverageDisposition.COVERED,
|
|
85
|
+
pressure_mode=CoverageDisposition.REQUIRED,
|
|
86
|
+
fault_mode=CoverageDisposition.REQUIRED,
|
|
87
|
+
client_id=client_id,
|
|
88
|
+
connection_id=connection_id,
|
|
89
|
+
session_id=session_id,
|
|
90
|
+
subevent=subevent,
|
|
91
|
+
**extra,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def session_for(self, client_id: str, connection_id: str, session_id: str) -> ClientSession:
|
|
95
|
+
session = self.sessions[session_id]
|
|
96
|
+
if session.client_id != client_id or session.connection_id != connection_id:
|
|
97
|
+
raise PermissionError("cross-client or cross-connection session access rejected")
|
|
98
|
+
if session.closed:
|
|
99
|
+
raise RuntimeError("post-close send rejected")
|
|
100
|
+
return session
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def sequential_pair(carrier: ProtocolCarrier, scope: SessionScope | None = None) -> ClientSessionTopologyHarness:
|
|
104
|
+
topology = ClientTopology.SEQUENTIAL_CLIENTS
|
|
105
|
+
harness = ClientSessionTopologyHarness(carrier, scope)
|
|
106
|
+
harness.open("client-a", "conn-a", "session-a", topology)
|
|
107
|
+
harness.send("client-a", "conn-a", "session-a", topology, "a-1")
|
|
108
|
+
harness.close("client-a", "conn-a", "session-a", topology)
|
|
109
|
+
harness.open("client-b", "conn-b", "session-b", topology)
|
|
110
|
+
harness.send("client-b", "conn-b", "session-b", topology, "b-1")
|
|
111
|
+
harness.close("client-b", "conn-b", "session-b", topology)
|
|
112
|
+
return harness
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def bounded_interleaved_pair(
|
|
116
|
+
carrier: ProtocolCarrier,
|
|
117
|
+
scope: SessionScope | None = None,
|
|
118
|
+
) -> ClientSessionTopologyHarness:
|
|
119
|
+
topology = ClientTopology.BOUNDED_INTERLEAVED_CLIENTS
|
|
120
|
+
harness = ClientSessionTopologyHarness(carrier, scope)
|
|
121
|
+
harness.open("client-a", "conn-a", "session-a", topology)
|
|
122
|
+
harness.open("client-b", "conn-b", "session-b", topology)
|
|
123
|
+
for client_id, connection_id, session_id, payload in (
|
|
124
|
+
("client-a", "conn-a", "session-a", "a-1"),
|
|
125
|
+
("client-b", "conn-b", "session-b", "b-1"),
|
|
126
|
+
("client-a", "conn-a", "session-a", "a-2"),
|
|
127
|
+
("client-b", "conn-b", "session-b", "b-2"),
|
|
128
|
+
):
|
|
129
|
+
harness.send(client_id, connection_id, session_id, topology, payload)
|
|
130
|
+
return harness
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from .models import (
|
|
7
|
+
BEHAVIOR_AXIS_VALUES,
|
|
8
|
+
CLIENT_TOPOLOGY_VALUES,
|
|
9
|
+
DISPOSITION_VALUES,
|
|
10
|
+
GOVERNED_IDENTIFIER_FIELDS,
|
|
11
|
+
INTERNAL_ONLY_FIELDS,
|
|
12
|
+
PROTOCOL_CARRIER_DEFAULT_SCOPES,
|
|
13
|
+
PROTOCOL_CARRIER_VALUES,
|
|
14
|
+
REQUIRED_MATRIX_AXES,
|
|
15
|
+
SESSION_SCOPE_VALUES,
|
|
16
|
+
BehaviorAxis,
|
|
17
|
+
SessionScope,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _enum_value(value: object) -> str:
|
|
22
|
+
if isinstance(value, Enum):
|
|
23
|
+
return str(value.value)
|
|
24
|
+
return str(value)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalized(value: object) -> str:
|
|
28
|
+
return str(value).strip().lower().replace("-", "_").replace(".", "_")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def classify_default_session_scope(protocol_carrier: object) -> SessionScope:
|
|
32
|
+
carrier = _normalized(_enum_value(protocol_carrier))
|
|
33
|
+
try:
|
|
34
|
+
return PROTOCOL_CARRIER_DEFAULT_SCOPES[carrier]
|
|
35
|
+
except KeyError as exc:
|
|
36
|
+
raise ValueError(f"unknown protocol carrier: {protocol_carrier!r}") from exc
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_no_internal_lane(record: Mapping[str, Any]) -> None:
|
|
40
|
+
if INTERNAL_ONLY_FIELDS.intersection(record):
|
|
41
|
+
raise ValueError("lane is protocol-internal and not a governed proof field")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_governed_identifiers(record: Mapping[str, Any]) -> None:
|
|
45
|
+
validate_no_internal_lane(record)
|
|
46
|
+
for field in GOVERNED_IDENTIFIER_FIELDS:
|
|
47
|
+
if field in record and record[field] is None:
|
|
48
|
+
raise ValueError(f"{field} must be omitted or populated")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_matrix_row(row: Mapping[str, Any]) -> None:
|
|
52
|
+
validate_governed_identifiers(row)
|
|
53
|
+
missing = sorted(REQUIRED_MATRIX_AXES.difference(row))
|
|
54
|
+
if missing:
|
|
55
|
+
raise ValueError(f"client-session matrix row missing axes: {missing}")
|
|
56
|
+
|
|
57
|
+
carrier = _normalized(row["protocol_carrier"])
|
|
58
|
+
if carrier not in PROTOCOL_CARRIER_VALUES:
|
|
59
|
+
raise ValueError(f"unknown protocol carrier: {row['protocol_carrier']!r}")
|
|
60
|
+
|
|
61
|
+
topology = _enum_value(row["client_topology"])
|
|
62
|
+
if topology not in CLIENT_TOPOLOGY_VALUES:
|
|
63
|
+
raise ValueError(f"unknown client topology: {topology!r}")
|
|
64
|
+
|
|
65
|
+
scope = _enum_value(row["session_scope"])
|
|
66
|
+
if scope not in SESSION_SCOPE_VALUES:
|
|
67
|
+
raise ValueError(f"unknown session scope: {scope!r}")
|
|
68
|
+
|
|
69
|
+
disposition = _enum_value(row["disposition"])
|
|
70
|
+
if disposition not in DISPOSITION_VALUES:
|
|
71
|
+
raise ValueError(f"unknown disposition: {disposition!r}")
|
|
72
|
+
|
|
73
|
+
for axis in BehaviorAxis:
|
|
74
|
+
axis_value = _enum_value(row[axis.value])
|
|
75
|
+
if axis_value not in DISPOSITION_VALUES:
|
|
76
|
+
raise ValueError(f"{axis.value} must be a coverage disposition")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .models import *
|
|
4
|
+
from .validation import *
|
|
5
|
+
from .validation import _is_token, _validate_header_name, _validate_header_value
|
|
6
|
+
from .target import *
|
|
7
|
+
from .target import _parse_request_target
|
|
8
|
+
from .body import *
|
|
9
|
+
from .body import _read_chunked_body, _read_line, _read_request_head_until_terminator, _readexactly
|
|
10
|
+
from .head import *
|
|
11
|
+
from .head import _parse_request_head_bytes, _parse_transfer_encoding
|
|
12
|
+
from .core import *
|
|
13
|
+
|
|
14
|
+
__all__ = [name for name in globals() if not name.startswith("__")]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from tigrcorn_core.errors import ProtocolError
|
|
6
|
+
from tigrcorn_core.types import StreamReaderLike
|
|
7
|
+
|
|
8
|
+
from .validation import _validate_header_name, _validate_header_value
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def _read_line(reader: StreamReaderLike) -> bytes:
|
|
12
|
+
try:
|
|
13
|
+
return await reader.readuntil(b"\r\n")
|
|
14
|
+
except asyncio.IncompleteReadError as exc:
|
|
15
|
+
raise ProtocolError('unexpected EOF while reading HTTP/1.1 body') from exc
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def _readexactly(reader: StreamReaderLike, amount: int) -> bytes:
|
|
19
|
+
try:
|
|
20
|
+
return await reader.readexactly(amount)
|
|
21
|
+
except asyncio.IncompleteReadError as exc:
|
|
22
|
+
raise ProtocolError('unexpected EOF while reading HTTP/1.1 body') from exc
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def _read_request_head_until_terminator(
|
|
26
|
+
reader: StreamReaderLike,
|
|
27
|
+
*,
|
|
28
|
+
limit: int,
|
|
29
|
+
buffer_size: int,
|
|
30
|
+
) -> bytes:
|
|
31
|
+
limited_readuntil = getattr(reader, 'readuntil_limited', None)
|
|
32
|
+
if callable(limited_readuntil):
|
|
33
|
+
try:
|
|
34
|
+
return await limited_readuntil(b"\r\n\r\n", limit=limit, read_chunk_size=buffer_size)
|
|
35
|
+
except TypeError:
|
|
36
|
+
return await limited_readuntil(b"\r\n\r\n", limit=limit)
|
|
37
|
+
head = await reader.readuntil(b"\r\n\r\n")
|
|
38
|
+
if len(head) > limit:
|
|
39
|
+
raise asyncio.LimitOverrunError('request head exceeds configured HTTP/1.1 request-head limit', consumed=len(head))
|
|
40
|
+
return head
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _consume_chunked_trailers(reader: StreamReaderLike) -> None:
|
|
44
|
+
while True:
|
|
45
|
+
trailer = await _read_line(reader)
|
|
46
|
+
if trailer == b"\r\n":
|
|
47
|
+
return
|
|
48
|
+
if trailer[:1] in {b' ', b'\t'}:
|
|
49
|
+
raise ProtocolError('obsolete line folding is not supported')
|
|
50
|
+
if b':' not in trailer[:-2]:
|
|
51
|
+
raise ProtocolError('malformed chunk trailer line')
|
|
52
|
+
name, value = trailer[:-2].split(b':', 1)
|
|
53
|
+
_validate_header_name(name.strip().lower())
|
|
54
|
+
_validate_header_value(value.strip())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _read_chunked_body(reader: StreamReaderLike, *, max_body_size: int) -> bytes:
|
|
58
|
+
parts: list[bytes] = []
|
|
59
|
+
total = 0
|
|
60
|
+
while True:
|
|
61
|
+
line = await _read_line(reader)
|
|
62
|
+
size_token = line[:-2].split(b';', 1)[0].strip()
|
|
63
|
+
try:
|
|
64
|
+
size = int(size_token, 16)
|
|
65
|
+
except ValueError as exc:
|
|
66
|
+
raise ProtocolError('invalid chunk size') from exc
|
|
67
|
+
if size < 0:
|
|
68
|
+
raise ProtocolError('invalid chunk size')
|
|
69
|
+
if size == 0:
|
|
70
|
+
await _consume_chunked_trailers(reader)
|
|
71
|
+
return b''.join(parts)
|
|
72
|
+
chunk = await _readexactly(reader, size)
|
|
73
|
+
terminator = await _readexactly(reader, 2)
|
|
74
|
+
if terminator != b"\r\n":
|
|
75
|
+
raise ProtocolError('invalid chunk terminator')
|
|
76
|
+
total += size
|
|
77
|
+
if total > max_body_size:
|
|
78
|
+
raise ProtocolError('request body exceeds configured max_body_size')
|
|
79
|
+
parts.append(chunk)
|