tigrcorn-contract 0.3.16__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.
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ from .classification import (
4
+ BindingClassification,
5
+ FamilyCapability,
6
+ ProductSurfaceStatus,
7
+ classify_binding,
8
+ family_capability,
9
+ product_surface_excluded,
10
+ product_surface_status,
11
+ require_product_boundary_exclusion,
12
+ require_product_runtime_available,
13
+ runtime_interface_available,
14
+ validate_binding_legality,
15
+ )
16
+ from .compat import (
17
+ CompatibilityParityRow,
18
+ HTTPFeatureContractMap,
19
+ alt_svc_contract_map,
20
+ asgi3_compat_scope,
21
+ asgi_extension_bridge,
22
+ compatibility_feature_parity,
23
+ content_coding_contract_map,
24
+ early_hints_contract_map,
25
+ http_feature_contract_map,
26
+ observability_contract_metadata,
27
+ proxy_normalization_contract_map,
28
+ static_delivery_contract_map,
29
+ trailers_contract_map,
30
+ )
31
+ from .events import (
32
+ CompletionLevel,
33
+ CompletionStatus,
34
+ GenericDatagramRuntime,
35
+ GenericStreamRuntime,
36
+ datagram_receive,
37
+ datagram_send,
38
+ emit_complete,
39
+ http_disconnect,
40
+ http_request,
41
+ http_response_body,
42
+ http_response_pathsend,
43
+ http_response_start,
44
+ lifespan_shutdown,
45
+ lifespan_shutdown_complete,
46
+ lifespan_shutdown_failed,
47
+ lifespan_startup,
48
+ lifespan_startup_complete,
49
+ lifespan_startup_failed,
50
+ map_contract_event,
51
+ stream_receive,
52
+ stream_send,
53
+ validate_datagram_event,
54
+ validate_event_order,
55
+ validate_stream_event,
56
+ websocket_accept,
57
+ websocket_close,
58
+ websocket_connect,
59
+ websocket_disconnect,
60
+ websocket_receive,
61
+ websocket_send,
62
+ webtransport_accept,
63
+ webtransport_close,
64
+ webtransport_connect,
65
+ webtransport_datagram_receive,
66
+ webtransport_datagram_send,
67
+ webtransport_disconnect,
68
+ webtransport_stream_receive,
69
+ webtransport_stream_send,
70
+ )
71
+ from .metadata import (
72
+ ConnectionIdentity,
73
+ EndpointMetadata,
74
+ SecurityMetadata,
75
+ StreamIdentity,
76
+ UnitIdentity,
77
+ asgi3_extensions,
78
+ datagram_identity,
79
+ endpoint_metadata,
80
+ require_lossless_metadata,
81
+ security_metadata,
82
+ stream_identity,
83
+ transport_identity,
84
+ unit_identity,
85
+ validate_connection_identity,
86
+ validate_endpoint_metadata,
87
+ validate_security_metadata,
88
+ validate_stream_identity,
89
+ )
90
+ from .projection import (
91
+ EventProjection,
92
+ ScopeProjection,
93
+ project_event_classification,
94
+ project_receive_event,
95
+ project_scope_classification,
96
+ project_send_event,
97
+ validate_projected_event,
98
+ )
99
+ from .scopes import SUPPORTED_SCOPE_TYPES, contract_scope, validate_scope
100
+
101
+ __all__ = [
102
+ "BindingClassification",
103
+ "CompletionLevel",
104
+ "CompletionStatus",
105
+ "ConnectionIdentity",
106
+ "EndpointMetadata",
107
+ "FamilyCapability",
108
+ "GenericDatagramRuntime",
109
+ "GenericStreamRuntime",
110
+ "ProductSurfaceStatus",
111
+ "CompatibilityParityRow",
112
+ "HTTPFeatureContractMap",
113
+ "SUPPORTED_SCOPE_TYPES",
114
+ "SecurityMetadata",
115
+ "EventProjection",
116
+ "ScopeProjection",
117
+ "StreamIdentity",
118
+ "UnitIdentity",
119
+ "alt_svc_contract_map",
120
+ "asgi3_extensions",
121
+ "asgi3_compat_scope",
122
+ "asgi_extension_bridge",
123
+ "classify_binding",
124
+ "compatibility_feature_parity",
125
+ "contract_scope",
126
+ "content_coding_contract_map",
127
+ "datagram_identity",
128
+ "datagram_receive",
129
+ "datagram_send",
130
+ "emit_complete",
131
+ "endpoint_metadata",
132
+ "early_hints_contract_map",
133
+ "family_capability",
134
+ "http_disconnect",
135
+ "http_feature_contract_map",
136
+ "http_request",
137
+ "http_response_body",
138
+ "http_response_pathsend",
139
+ "http_response_start",
140
+ "lifespan_shutdown",
141
+ "lifespan_shutdown_complete",
142
+ "lifespan_shutdown_failed",
143
+ "lifespan_startup",
144
+ "lifespan_startup_complete",
145
+ "lifespan_startup_failed",
146
+ "map_contract_event",
147
+ "observability_contract_metadata",
148
+ "proxy_normalization_contract_map",
149
+ "product_surface_status",
150
+ "product_surface_excluded",
151
+ "project_event_classification",
152
+ "project_receive_event",
153
+ "project_scope_classification",
154
+ "project_send_event",
155
+ "require_lossless_metadata",
156
+ "require_product_boundary_exclusion",
157
+ "require_product_runtime_available",
158
+ "runtime_interface_available",
159
+ "security_metadata",
160
+ "static_delivery_contract_map",
161
+ "stream_identity",
162
+ "stream_receive",
163
+ "stream_send",
164
+ "transport_identity",
165
+ "trailers_contract_map",
166
+ "unit_identity",
167
+ "validate_binding_legality",
168
+ "validate_connection_identity",
169
+ "validate_datagram_event",
170
+ "validate_endpoint_metadata",
171
+ "validate_event_order",
172
+ "validate_projected_event",
173
+ "validate_security_metadata",
174
+ "validate_scope",
175
+ "validate_stream_event",
176
+ "validate_stream_identity",
177
+ "websocket_accept",
178
+ "websocket_close",
179
+ "websocket_connect",
180
+ "websocket_disconnect",
181
+ "websocket_receive",
182
+ "websocket_send",
183
+ "webtransport_accept",
184
+ "webtransport_close",
185
+ "webtransport_connect",
186
+ "webtransport_datagram_receive",
187
+ "webtransport_datagram_send",
188
+ "webtransport_disconnect",
189
+ "webtransport_stream_receive",
190
+ "webtransport_stream_send",
191
+ ]
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ from tigrcorn_core.errors import ConfigError
7
+
8
+ BindingKind = Literal["http", "http.stream", "websocket", "lifespan", "webtransport", "stream", "datagram", "rest", "jsonrpc", "sse"]
9
+ ProductSurfaceKind = Literal[
10
+ "auto",
11
+ "tigr-asgi-contract",
12
+ "asgi3",
13
+ "asgi2",
14
+ "wsgi",
15
+ "rsgi",
16
+ "rest",
17
+ "jsonrpc",
18
+ ]
19
+
20
+ _SERVER_OWNED_RUNTIMES = {"http", "http.stream", "websocket", "lifespan", "webtransport", "stream", "datagram"}
21
+ _CLASSIFICATION_ONLY = {"rest", "jsonrpc", "sse"}
22
+ _SUPPORTED_APP_INTERFACES = {"auto", "tigr-asgi-contract", "asgi3"}
23
+ _UNSUPPORTED_COMPAT_INTERFACES = {"asgi2", "wsgi", "rsgi"}
24
+ _RUNTIME_EXCLUDED_CLASSIFICATIONS = {"rest", "jsonrpc"}
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class BindingClassification:
29
+ kind: BindingKind
30
+ runtime_owned: bool
31
+ classification_only: bool
32
+ dispatch_runtime: str
33
+ scope_type: str
34
+ family: str
35
+ exchange: str
36
+ framing: str | None = None
37
+ allowed_framings: tuple[str, ...] = ()
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class FamilyCapability:
42
+ family: str
43
+ bindings: tuple[str, ...]
44
+ subevents: tuple[str, ...]
45
+ exchanges: tuple[str, ...]
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class ProductSurfaceStatus:
50
+ kind: ProductSurfaceKind
51
+ runtime_available: bool
52
+ classification_only: bool
53
+ compatibility_exclusion: bool
54
+ reason: str
55
+
56
+
57
+ _FAMILY_CAPABILITIES = {
58
+ "request": FamilyCapability(
59
+ family="request",
60
+ bindings=("http", "http.stream", "rest", "jsonrpc"),
61
+ subevents=("request.open", "request.body_in", "request.chunk_in", "request.close", "request.disconnect"),
62
+ exchanges=("unary", "server_stream"),
63
+ ),
64
+ "session": FamilyCapability(
65
+ family="session",
66
+ bindings=("websocket", "webtransport", "lifespan"),
67
+ subevents=("session.open", "session.accept", "session.ready", "session.heartbeat", "session.close", "session.disconnect"),
68
+ exchanges=("duplex",),
69
+ ),
70
+ "message": FamilyCapability(
71
+ family="message",
72
+ bindings=("websocket",),
73
+ subevents=("message.in", "message.decode", "message.handle", "message.out", "message.ack", "message.nack"),
74
+ exchanges=("duplex",),
75
+ ),
76
+ "stream": FamilyCapability(
77
+ family="stream",
78
+ bindings=("http.stream", "webtransport", "stream", "sse"),
79
+ subevents=("stream.open", "stream.chunk_in", "stream.chunk_out", "stream.flush", "stream.finalize", "stream.abort", "stream.close"),
80
+ exchanges=("server_stream", "duplex"),
81
+ ),
82
+ "datagram": FamilyCapability(
83
+ family="datagram",
84
+ bindings=("webtransport", "datagram"),
85
+ subevents=("datagram.in", "datagram.handle", "datagram.out", "datagram.ack", "datagram.close"),
86
+ exchanges=("duplex",),
87
+ ),
88
+ }
89
+
90
+ _BINDING_SHAPES: dict[str, tuple[str, str, str, str | None, tuple[str, ...]]] = {
91
+ "http": ("http", "request", "unary", None, ()),
92
+ "http.stream": ("http", "stream", "server_stream", None, ()),
93
+ "websocket": ("websocket", "message", "duplex", None, ()),
94
+ "lifespan": ("lifespan", "session", "duplex", None, ()),
95
+ "webtransport": ("webtransport", "session", "duplex", None, ()),
96
+ "stream": ("tigrcorn.stream", "stream", "duplex", None, ()),
97
+ "datagram": ("tigrcorn.datagram", "datagram", "duplex", None, ()),
98
+ "rest": ("http", "request", "unary", "json", ("json",)),
99
+ "jsonrpc": ("http", "request", "unary", "jsonrpc", ("jsonrpc",)),
100
+ "sse": ("http", "stream", "server_stream", "sse", ("sse",)),
101
+ }
102
+
103
+
104
+ def classify_binding(kind: str) -> BindingClassification:
105
+ normalized = kind.strip().lower().replace("_", "-")
106
+ if normalized == "json-rpc":
107
+ normalized = "jsonrpc"
108
+ if normalized not in _SERVER_OWNED_RUNTIMES | _CLASSIFICATION_ONLY:
109
+ raise ConfigError(f"unsupported binding classification: {kind!r}")
110
+ scope_type, family, exchange, framing, allowed_framings = _BINDING_SHAPES[normalized]
111
+ return BindingClassification(
112
+ kind=normalized, # type: ignore[arg-type]
113
+ runtime_owned=normalized in _SERVER_OWNED_RUNTIMES,
114
+ classification_only=normalized in _CLASSIFICATION_ONLY,
115
+ dispatch_runtime="application" if normalized in _CLASSIFICATION_ONLY else "tigrcorn",
116
+ scope_type=scope_type,
117
+ family=family,
118
+ exchange=exchange,
119
+ framing=framing,
120
+ allowed_framings=allowed_framings,
121
+ )
122
+
123
+
124
+ def runtime_interface_available(interface: str) -> bool:
125
+ return product_surface_status(interface).runtime_available
126
+
127
+
128
+ def _normalize_product_surface(value: str) -> ProductSurfaceKind:
129
+ try:
130
+ normalized = value.strip().lower().replace("_", "-")
131
+ except AttributeError as exc:
132
+ raise ConfigError(f"unsupported product surface: {value!r}") from exc
133
+ if normalized == "json-rpc":
134
+ normalized = "jsonrpc"
135
+ if normalized not in _SUPPORTED_APP_INTERFACES | _UNSUPPORTED_COMPAT_INTERFACES | _RUNTIME_EXCLUDED_CLASSIFICATIONS:
136
+ raise ConfigError(f"unsupported product surface: {value!r}")
137
+ return normalized # type: ignore[return-value]
138
+
139
+
140
+ def product_surface_status(surface: str) -> ProductSurfaceStatus:
141
+ normalized = _normalize_product_surface(surface)
142
+ if normalized in _SUPPORTED_APP_INTERFACES:
143
+ return ProductSurfaceStatus(
144
+ kind=normalized,
145
+ runtime_available=True,
146
+ classification_only=False,
147
+ compatibility_exclusion=False,
148
+ reason="supported app interface",
149
+ )
150
+ if normalized in _RUNTIME_EXCLUDED_CLASSIFICATIONS:
151
+ return ProductSurfaceStatus(
152
+ kind=normalized,
153
+ runtime_available=False,
154
+ classification_only=True,
155
+ compatibility_exclusion=False,
156
+ reason="classification-only binding; runtime belongs to the application layer",
157
+ )
158
+ return ProductSurfaceStatus(
159
+ kind=normalized,
160
+ runtime_available=False,
161
+ classification_only=False,
162
+ compatibility_exclusion=True,
163
+ reason="unsupported compatibility interface",
164
+ )
165
+
166
+
167
+ def require_product_runtime_available(surface: str) -> ProductSurfaceStatus:
168
+ status = product_surface_status(surface)
169
+ if not status.runtime_available:
170
+ raise ConfigError(f"unsupported runtime product surface: {surface!r} ({status.reason})")
171
+ return status
172
+
173
+
174
+ def product_surface_excluded(surface: str) -> bool:
175
+ return not product_surface_status(surface).runtime_available
176
+
177
+
178
+ def require_product_boundary_exclusion(surface: str) -> ProductSurfaceStatus:
179
+ status = product_surface_status(surface)
180
+ if status.runtime_available:
181
+ raise ConfigError(f"product surface is supported, not excluded: {surface!r}")
182
+ return status
183
+
184
+
185
+ def family_capability(family: str) -> FamilyCapability:
186
+ normalized = family.strip().lower()
187
+ try:
188
+ return _FAMILY_CAPABILITIES[normalized]
189
+ except KeyError as exc:
190
+ raise ConfigError(f"unsupported contract family: {family!r}") from exc
191
+
192
+
193
+ def validate_binding_legality(*, binding: str, family: str, subevent: str | None = None, exchange: str | None = None) -> None:
194
+ normalized_binding = binding.strip().lower().replace("_", "-")
195
+ if normalized_binding == "json-rpc":
196
+ normalized_binding = "jsonrpc"
197
+ classification = classify_binding(normalized_binding)
198
+ normalized_family = family.strip().lower()
199
+ if classification.classification_only and normalized_family != classification.family:
200
+ raise ConfigError(f"binding {binding!r} is illegal for family {family!r}")
201
+ capability = family_capability(family)
202
+ if normalized_binding not in capability.bindings:
203
+ raise ConfigError(f"binding {binding!r} is illegal for family {family!r}")
204
+ if subevent is not None and subevent not in capability.subevents and not subevent.endswith(".emit_complete"):
205
+ raise ConfigError(f"subevent {subevent!r} is illegal for family {family!r}")
206
+ if classification.classification_only and exchange is not None and exchange != classification.exchange:
207
+ raise ConfigError(f"exchange {exchange!r} is illegal for binding {binding!r}")
208
+ if exchange is not None and exchange not in capability.exchanges:
209
+ raise ConfigError(f"exchange {exchange!r} is illegal for family {family!r}")
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Literal
5
+
6
+ from tigrcorn_core.errors import ProtocolError
7
+
8
+ from .metadata import UnitIdentity, asgi3_extensions
9
+ from .scopes import validate_scope
10
+
11
+ HTTPFeatureKind = Literal[
12
+ "alt-svc",
13
+ "content-coding",
14
+ "early-hints",
15
+ "proxy-normalization",
16
+ "static-delivery",
17
+ "trailers",
18
+ ]
19
+
20
+ _HTTP_FEATURE_EVENTS = {
21
+ "alt-svc": ("http.response.start",),
22
+ "content-coding": ("http.response.start", "http.response.body"),
23
+ "early-hints": ("http.response.start",),
24
+ "proxy-normalization": ("http.request",),
25
+ "static-delivery": ("http.response.pathsend", "http.response.body"),
26
+ "trailers": ("http.request.trailers", "http.response.trailers"),
27
+ }
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class CompatibilityParityRow:
32
+ feature_id: str
33
+ native_contract: bool
34
+ asgi3_compat: bool
35
+ notes: str = ""
36
+
37
+ def as_dict(self) -> dict[str, Any]:
38
+ return {
39
+ "feature_id": self.feature_id,
40
+ "native_contract": self.native_contract,
41
+ "asgi3_compat": self.asgi3_compat,
42
+ "notes": self.notes,
43
+ }
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class HTTPFeatureContractMap:
48
+ feature: HTTPFeatureKind
49
+ contract_events: tuple[str, ...]
50
+ asgi_extensions: tuple[str, ...] = ()
51
+ metadata: dict[str, Any] = field(default_factory=dict)
52
+
53
+ def as_dict(self) -> dict[str, Any]:
54
+ return {
55
+ "feature": self.feature,
56
+ "contract_events": list(self.contract_events),
57
+ "asgi_extensions": list(self.asgi_extensions),
58
+ "metadata": dict(self.metadata),
59
+ }
60
+
61
+
62
+ def asgi3_compat_scope(scope: dict[str, Any], *, extensions: dict[str, Any] | None = None) -> dict[str, Any]:
63
+ validate_scope(scope)
64
+ compat_scope = dict(scope)
65
+ merged_extensions = dict(scope.get("extensions") or {})
66
+ merged_extensions.update(extensions or {})
67
+ merged_extensions.setdefault("tigrcorn.compat", {"interface": "asgi3", "native_contract": False})
68
+ compat_scope["extensions"] = merged_extensions
69
+ return compat_scope
70
+
71
+
72
+ def asgi_extension_bridge(
73
+ *,
74
+ unit: UnitIdentity | None = None,
75
+ capabilities: dict[str, Any] | None = None,
76
+ feature_maps: list[HTTPFeatureContractMap] | None = None,
77
+ **extension_parts: Any,
78
+ ) -> dict[str, Any]:
79
+ bridge = asgi3_extensions(unit=unit, **extension_parts)
80
+ bridge["tigrcorn.capabilities"] = dict(capabilities or {})
81
+ bridge["tigrcorn.http_features"] = [item.as_dict() for item in feature_maps or []]
82
+ return bridge
83
+
84
+
85
+ def compatibility_feature_parity(feature_id: str, *, native_contract: bool, asgi3_compat: bool, notes: str = "") -> CompatibilityParityRow:
86
+ if not feature_id.startswith("feat:"):
87
+ raise ProtocolError("compatibility parity rows require a feature id")
88
+ return CompatibilityParityRow(
89
+ feature_id=feature_id,
90
+ native_contract=native_contract,
91
+ asgi3_compat=asgi3_compat,
92
+ notes=notes,
93
+ )
94
+
95
+
96
+ def http_feature_contract_map(
97
+ feature: str,
98
+ *,
99
+ asgi_extensions: tuple[str, ...] = (),
100
+ metadata: dict[str, Any] | None = None,
101
+ ) -> HTTPFeatureContractMap:
102
+ normalized = feature.strip().lower().replace("_", "-")
103
+ if normalized not in _HTTP_FEATURE_EVENTS:
104
+ raise ProtocolError(f"unsupported HTTP contract feature map: {feature!r}")
105
+ return HTTPFeatureContractMap(
106
+ feature=normalized, # type: ignore[arg-type]
107
+ contract_events=_HTTP_FEATURE_EVENTS[normalized],
108
+ asgi_extensions=asgi_extensions,
109
+ metadata=dict(metadata or {}),
110
+ )
111
+
112
+
113
+ def alt_svc_contract_map(value: str, *, max_age: int | None = None, persist: bool = False) -> HTTPFeatureContractMap:
114
+ if not value:
115
+ raise ProtocolError("Alt-Svc contract map requires a header value")
116
+ metadata: dict[str, Any] = {"header": value, "persist": persist}
117
+ if max_age is not None:
118
+ metadata["max_age"] = max_age
119
+ return http_feature_contract_map("alt-svc", metadata=metadata)
120
+
121
+
122
+ def content_coding_contract_map(codings: tuple[str, ...]) -> HTTPFeatureContractMap:
123
+ if not codings:
124
+ raise ProtocolError("content-coding contract map requires at least one coding")
125
+ return http_feature_contract_map("content-coding", metadata={"codings": list(codings)})
126
+
127
+
128
+ def early_hints_contract_map(headers: list[tuple[bytes, bytes]]) -> HTTPFeatureContractMap:
129
+ if not headers:
130
+ raise ProtocolError("early-hints contract map requires headers")
131
+ return http_feature_contract_map("early-hints", metadata={"status": 103, "headers": headers})
132
+
133
+
134
+ def proxy_normalization_contract_map(*, trusted: bool, forwarded_for: str | None = None, scheme: str | None = None) -> HTTPFeatureContractMap:
135
+ metadata = {"trusted": trusted}
136
+ if forwarded_for is not None:
137
+ metadata["forwarded_for"] = forwarded_for
138
+ if scheme is not None:
139
+ metadata["scheme"] = scheme
140
+ return http_feature_contract_map("proxy-normalization", metadata=metadata)
141
+
142
+
143
+ def static_delivery_contract_map(path: str, *, pathsend: bool = True, range_request: bool = False, etag: str | None = None) -> HTTPFeatureContractMap:
144
+ if not path.startswith("/"):
145
+ raise ProtocolError("static delivery contract path must be absolute")
146
+ metadata: dict[str, Any] = {"path": path, "pathsend": pathsend, "range_request": range_request}
147
+ if etag is not None:
148
+ metadata["etag"] = etag
149
+ return http_feature_contract_map(
150
+ "static-delivery",
151
+ asgi_extensions=("http.response.pathsend",) if pathsend else (),
152
+ metadata=metadata,
153
+ )
154
+
155
+
156
+ def trailers_contract_map(*, request: bool = False, response: bool = False) -> HTTPFeatureContractMap:
157
+ if not request and not response:
158
+ raise ProtocolError("trailers contract map requires request or response trailers")
159
+ extensions = []
160
+ if request:
161
+ extensions.append("tigrcorn.http.request_trailers")
162
+ if response:
163
+ extensions.append("http.response.trailers")
164
+ return http_feature_contract_map("trailers", asgi_extensions=tuple(extensions), metadata={"request": request, "response": response})
165
+
166
+
167
+ def observability_contract_metadata(
168
+ *,
169
+ unit_id: str,
170
+ feature_id: str,
171
+ boundary_id: str,
172
+ attributes: dict[str, Any] | None = None,
173
+ ) -> dict[str, Any]:
174
+ if not unit_id or not feature_id.startswith("feat:") or not boundary_id.startswith("bnd:"):
175
+ raise ProtocolError("observability contract metadata requires unit, feature, and boundary ids")
176
+ return {
177
+ "tigrcorn.observability": {
178
+ "unit_id": unit_id,
179
+ "feature_id": feature_id,
180
+ "boundary_id": boundary_id,
181
+ "attributes": dict(attributes or {}),
182
+ }
183
+ }