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
tigrbl_kernel/__init__.py
CHANGED
|
@@ -3,19 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
from importlib import import_module
|
|
4
4
|
from typing import Any, Dict, List, Mapping
|
|
5
5
|
|
|
6
|
-
try: # pragma: no cover - additive optional integration
|
|
7
|
-
_runtime_rust = import_module("tigrbl_runtime.rust")
|
|
8
|
-
ExecutionBackend = _runtime_rust.ExecutionBackend
|
|
9
|
-
RustBackendConfig = _runtime_rust.RustBackendConfig
|
|
10
|
-
except Exception: # pragma: no cover
|
|
11
|
-
ExecutionBackend = RustBackendConfig = None
|
|
12
|
-
|
|
13
|
-
from .rust_compile import (
|
|
14
|
-
build_rust_kernel,
|
|
15
|
-
build_rust_parity_snapshot,
|
|
16
|
-
normalize_rust_spec,
|
|
17
|
-
)
|
|
18
|
-
from .rust_plan import RustPlan
|
|
19
6
|
from .measure import (
|
|
20
7
|
build_packed_kernel_measurement_view,
|
|
21
8
|
load_packed_kernel_hot_block,
|
|
@@ -80,18 +67,13 @@ def plan_labels(model: type, alias: str) -> list[str]:
|
|
|
80
67
|
|
|
81
68
|
|
|
82
69
|
__all__ = [
|
|
83
|
-
"ExecutionBackend",
|
|
84
70
|
"Kernel",
|
|
85
|
-
"RustBackendConfig",
|
|
86
|
-
"RustPlan",
|
|
87
71
|
"BatchOpPlan",
|
|
88
72
|
"OpView",
|
|
89
73
|
"PackedKernel",
|
|
90
74
|
"SchemaIn",
|
|
91
75
|
"SchemaOut",
|
|
92
76
|
"build_kernel_plan",
|
|
93
|
-
"build_rust_kernel",
|
|
94
|
-
"build_rust_parity_snapshot",
|
|
95
77
|
"build_packed_kernel",
|
|
96
78
|
"build_packed_kernel_measurement_view",
|
|
97
79
|
"get_cached_specs",
|
|
@@ -99,9 +81,9 @@ __all__ = [
|
|
|
99
81
|
"load_packed_kernel_hot_block",
|
|
100
82
|
"load_packed_kernel_hot_sections",
|
|
101
83
|
"measure_packed_kernel",
|
|
102
|
-
"normalize_rust_spec",
|
|
103
84
|
"packed_kernel_measurement",
|
|
104
85
|
"plan_labels",
|
|
86
|
+
"protocol_streams",
|
|
105
87
|
"serialize_packed_kernel_measurement_view",
|
|
106
88
|
"segment_fusion",
|
|
107
89
|
"transport_atoms",
|
tigrbl_kernel/_build.py
CHANGED
|
@@ -15,7 +15,7 @@ from tigrbl_atoms.atoms.sys.phase_db import run as _bind_phase_db
|
|
|
15
15
|
from tigrbl_atoms.phases import phase_info
|
|
16
16
|
from tigrbl_atoms.types import EdgeTarget, PhaseTreeEdge, PhaseTreeNode, error_phase_for
|
|
17
17
|
from tigrbl_core.config.resolver import resolve_cfg
|
|
18
|
-
from tigrbl_typing.phases import normalize_phase
|
|
18
|
+
from tigrbl_typing.phases import canonicalize_phase_input as normalize_phase
|
|
19
19
|
|
|
20
20
|
from . import events as _ev
|
|
21
21
|
from .atoms import (
|
tigrbl_kernel/_compile.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import replace
|
|
4
|
+
from types import SimpleNamespace
|
|
4
5
|
from typing import Any, Mapping
|
|
5
6
|
|
|
6
7
|
from tigrbl_atoms import StepFn
|
|
8
|
+
from tigrbl_core._spec.binding_spec import HttpRestBindingSpec
|
|
7
9
|
from tigrbl_core.config.constants import __JSONRPC_DEFAULT_ENDPOINT__
|
|
10
|
+
from tigrbl_core._spec.op_spec import OpSpec
|
|
11
|
+
from tigrbl_core._spec.well_known_spec import well_known_op_alias
|
|
8
12
|
|
|
9
13
|
from . import events as _ev
|
|
10
14
|
from .models import KernelPlan, OpKey, OpMeta, OpView
|
|
@@ -24,6 +28,101 @@ from .utils import (
|
|
|
24
28
|
DEFAULT_PHASE_ORDER = tuple(getattr(_ev, "PHASES", ())) or _DEFAULT_PHASE_ORDER
|
|
25
29
|
|
|
26
30
|
|
|
31
|
+
def _pathspec_iter(app: Any) -> tuple[Any, ...]:
|
|
32
|
+
collected: list[Any] = []
|
|
33
|
+
collected.extend(tuple(getattr(app, "_tigrbl_path_specs", ()) or ()))
|
|
34
|
+
routers = getattr(app, "routers", None)
|
|
35
|
+
if isinstance(routers, Mapping):
|
|
36
|
+
router_values = tuple(routers.values())
|
|
37
|
+
elif isinstance(routers, (list, tuple)):
|
|
38
|
+
router_values = tuple(routers)
|
|
39
|
+
else:
|
|
40
|
+
router_values = ()
|
|
41
|
+
for router in router_values:
|
|
42
|
+
collected.extend(tuple(getattr(router, "_tigrbl_path_specs", ()) or ()))
|
|
43
|
+
|
|
44
|
+
seen: set[tuple[str, str, str]] = set()
|
|
45
|
+
deduped: list[Any] = []
|
|
46
|
+
for path in collected:
|
|
47
|
+
resource = getattr(path, "well_known", None)
|
|
48
|
+
key = (
|
|
49
|
+
str(getattr(path, "kind", "")),
|
|
50
|
+
str(getattr(path, "path", "")),
|
|
51
|
+
str(getattr(resource, "name", "")),
|
|
52
|
+
)
|
|
53
|
+
if key in seen:
|
|
54
|
+
continue
|
|
55
|
+
seen.add(key)
|
|
56
|
+
deduped.append(path)
|
|
57
|
+
return tuple(deduped)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _well_known_path_model(app: Any) -> type | None:
|
|
61
|
+
pathspecs = tuple(
|
|
62
|
+
path
|
|
63
|
+
for path in _pathspec_iter(app)
|
|
64
|
+
if getattr(path, "kind", None) == "well-known"
|
|
65
|
+
and getattr(path, "well_known", None) is not None
|
|
66
|
+
)
|
|
67
|
+
if not pathspecs:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
ops: list[Any] = []
|
|
71
|
+
resources: dict[str, dict[str, Any]] = {}
|
|
72
|
+
for path in pathspecs:
|
|
73
|
+
resource = path.well_known
|
|
74
|
+
alias = well_known_op_alias(resource.name)
|
|
75
|
+
resources[alias] = {
|
|
76
|
+
"name": resource.name,
|
|
77
|
+
"payload": resource.payload,
|
|
78
|
+
"media_type": resource.media_type,
|
|
79
|
+
"status_code": resource.status_code,
|
|
80
|
+
"headers": dict(resource.headers or {}),
|
|
81
|
+
}
|
|
82
|
+
ops.append(
|
|
83
|
+
OpSpec(
|
|
84
|
+
alias=alias,
|
|
85
|
+
target="well_known",
|
|
86
|
+
arity="collection",
|
|
87
|
+
persist="skip",
|
|
88
|
+
expose_routes=False,
|
|
89
|
+
expose_rpc=False,
|
|
90
|
+
bindings=(
|
|
91
|
+
HttpRestBindingSpec(
|
|
92
|
+
proto="http.rest",
|
|
93
|
+
path=str(path.path),
|
|
94
|
+
methods=("GET",),
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
status_code=resource.status_code,
|
|
98
|
+
exchange="request_response",
|
|
99
|
+
tx_scope="none",
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
ops_tuple = tuple(ops)
|
|
104
|
+
model = type("TigrblWellKnownPathOps", (), {})
|
|
105
|
+
model.resource_name = "well_known"
|
|
106
|
+
model.__tigrbl_well_known_resources__ = resources
|
|
107
|
+
model.__tigrbl_ops__ = ops_tuple
|
|
108
|
+
model.ops = SimpleNamespace(
|
|
109
|
+
all=ops_tuple,
|
|
110
|
+
by_alias={op.alias: (op,) for op in ops_tuple},
|
|
111
|
+
by_key={(op.alias, op.target): op for op in ops_tuple},
|
|
112
|
+
)
|
|
113
|
+
model.opspecs = model.ops
|
|
114
|
+
setattr(app, "_tigrbl_kernel_well_known_model", model)
|
|
115
|
+
return model
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _compile_models(app: Any) -> tuple[Any, ...]:
|
|
119
|
+
models = list(_table_iter(app))
|
|
120
|
+
well_known_model = _well_known_path_model(app)
|
|
121
|
+
if well_known_model is not None:
|
|
122
|
+
models.append(well_known_model)
|
|
123
|
+
return tuple(models)
|
|
124
|
+
|
|
125
|
+
|
|
27
126
|
def _compile_opview_from_specs(self: Any, specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
28
127
|
return compile_opview_from_specs(specs, sp)
|
|
29
128
|
|
|
@@ -50,7 +149,7 @@ def _compile_plan(self: Any, app: Any) -> KernelPlan:
|
|
|
50
149
|
egress_chain = self._build_egress(app)
|
|
51
150
|
phases, mainline_phases, error_phases = _phase_info_map(DEFAULT_PHASE_ORDER)
|
|
52
151
|
|
|
53
|
-
for model in
|
|
152
|
+
for model in _compile_models(app):
|
|
54
153
|
for sp in _opspecs(model):
|
|
55
154
|
meta_index = len(opmeta)
|
|
56
155
|
target = (getattr(sp, "target", sp.alias) or sp.alias).lower()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from tigrbl_core._spec.hook_spec import matches_hook_selector
|
|
7
|
+
|
|
8
|
+
from .webtransport_events import validate_webtransport_event_payload
|
|
9
|
+
|
|
10
|
+
WEBTRANSPORT_SUBEVENTS = {
|
|
11
|
+
"webtransport.connect": "session.open",
|
|
12
|
+
"webtransport.accept": "session.accept",
|
|
13
|
+
"webtransport.disconnect": "session.close",
|
|
14
|
+
"webtransport.close": "session.close",
|
|
15
|
+
"webtransport.stream.receive": "stream.chunk.received",
|
|
16
|
+
"webtransport.stream.send": "stream.chunk.emit",
|
|
17
|
+
"webtransport.stream.close": "stream.close",
|
|
18
|
+
"webtransport.stream.reset": "stream.close",
|
|
19
|
+
"webtransport.stream.stop_sending": "stream.close",
|
|
20
|
+
"webtransport.datagram.receive": "datagram.received",
|
|
21
|
+
"webtransport.datagram.send": "datagram.emit",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def normalize_exchange(exchange: str | None) -> str:
|
|
26
|
+
token = str(exchange or "request_response")
|
|
27
|
+
if token == "bidirectional":
|
|
28
|
+
return "bidirectional_stream"
|
|
29
|
+
return token
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def channel_family(scope_type: str, exchange: str) -> str:
|
|
33
|
+
exchange = normalize_exchange(exchange)
|
|
34
|
+
if scope_type == "websocket":
|
|
35
|
+
return "socket"
|
|
36
|
+
if scope_type == "webtransport":
|
|
37
|
+
return "session"
|
|
38
|
+
if exchange in {"server_stream", "client_stream", "bidirectional_stream"}:
|
|
39
|
+
return "stream"
|
|
40
|
+
if exchange == "fire_and_forget":
|
|
41
|
+
return "request"
|
|
42
|
+
return "response"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def channel_kind(scope_type: str, exchange: str) -> str:
|
|
46
|
+
exchange = normalize_exchange(exchange)
|
|
47
|
+
if scope_type == "websocket":
|
|
48
|
+
return "websocket"
|
|
49
|
+
if scope_type == "webtransport":
|
|
50
|
+
return "webtransport"
|
|
51
|
+
if exchange in {"server_stream", "client_stream", "bidirectional_stream"}:
|
|
52
|
+
return "stream"
|
|
53
|
+
if exchange == "event_stream":
|
|
54
|
+
return "sse"
|
|
55
|
+
return "http"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def channel_subevents(scope_type: str, exchange: str) -> tuple[str, ...]:
|
|
59
|
+
exchange = normalize_exchange(exchange)
|
|
60
|
+
if scope_type in {"websocket", "webtransport"}:
|
|
61
|
+
return ("connect", "receive", "emit", "complete", "disconnect")
|
|
62
|
+
if exchange in {"server_stream", "client_stream", "bidirectional_stream", "event_stream"}:
|
|
63
|
+
return ("receive", "emit", "complete")
|
|
64
|
+
return ("receive", "emit", "complete")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def webtransport_event_metadata(
|
|
68
|
+
*,
|
|
69
|
+
direction: str,
|
|
70
|
+
message: Mapping[str, Any],
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
event_type = str(message.get("type") or "")
|
|
73
|
+
metadata: dict[str, Any] = {
|
|
74
|
+
"binding": "webtransport",
|
|
75
|
+
"event_type": event_type,
|
|
76
|
+
"type": event_type,
|
|
77
|
+
"subevent": WEBTRANSPORT_SUBEVENTS.get(event_type, event_type),
|
|
78
|
+
"framing": message.get("framing"),
|
|
79
|
+
}
|
|
80
|
+
try:
|
|
81
|
+
projection = validate_webtransport_event_payload(
|
|
82
|
+
event=event_type,
|
|
83
|
+
channel=direction,
|
|
84
|
+
payload=dict(message),
|
|
85
|
+
)
|
|
86
|
+
except ValueError:
|
|
87
|
+
projection = {}
|
|
88
|
+
for key in (
|
|
89
|
+
"family",
|
|
90
|
+
"lane",
|
|
91
|
+
"exchange",
|
|
92
|
+
"stream_initiator",
|
|
93
|
+
"stream_direction",
|
|
94
|
+
"direction",
|
|
95
|
+
"lane_id",
|
|
96
|
+
"stream_ordinal",
|
|
97
|
+
"stream_id_width",
|
|
98
|
+
):
|
|
99
|
+
if projection.get(key) is not None:
|
|
100
|
+
metadata[key] = projection[key]
|
|
101
|
+
if "family" not in metadata:
|
|
102
|
+
if ".stream." in event_type:
|
|
103
|
+
metadata["family"] = "stream"
|
|
104
|
+
elif ".datagram." in event_type:
|
|
105
|
+
metadata["family"] = "datagram"
|
|
106
|
+
else:
|
|
107
|
+
metadata["family"] = "session"
|
|
108
|
+
if "lane" not in metadata:
|
|
109
|
+
metadata["lane"] = metadata["family"]
|
|
110
|
+
if "exchange" not in metadata:
|
|
111
|
+
metadata["exchange"] = "request_response"
|
|
112
|
+
return metadata
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def select_webtransport_hooks(
|
|
116
|
+
hooks: Iterable[Any],
|
|
117
|
+
*,
|
|
118
|
+
direction: str,
|
|
119
|
+
metadata: Mapping[str, Any],
|
|
120
|
+
) -> tuple[Any, ...]:
|
|
121
|
+
allowed_phases = (
|
|
122
|
+
{"PRE_HANDLER", "HANDLER"}
|
|
123
|
+
if direction == "receive"
|
|
124
|
+
else {"POST_HANDLER", "POST_RESPONSE"}
|
|
125
|
+
)
|
|
126
|
+
return tuple(
|
|
127
|
+
hook
|
|
128
|
+
for hook in hooks
|
|
129
|
+
if str(getattr(hook.phase, "value", hook.phase)) in allowed_phases
|
|
130
|
+
if matches_hook_selector(hook, metadata)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = [
|
|
135
|
+
"WEBTRANSPORT_SUBEVENTS",
|
|
136
|
+
"channel_family",
|
|
137
|
+
"channel_kind",
|
|
138
|
+
"channel_subevents",
|
|
139
|
+
"normalize_exchange",
|
|
140
|
+
"select_webtransport_hooks",
|
|
141
|
+
"webtransport_event_metadata",
|
|
142
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from hashlib import sha256
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tigrbl_kernel.protocol_bindings import compile_binding_protocol_plan
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
TRANSPORT_ENVELOPE_KEYS = frozenset(
|
|
12
|
+
{
|
|
13
|
+
"headers",
|
|
14
|
+
"http_status",
|
|
15
|
+
"jsonrpc_id",
|
|
16
|
+
"jsonrpc_version",
|
|
17
|
+
"path",
|
|
18
|
+
"rpc_method",
|
|
19
|
+
"stream_id",
|
|
20
|
+
"transport",
|
|
21
|
+
"ws_opcode",
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class EquivalenceCase:
|
|
28
|
+
op_id: str
|
|
29
|
+
category: str
|
|
30
|
+
bindings: tuple[Mapping[str, Any], ...]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def canonical_operation_id(op_id: str) -> str:
|
|
34
|
+
normalized = str(op_id).strip()
|
|
35
|
+
if not normalized or "." not in normalized:
|
|
36
|
+
raise ValueError("canonical operation id must be a non-empty table.operation token")
|
|
37
|
+
table, op = normalized.split(".", maxsplit=1)
|
|
38
|
+
if not table or not op:
|
|
39
|
+
raise ValueError("canonical operation id requires table and operation components")
|
|
40
|
+
return f"{table}.{op}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compile_equivalence_manifest(
|
|
44
|
+
op_id: str,
|
|
45
|
+
bindings: Iterable[Mapping[str, Any]],
|
|
46
|
+
*,
|
|
47
|
+
schema_identity: str = "schema:v1",
|
|
48
|
+
runtime_plan_identity: str = "runtime-plan:v1",
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
canonical_id = canonical_operation_id(op_id)
|
|
51
|
+
plans = tuple(compile_binding_protocol_plan(canonical_id, binding) for binding in bindings)
|
|
52
|
+
if not plans:
|
|
53
|
+
raise ValueError("at least one transport binding is required")
|
|
54
|
+
return {
|
|
55
|
+
"op_id": canonical_id,
|
|
56
|
+
"schema_identity": str(schema_identity),
|
|
57
|
+
"runtime_plan_identity": str(runtime_plan_identity),
|
|
58
|
+
"bindings": tuple(_plan_fingerprint(plan) for plan in plans),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalized_transport_result(result: Mapping[str, Any]) -> dict[str, Any]:
|
|
63
|
+
semantic = {
|
|
64
|
+
key: _freeze(value)
|
|
65
|
+
for key, value in result.items()
|
|
66
|
+
if key not in TRANSPORT_ENVELOPE_KEYS
|
|
67
|
+
}
|
|
68
|
+
if "diagnostics" in semantic and isinstance(semantic["diagnostics"], Mapping):
|
|
69
|
+
diagnostics = dict(semantic["diagnostics"])
|
|
70
|
+
diagnostics.pop("trace_id", None)
|
|
71
|
+
diagnostics.pop("qlog_id", None)
|
|
72
|
+
semantic["diagnostics"] = _freeze(diagnostics)
|
|
73
|
+
return dict(sorted(semantic.items()))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def equivalent_transport_results(*results: Mapping[str, Any]) -> bool:
|
|
77
|
+
if not results:
|
|
78
|
+
raise ValueError("at least one transport result is required")
|
|
79
|
+
first = normalized_transport_result(results[0])
|
|
80
|
+
return all(normalized_transport_result(result) == first for result in results[1:])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def equivalent_binding_group(op_id: str, bindings: Iterable[Mapping[str, Any]]) -> bool:
|
|
84
|
+
manifest = compile_equivalence_manifest(op_id, bindings)
|
|
85
|
+
families = {binding["family"] for binding in manifest["bindings"]}
|
|
86
|
+
return bool(families) and len({manifest["op_id"]}) == 1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def standard_equivalence_corpus() -> tuple[EquivalenceCase, ...]:
|
|
90
|
+
return (
|
|
91
|
+
EquivalenceCase(
|
|
92
|
+
"Item.read",
|
|
93
|
+
"standard",
|
|
94
|
+
(
|
|
95
|
+
{"kind": "http.rest", "path": "/items/{id}", "methods": ("GET",)},
|
|
96
|
+
{"kind": "http.jsonrpc", "rpc_method": "Item.read"},
|
|
97
|
+
{"kind": "ws", "path": "/socket", "framing": "jsonrpc", "subprotocols": ("jsonrpc",)},
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
EquivalenceCase(
|
|
101
|
+
"Item.bulk_create",
|
|
102
|
+
"bulk",
|
|
103
|
+
(
|
|
104
|
+
{"kind": "http.rest", "path": "/items/bulk", "methods": ("POST",)},
|
|
105
|
+
{"kind": "http.jsonrpc", "rpc_method": "Item.bulk_create"},
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
EquivalenceCase(
|
|
109
|
+
"Item.search",
|
|
110
|
+
"query",
|
|
111
|
+
(
|
|
112
|
+
{"kind": "http.rest", "path": "/items", "methods": ("GET",)},
|
|
113
|
+
{"kind": "http.jsonrpc", "rpc_method": "Item.search"},
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
EquivalenceCase(
|
|
117
|
+
"Item.tail",
|
|
118
|
+
"stream",
|
|
119
|
+
(
|
|
120
|
+
{"kind": "http.stream", "path": "/items/tail"},
|
|
121
|
+
{"kind": "http.sse", "path": "/items/events"},
|
|
122
|
+
{"kind": "webtransport", "profile": "bidi_stream"},
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
EquivalenceCase(
|
|
126
|
+
"Item.custom_action",
|
|
127
|
+
"custom",
|
|
128
|
+
(
|
|
129
|
+
{"kind": "http.jsonrpc", "rpc_method": "Item.custom_action"},
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _plan_fingerprint(plan: Mapping[str, Any]) -> dict[str, Any]:
|
|
136
|
+
stable = {
|
|
137
|
+
"binding_kind": plan["binding_kind"],
|
|
138
|
+
"family": plan["family"],
|
|
139
|
+
"framing": plan["framing"],
|
|
140
|
+
"capability_mask": plan["capability_requirements"]["required_mask"],
|
|
141
|
+
"lifecycle": tuple(
|
|
142
|
+
(row["family"], row["subevent"]) for row in plan["lifecycle_rows"]
|
|
143
|
+
),
|
|
144
|
+
}
|
|
145
|
+
stable["fingerprint"] = sha256(repr(stable).encode("utf-8")).hexdigest()
|
|
146
|
+
return stable
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _freeze(value: Any) -> Any:
|
|
150
|
+
if isinstance(value, Mapping):
|
|
151
|
+
return {key: _freeze(value[key]) for key in sorted(value)}
|
|
152
|
+
if isinstance(value, list):
|
|
153
|
+
return tuple(_freeze(item) for item in value)
|
|
154
|
+
if isinstance(value, tuple):
|
|
155
|
+
return tuple(_freeze(item) for item in value)
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"EquivalenceCase",
|
|
161
|
+
"canonical_operation_id",
|
|
162
|
+
"compile_equivalence_manifest",
|
|
163
|
+
"equivalent_binding_group",
|
|
164
|
+
"equivalent_transport_results",
|
|
165
|
+
"normalized_transport_result",
|
|
166
|
+
"standard_equivalence_corpus",
|
|
167
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
_BINDINGS = {
|
|
6
|
+
"http.rest": ("request_response", "request_response", "request.received"),
|
|
7
|
+
"http.jsonrpc": ("request_response", "rpc", "rpc.request"),
|
|
8
|
+
"http.stream": ("server_stream", "stream", "stream.start"),
|
|
9
|
+
"http.sse": ("server_stream", "event_stream", "message.emit"),
|
|
10
|
+
"websocket": ("bidirectional_stream", "socket", "message.received"),
|
|
11
|
+
"ws": ("bidirectional_stream", "socket", "message.received"),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def derive_runtime_event(event: dict[str, Any]) -> dict[str, Any]:
|
|
16
|
+
subevent = event.get("subevent")
|
|
17
|
+
if isinstance(subevent, str) and "." not in subevent:
|
|
18
|
+
raise ValueError("subevent must be qualified, for example request.received")
|
|
19
|
+
binding = str(event.get("binding", ""))
|
|
20
|
+
try:
|
|
21
|
+
exchange, family, default_subevent = _BINDINGS[binding]
|
|
22
|
+
except KeyError as exc:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"unsupported binding for exchange/subevent dispatch: {binding}"
|
|
25
|
+
) from exc
|
|
26
|
+
return {
|
|
27
|
+
**event,
|
|
28
|
+
"binding": binding,
|
|
29
|
+
"exchange": exchange,
|
|
30
|
+
"family": family,
|
|
31
|
+
"subevent": subevent or default_subevent,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_operation(metadata: dict[str, Any]) -> dict[str, Any]:
|
|
36
|
+
binding = str(metadata.get("binding", ""))
|
|
37
|
+
subevent = str(metadata.get("subevent", ""))
|
|
38
|
+
method = str(metadata.get("method", metadata.get("path", "")))
|
|
39
|
+
return {
|
|
40
|
+
"exchange": metadata["exchange"],
|
|
41
|
+
"family": metadata["family"],
|
|
42
|
+
"subevent": subevent,
|
|
43
|
+
"op_code": f"{binding}:{subevent}:{method}".strip(":"),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["derive_runtime_event", "resolve_operation"]
|
tigrbl_kernel/events.py
CHANGED
|
@@ -4,3 +4,13 @@ Canonical event anchors and ordering now live in ``tigrbl_atoms.events``.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from tigrbl_atoms.events import * # noqa: F403
|
|
7
|
+
from tigrbl_atoms.events import __all__ as _ATOM_EVENT_EXPORTS
|
|
8
|
+
from tigrbl_atoms.events import canonicalize_phase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_error_phase(name: str) -> bool:
|
|
12
|
+
phase = canonicalize_phase(name)
|
|
13
|
+
return str(phase).startswith("ON_") or phase == "TX_ROLLBACK"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [*_ATOM_EVENT_EXPORTS, "is_error_phase"]
|
tigrbl_kernel/helpers.py
CHANGED
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
from typing import Any, Iterable, Mapping, Optional, Sequence
|
|
7
7
|
|
|
8
8
|
from tigrbl_atoms.types import AtomFailure, BaseCtx, FailedCtx, build_error_ctx
|
|
9
|
-
from tigrbl_typing.phases import
|
|
9
|
+
from tigrbl_typing.phases import canonicalize_phase_input
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
12
|
from tigrbl_kernel import trace as _trace # type: ignore
|
|
@@ -210,7 +210,7 @@ async def _run_chain(ctx: Any, chain: Optional[Iterable[Any]], *, phase: str) ->
|
|
|
210
210
|
def _g(phases: Optional[Mapping[str, Sequence[Any]]], key: str) -> Sequence[Any]:
|
|
211
211
|
if not phases:
|
|
212
212
|
return ()
|
|
213
|
-
normalized =
|
|
213
|
+
normalized = canonicalize_phase_input(key) or key
|
|
214
214
|
chain = phases.get(normalized, ())
|
|
215
215
|
if chain:
|
|
216
216
|
return chain
|
tigrbl_kernel/loop_modes.py
CHANGED
|
@@ -31,4 +31,22 @@ def select_loop_mode(
|
|
|
31
31
|
return "dispatch" if handlers else "owner"
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
def build_loop_controller(
|
|
35
|
+
*,
|
|
36
|
+
mode: str,
|
|
37
|
+
binding: str,
|
|
38
|
+
subevent_handlers: Iterable[str] = (),
|
|
39
|
+
) -> dict[str, object]:
|
|
40
|
+
if mode not in {"owner", "dispatch"}:
|
|
41
|
+
raise ValueError(f"unsupported runtime loop mode {mode!r}")
|
|
42
|
+
handlers = tuple(subevent_handlers or ())
|
|
43
|
+
return {
|
|
44
|
+
"mode": mode,
|
|
45
|
+
"binding": binding,
|
|
46
|
+
"subevent_handlers": handlers,
|
|
47
|
+
"dispatches_subevents": mode == "dispatch",
|
|
48
|
+
"owner_controls_receive": mode == "owner",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["build_loop_controller", "select_loop_mode"]
|
tigrbl_kernel/measure.py
CHANGED
tigrbl_kernel/ordering.py
CHANGED