tigrbl-kernel 0.4.5.dev4__py3-none-any.whl → 0.4.5.dev8__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 +2 -0
- tigrbl_kernel/_build.py +19 -1
- tigrbl_kernel/_compile.py +35 -7
- tigrbl_kernel/models.py +8 -0
- tigrbl_kernel/opview_compiler.py +183 -7
- tigrbl_kernel/packed_selectors.py +145 -5
- tigrbl_kernel/protocol_chains/websocket.py +494 -2
- tigrbl_kernel/webtransport_session_plan.py +92 -0
- {tigrbl_kernel-0.4.5.dev4.dist-info → tigrbl_kernel-0.4.5.dev8.dist-info}/METADATA +9 -9
- {tigrbl_kernel-0.4.5.dev4.dist-info → tigrbl_kernel-0.4.5.dev8.dist-info}/RECORD +13 -12
- {tigrbl_kernel-0.4.5.dev4.dist-info → tigrbl_kernel-0.4.5.dev8.dist-info}/WHEEL +1 -1
- {tigrbl_kernel-0.4.5.dev4.dist-info → tigrbl_kernel-0.4.5.dev8.dist-info}/licenses/LICENSE +0 -0
- {tigrbl_kernel-0.4.5.dev4.dist-info → tigrbl_kernel-0.4.5.dev8.dist-info}/licenses/NOTICE +0 -0
tigrbl_kernel/__init__.py
CHANGED
|
@@ -18,6 +18,7 @@ _LAZY_EXPORTS = {
|
|
|
18
18
|
"PackedKernel": "models",
|
|
19
19
|
"SchemaIn": "models",
|
|
20
20
|
"SchemaOut": "models",
|
|
21
|
+
"SchemaStored": "models",
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
_default_kernel = None
|
|
@@ -73,6 +74,7 @@ __all__ = [
|
|
|
73
74
|
"PackedKernel",
|
|
74
75
|
"SchemaIn",
|
|
75
76
|
"SchemaOut",
|
|
77
|
+
"SchemaStored",
|
|
76
78
|
"build_kernel_plan",
|
|
77
79
|
"build_packed_kernel",
|
|
78
80
|
"build_packed_kernel_measurement_view",
|
tigrbl_kernel/_build.py
CHANGED
|
@@ -91,6 +91,12 @@ _DECODER_TIME = 9
|
|
|
91
91
|
_WS_FAST_PATH_BYPASS_ATOM_NAMES = frozenset(
|
|
92
92
|
{
|
|
93
93
|
"ingress.ctx_init",
|
|
94
|
+
"ingress.transport_extract",
|
|
95
|
+
"transport.sink_bind",
|
|
96
|
+
"ingress.input_prepare",
|
|
97
|
+
"dispatch.binding_match",
|
|
98
|
+
"dispatch.binding_parse",
|
|
99
|
+
"dispatch.input_normalize",
|
|
94
100
|
"dispatch.op_resolve",
|
|
95
101
|
"schema.collect_in",
|
|
96
102
|
"handler.custom",
|
|
@@ -213,6 +219,8 @@ def _is_batch_domain_run(run: Any) -> bool:
|
|
|
213
219
|
except ValueError:
|
|
214
220
|
return False
|
|
215
221
|
domain = parts[index + 1] if index + 1 < len(parts) else None
|
|
222
|
+
if domain == "transport" and parts[-1] == "sink_bind":
|
|
223
|
+
return False
|
|
216
224
|
return domain in {"transport", "intent", "batch", "fanout"}
|
|
217
225
|
|
|
218
226
|
|
|
@@ -1156,8 +1164,18 @@ def _pack_kernel_plan(
|
|
|
1156
1164
|
)
|
|
1157
1165
|
alias_name = str(getattr(meta, "alias", "") or "")
|
|
1158
1166
|
target_name = str(getattr(meta, "target", "") or "")
|
|
1167
|
+
opview = opviews[program_id] if program_id < len(opviews) else None
|
|
1159
1168
|
compiled_param_eligible = not (
|
|
1160
|
-
alias_name.startswith("bulk_")
|
|
1169
|
+
alias_name.startswith("bulk_")
|
|
1170
|
+
or target_name.startswith("bulk_")
|
|
1171
|
+
# Selector aliases and URL precedence are validated by wire.validate_in.
|
|
1172
|
+
# The scalar compiled decoder does not implement that contract yet.
|
|
1173
|
+
or any(
|
|
1174
|
+
entry.get("selector")
|
|
1175
|
+
for entry in (
|
|
1176
|
+
getattr(getattr(opview, "schema_in", None), "by_field", {}) or {}
|
|
1177
|
+
).values()
|
|
1178
|
+
)
|
|
1161
1179
|
)
|
|
1162
1180
|
compiled_param_phase_steps = (
|
|
1163
1181
|
program_compiled_param_phase_steps[program_id]
|
tigrbl_kernel/_compile.py
CHANGED
|
@@ -95,6 +95,7 @@ def _route_metadata_for_binding(binding: Any) -> dict[str, Any]:
|
|
|
95
95
|
metadata["lane"] = lane
|
|
96
96
|
metadata["inner_framing"] = framing_kind(inner_framing)
|
|
97
97
|
metadata["inner_framing_spec"] = framing_spec_name(inner_framing)
|
|
98
|
+
metadata["rpc_method"] = getattr(binding, "rpc_method", None)
|
|
98
99
|
if inner_framing is not None:
|
|
99
100
|
metadata["inner_framing_kind"] = str(inner_framing)
|
|
100
101
|
metadata["inner_framing_spec"] = framing_spec_name(inner_framing)
|
|
@@ -206,8 +207,6 @@ def _compile_plan(self: Any, app: Any) -> KernelPlan:
|
|
|
206
207
|
from tigrbl_core._spec.binding_spec import (
|
|
207
208
|
HttpJsonRpcBindingSpec,
|
|
208
209
|
HttpRestBindingSpec,
|
|
209
|
-
HttpJsonRpcProtocolBindingSpec,
|
|
210
|
-
HttpRestProtocolBindingSpec,
|
|
211
210
|
HttpStreamBindingSpec,
|
|
212
211
|
SseBindingSpec,
|
|
213
212
|
WebSocketProtocolBindingSpec,
|
|
@@ -281,7 +280,9 @@ def _compile_plan(self: Any, app: Any) -> KernelPlan:
|
|
|
281
280
|
meta_index
|
|
282
281
|
)
|
|
283
282
|
|
|
284
|
-
elif isinstance(
|
|
283
|
+
elif isinstance(
|
|
284
|
+
binding, (HttpJsonRpcBindingSpec, HttpJsonRpcProtocolBindingSpec)
|
|
285
|
+
):
|
|
285
286
|
rpc_path = str(
|
|
286
287
|
getattr(
|
|
287
288
|
binding,
|
|
@@ -302,8 +303,12 @@ def _compile_plan(self: Any, app: Any) -> KernelPlan:
|
|
|
302
303
|
proto_bucket = route_data.setdefault(
|
|
303
304
|
binding.proto, {"paths": {}, "endpoints": {}}
|
|
304
305
|
)
|
|
305
|
-
path_bucket = proto_bucket.setdefault("paths", {}).setdefault(
|
|
306
|
-
|
|
306
|
+
path_bucket = proto_bucket.setdefault("paths", {}).setdefault(
|
|
307
|
+
rpc_path, {}
|
|
308
|
+
)
|
|
309
|
+
endpoint_bucket = proto_bucket.setdefault(
|
|
310
|
+
"endpoints", {}
|
|
311
|
+
).setdefault(rpc_path, {})
|
|
307
312
|
proto_bucket[rpc_method] = meta_index
|
|
308
313
|
row = {
|
|
309
314
|
"meta_index": meta_index,
|
|
@@ -326,6 +331,17 @@ def _compile_plan(self: Any, app: Any) -> KernelPlan:
|
|
|
326
331
|
bucket = route_data.setdefault(
|
|
327
332
|
binding.proto, {"exact": {}, "templated": []}
|
|
328
333
|
)
|
|
334
|
+
if (
|
|
335
|
+
isinstance(
|
|
336
|
+
binding, (WsBindingSpec, WebSocketProtocolBindingSpec)
|
|
337
|
+
)
|
|
338
|
+
and framing_kind(getattr(binding, "framing", None)) == "jsonrpc"
|
|
339
|
+
and bool(getattr(sp, "expose_method", True))
|
|
340
|
+
):
|
|
341
|
+
# JSON-RPC method operations are dispatched inside the
|
|
342
|
+
# framework-owned WebSocket session. Only the hidden
|
|
343
|
+
# transport operation may own the socket path itself.
|
|
344
|
+
continue
|
|
329
345
|
selector = binding.path
|
|
330
346
|
route_metadata = _route_metadata_for_binding(binding)
|
|
331
347
|
opkey_to_meta[OpKey(proto=binding.proto, selector=selector)] = (
|
|
@@ -345,13 +361,25 @@ def _compile_plan(self: Any, app: Any) -> KernelPlan:
|
|
|
345
361
|
}
|
|
346
362
|
)
|
|
347
363
|
else:
|
|
348
|
-
|
|
349
|
-
bucket.setdefault("exact_metadata", {})[selector] = {
|
|
364
|
+
route_row = {
|
|
350
365
|
"path": binding.path,
|
|
351
366
|
"meta_index": meta_index,
|
|
352
367
|
"selector": selector,
|
|
353
368
|
**route_metadata,
|
|
354
369
|
}
|
|
370
|
+
if binding.proto == "webtransport":
|
|
371
|
+
bucket["exact"].setdefault(selector, meta_index)
|
|
372
|
+
bucket.setdefault("routes", {}).setdefault(
|
|
373
|
+
selector, []
|
|
374
|
+
).append(route_row)
|
|
375
|
+
bucket.setdefault("exact_metadata", {}).setdefault(
|
|
376
|
+
selector, route_row
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
bucket["exact"][selector] = meta_index
|
|
380
|
+
bucket.setdefault("exact_metadata", {})[selector] = (
|
|
381
|
+
route_row
|
|
382
|
+
)
|
|
355
383
|
|
|
356
384
|
semantic = KernelPlan(
|
|
357
385
|
proto_indices=route_data,
|
tigrbl_kernel/models.py
CHANGED
|
@@ -51,6 +51,13 @@ class SchemaOut:
|
|
|
51
51
|
expose: Tuple[str, ...]
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class SchemaStored:
|
|
56
|
+
fields: Tuple[str, ...]
|
|
57
|
+
by_field: Dict[str, Dict[str, object]]
|
|
58
|
+
required_from_client: Tuple[str, ...] = ()
|
|
59
|
+
|
|
60
|
+
|
|
54
61
|
@dataclass(frozen=True)
|
|
55
62
|
class OpView:
|
|
56
63
|
schema_in: SchemaIn
|
|
@@ -59,6 +66,7 @@ class OpView:
|
|
|
59
66
|
virtual_producers: Dict[str, Callable[[object, dict], object]]
|
|
60
67
|
to_stored_transforms: Dict[str, Callable[[object, dict], object]]
|
|
61
68
|
refresh_hints: Tuple[str, ...]
|
|
69
|
+
schema_stored: SchemaStored | None = None
|
|
62
70
|
|
|
63
71
|
|
|
64
72
|
@dataclass(frozen=True, slots=True)
|
tigrbl_kernel/opview_compiler.py
CHANGED
|
@@ -2,7 +2,41 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Dict, Mapping
|
|
4
4
|
|
|
5
|
-
from .
|
|
5
|
+
from tigrbl_core.schema.builder.helpers import (
|
|
6
|
+
_normalize_py_type,
|
|
7
|
+
_storage_python_type,
|
|
8
|
+
_spec_python_type,
|
|
9
|
+
_input_selector_entries,
|
|
10
|
+
_input_filter_entries,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .models import OpView, SchemaIn, SchemaOut, SchemaStored
|
|
14
|
+
|
|
15
|
+
_READ_ONLY_TARGETS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"read",
|
|
18
|
+
"list",
|
|
19
|
+
"count",
|
|
20
|
+
"exists",
|
|
21
|
+
"aggregate",
|
|
22
|
+
"group_by",
|
|
23
|
+
"subscribe",
|
|
24
|
+
"tail",
|
|
25
|
+
"download",
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
_NON_PERSISTING_TARGETS = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"delete",
|
|
31
|
+
"clear",
|
|
32
|
+
"bulk_delete",
|
|
33
|
+
"close_stream",
|
|
34
|
+
"close_session",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
_CREATE_LIKE_TARGETS = frozenset({"create", "bulk_create"})
|
|
38
|
+
_UPDATE_LIKE_TARGETS = frozenset({"update", "merge", "bulk_update", "bulk_merge"})
|
|
39
|
+
_REPLACE_LIKE_TARGETS = frozenset({"replace", "bulk_replace"})
|
|
6
40
|
|
|
7
41
|
|
|
8
42
|
def _storage_requires_input(storage: Any, alias: str) -> bool:
|
|
@@ -22,22 +56,87 @@ def _storage_requires_input(storage: Any, alias: str) -> bool:
|
|
|
22
56
|
return not bool(getattr(storage, "nullable", True)) and not has_default
|
|
23
57
|
|
|
24
58
|
|
|
59
|
+
def _storage_has_server_default(spec: Any, storage: Any) -> bool:
|
|
60
|
+
return bool(
|
|
61
|
+
getattr(storage, "default", None) is not None
|
|
62
|
+
or getattr(storage, "server_default", None) is not None
|
|
63
|
+
or callable(getattr(storage, "default_factory", None))
|
|
64
|
+
or callable(getattr(spec, "default_factory", None))
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _supports_stored_shape(target: str) -> bool:
|
|
69
|
+
lowered = str(target or "").lower()
|
|
70
|
+
return lowered not in _READ_ONLY_TARGETS and lowered not in _NON_PERSISTING_TARGETS
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_input_enabled(io: Any, semantic_verbs: tuple[str, ...]) -> bool:
|
|
74
|
+
in_verbs = set(getattr(io, "in_verbs", ()) or ())
|
|
75
|
+
return any(name in in_verbs for name in semantic_verbs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_paired_enabled(io: Any, semantic_verbs: tuple[str, ...]) -> bool:
|
|
79
|
+
cfg = getattr(io, "_paired", None)
|
|
80
|
+
if cfg is None:
|
|
81
|
+
return False
|
|
82
|
+
paired_verbs = set(getattr(cfg, "verbs", ()) or ())
|
|
83
|
+
return any(name in paired_verbs for name in semantic_verbs)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _include_in_stored_schema(
|
|
87
|
+
*,
|
|
88
|
+
spec: Any,
|
|
89
|
+
io: Any,
|
|
90
|
+
storage: Any,
|
|
91
|
+
semantic_verbs: tuple[str, ...],
|
|
92
|
+
target: str,
|
|
93
|
+
) -> bool:
|
|
94
|
+
lowered = str(target or "").lower()
|
|
95
|
+
if storage is None or not _supports_stored_shape(lowered):
|
|
96
|
+
return False
|
|
97
|
+
if _is_input_enabled(io, semantic_verbs) or _is_paired_enabled(io, semantic_verbs):
|
|
98
|
+
return True
|
|
99
|
+
if lowered in _CREATE_LIKE_TARGETS or lowered in _UPDATE_LIKE_TARGETS:
|
|
100
|
+
return True
|
|
101
|
+
if lowered in _REPLACE_LIKE_TARGETS:
|
|
102
|
+
return True
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _stored_required(storage: Any, spec: Any, target: str, *, derived: bool) -> bool:
|
|
107
|
+
lowered = str(target or "").lower()
|
|
108
|
+
if lowered in _UPDATE_LIKE_TARGETS:
|
|
109
|
+
return False
|
|
110
|
+
if lowered in _NON_PERSISTING_TARGETS or lowered in _READ_ONLY_TARGETS:
|
|
111
|
+
return False
|
|
112
|
+
if derived:
|
|
113
|
+
return True
|
|
114
|
+
return not bool(
|
|
115
|
+
getattr(storage, "nullable", True)
|
|
116
|
+
) and not _storage_has_server_default(
|
|
117
|
+
spec,
|
|
118
|
+
storage,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
25
122
|
def compile_opview_from_specs(specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
26
123
|
"""Build a basic OpView from collected specs when no app/model is present."""
|
|
27
124
|
alias = str(getattr(sp, "alias", "") or "")
|
|
28
125
|
target = str(getattr(sp, "target", alias) or alias)
|
|
29
126
|
semantic_verbs = tuple(
|
|
30
127
|
dict.fromkeys(
|
|
31
|
-
name
|
|
32
|
-
for name in (alias, target)
|
|
33
|
-
if isinstance(name, str) and name
|
|
128
|
+
name for name in (alias, target) if isinstance(name, str) and name
|
|
34
129
|
)
|
|
35
130
|
)
|
|
36
131
|
|
|
37
132
|
in_fields: list[str] = []
|
|
38
133
|
out_fields: list[str] = []
|
|
134
|
+
stored_fields: list[str] = []
|
|
39
135
|
by_field_in: Dict[str, Dict[str, object]] = {}
|
|
40
136
|
by_field_out: Dict[str, Dict[str, object]] = {}
|
|
137
|
+
by_field_stored: Dict[str, Dict[str, object]] = {}
|
|
138
|
+
required_from_client: list[str] = []
|
|
139
|
+
to_stored_transforms: Dict[str, Any] = {}
|
|
41
140
|
|
|
42
141
|
for name, spec in specs.items():
|
|
43
142
|
io = getattr(spec, "io", None)
|
|
@@ -51,7 +150,7 @@ def compile_opview_from_specs(specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
|
51
150
|
meta: Dict[str, object] = {"in_enabled": True}
|
|
52
151
|
if storage is None:
|
|
53
152
|
meta["virtual"] = True
|
|
54
|
-
py_type =
|
|
153
|
+
py_type = _spec_python_type(spec)
|
|
55
154
|
if py_type is not None and py_type is not Any:
|
|
56
155
|
meta["py_type"] = py_type
|
|
57
156
|
constraints = getattr(fs, "constraints", {}) or {}
|
|
@@ -72,7 +171,13 @@ def compile_opview_from_specs(specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
|
72
171
|
getattr(io, "header_required_in", False)
|
|
73
172
|
)
|
|
74
173
|
required = bool(
|
|
75
|
-
(
|
|
174
|
+
(
|
|
175
|
+
fs
|
|
176
|
+
and any(
|
|
177
|
+
name in getattr(fs, "required_in", ())
|
|
178
|
+
for name in semantic_verbs
|
|
179
|
+
)
|
|
180
|
+
)
|
|
76
181
|
or _storage_requires_input(storage, target)
|
|
77
182
|
)
|
|
78
183
|
meta["required"] = required
|
|
@@ -96,6 +201,64 @@ def compile_opview_from_specs(specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
|
96
201
|
meta_out["py_type"] = py_type
|
|
97
202
|
by_field_out[name] = meta_out
|
|
98
203
|
|
|
204
|
+
if _include_in_stored_schema(
|
|
205
|
+
spec=spec,
|
|
206
|
+
io=io,
|
|
207
|
+
storage=storage,
|
|
208
|
+
semantic_verbs=semantic_verbs,
|
|
209
|
+
target=target,
|
|
210
|
+
):
|
|
211
|
+
stored_fields.append(name)
|
|
212
|
+
input_enabled = _is_input_enabled(io, semantic_verbs)
|
|
213
|
+
paired_enabled = _is_paired_enabled(io, semantic_verbs)
|
|
214
|
+
server_default = _storage_has_server_default(spec, storage)
|
|
215
|
+
py_type = getattr(fs, "py_type", None)
|
|
216
|
+
if py_type is None or py_type is Any:
|
|
217
|
+
py_type = _storage_python_type(getattr(storage, "type_", None))
|
|
218
|
+
else:
|
|
219
|
+
py_type = _normalize_py_type(py_type)
|
|
220
|
+
stored_meta: Dict[str, object] = {
|
|
221
|
+
"nullable": bool(getattr(storage, "nullable", True)),
|
|
222
|
+
"from_client": input_enabled,
|
|
223
|
+
"required_from_client": bool(
|
|
224
|
+
input_enabled and _storage_requires_input(storage, target)
|
|
225
|
+
),
|
|
226
|
+
"server_default": server_default,
|
|
227
|
+
}
|
|
228
|
+
if py_type is not Any:
|
|
229
|
+
stored_meta["py_type"] = py_type
|
|
230
|
+
constraints = getattr(fs, "constraints", {}) or {}
|
|
231
|
+
if isinstance(constraints, Mapping):
|
|
232
|
+
max_length = constraints.get("max_length")
|
|
233
|
+
if isinstance(max_length, int) and max_length > 0:
|
|
234
|
+
stored_meta["max_length"] = max_length
|
|
235
|
+
|
|
236
|
+
transform = getattr(getattr(storage, "transform", None), "to_stored", None)
|
|
237
|
+
if callable(transform):
|
|
238
|
+
to_stored_transforms[name] = transform
|
|
239
|
+
|
|
240
|
+
derived = paired_enabled or callable(transform)
|
|
241
|
+
stored_meta["derived"] = derived
|
|
242
|
+
stored_meta["required"] = _stored_required(
|
|
243
|
+
storage,
|
|
244
|
+
spec,
|
|
245
|
+
target,
|
|
246
|
+
derived=derived,
|
|
247
|
+
)
|
|
248
|
+
if stored_meta["required_from_client"]:
|
|
249
|
+
required_from_client.append(name)
|
|
250
|
+
by_field_stored[name] = stored_meta
|
|
251
|
+
|
|
252
|
+
for name, meta in {
|
|
253
|
+
**_input_filter_entries(specs, target),
|
|
254
|
+
**_input_selector_entries(specs, target),
|
|
255
|
+
}.items():
|
|
256
|
+
if name not in by_field_in:
|
|
257
|
+
in_fields.append(name)
|
|
258
|
+
by_field_in[name] = meta
|
|
259
|
+
elif meta.get("selector"):
|
|
260
|
+
by_field_in[name] = {**meta, **by_field_in[name], "selector": True}
|
|
261
|
+
|
|
99
262
|
schema_in = SchemaIn(
|
|
100
263
|
fields=tuple(sorted(in_fields)),
|
|
101
264
|
by_field={field: by_field_in.get(field, {}) for field in sorted(in_fields)},
|
|
@@ -105,6 +268,18 @@ def compile_opview_from_specs(specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
|
105
268
|
by_field={field: by_field_out.get(field, {}) for field in sorted(out_fields)},
|
|
106
269
|
expose=tuple(sorted(out_fields)),
|
|
107
270
|
)
|
|
271
|
+
stored_fields_sorted = tuple(sorted(stored_fields))
|
|
272
|
+
schema_stored = (
|
|
273
|
+
SchemaStored(
|
|
274
|
+
fields=stored_fields_sorted,
|
|
275
|
+
by_field={
|
|
276
|
+
field: by_field_stored.get(field, {}) for field in stored_fields_sorted
|
|
277
|
+
},
|
|
278
|
+
required_from_client=tuple(sorted(required_from_client)),
|
|
279
|
+
)
|
|
280
|
+
if stored_fields_sorted
|
|
281
|
+
else None
|
|
282
|
+
)
|
|
108
283
|
paired_index: Dict[str, Dict[str, object]] = {}
|
|
109
284
|
for field, col in specs.items():
|
|
110
285
|
io = getattr(col, "io", None)
|
|
@@ -131,6 +306,7 @@ def compile_opview_from_specs(specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
|
131
306
|
schema_out=schema_out,
|
|
132
307
|
paired_index=paired_index,
|
|
133
308
|
virtual_producers={},
|
|
134
|
-
to_stored_transforms=
|
|
309
|
+
to_stored_transforms=to_stored_transforms,
|
|
135
310
|
refresh_hints=(),
|
|
311
|
+
schema_stored=schema_stored,
|
|
136
312
|
)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping, MutableMapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
4
7
|
|
|
5
8
|
from .models import KernelPlan, PackedKernel
|
|
6
9
|
from .packed_access import (
|
|
@@ -15,7 +18,9 @@ ExactRouteVerifyCache = MutableMapping[
|
|
|
15
18
|
int, Mapping[int, Mapping[int, tuple[tuple[str, int], ...]]]
|
|
16
19
|
]
|
|
17
20
|
ExactWebSocketCache = MutableMapping[int, Mapping[tuple[str, str], int]]
|
|
18
|
-
ExactJsonRpcCache = MutableMapping[
|
|
21
|
+
ExactJsonRpcCache = MutableMapping[
|
|
22
|
+
int, Mapping[str, Mapping[str, tuple[int, str, str]]]
|
|
23
|
+
]
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
def resolve_hot_exact_route_slices(
|
|
@@ -32,9 +37,7 @@ def resolve_hot_exact_route_slices(
|
|
|
32
37
|
if method_ids is None or path_hashes is None or program_ids is None:
|
|
33
38
|
cache[packed_id] = {}
|
|
34
39
|
return {}
|
|
35
|
-
if not (
|
|
36
|
-
int(method_ids.count) == int(path_hashes.count) == int(program_ids.count)
|
|
37
|
-
):
|
|
40
|
+
if not (int(method_ids.count) == int(path_hashes.count) == int(program_ids.count)):
|
|
38
41
|
cache[packed_id] = {}
|
|
39
42
|
return {}
|
|
40
43
|
directory: dict[int, tuple[int, int]] = {}
|
|
@@ -229,11 +232,148 @@ def resolve_hot_exact_jsonrpc_routes(
|
|
|
229
232
|
selector = str(entry.get("selector") or f"{endpoint}:{rpc_method}")
|
|
230
233
|
method_map[rpc_method] = (meta_index, str(proto), selector)
|
|
231
234
|
|
|
232
|
-
frozen = {
|
|
235
|
+
frozen = {
|
|
236
|
+
endpoint: dict(method_map) for endpoint, method_map in exact_routes.items()
|
|
237
|
+
}
|
|
233
238
|
cache[plan_id] = frozen
|
|
234
239
|
return frozen
|
|
235
240
|
|
|
236
241
|
|
|
242
|
+
@dataclass(frozen=True, slots=True)
|
|
243
|
+
class WebTransportProgramSelection:
|
|
244
|
+
program_id: int = -1
|
|
245
|
+
path_params: Mapping[str, str] | None = None
|
|
246
|
+
lane: str | None = None
|
|
247
|
+
rpc_envelope: Mapping[str, Any] | None = None
|
|
248
|
+
disposition: str = "no_match"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _webtransport_route_rows(
|
|
252
|
+
plan: KernelPlan,
|
|
253
|
+
path: str,
|
|
254
|
+
) -> tuple[tuple[Mapping[str, Any], Mapping[str, str]], ...]:
|
|
255
|
+
bucket = plan.proto_indices.get("webtransport")
|
|
256
|
+
if not isinstance(bucket, Mapping):
|
|
257
|
+
return ()
|
|
258
|
+
|
|
259
|
+
rows: list[tuple[Mapping[str, Any], Mapping[str, str]]] = []
|
|
260
|
+
exact_routes = bucket.get("routes")
|
|
261
|
+
if isinstance(exact_routes, Mapping):
|
|
262
|
+
exact = exact_routes.get(path)
|
|
263
|
+
if isinstance(exact, list):
|
|
264
|
+
rows.extend((row, {}) for row in exact if isinstance(row, Mapping))
|
|
265
|
+
|
|
266
|
+
templated = bucket.get("templated")
|
|
267
|
+
if isinstance(templated, list):
|
|
268
|
+
for row in templated:
|
|
269
|
+
if not isinstance(row, Mapping):
|
|
270
|
+
continue
|
|
271
|
+
pattern = row.get("pattern")
|
|
272
|
+
if pattern is None or not hasattr(pattern, "match"):
|
|
273
|
+
continue
|
|
274
|
+
matched = pattern.match(path)
|
|
275
|
+
if matched is not None:
|
|
276
|
+
rows.append((row, matched.groupdict()))
|
|
277
|
+
return tuple(rows)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _webtransport_rpc_envelope(message: Mapping[str, Any]) -> Mapping[str, Any] | None:
|
|
281
|
+
payload = message.get("data")
|
|
282
|
+
if payload is None:
|
|
283
|
+
payload = message.get("bytes")
|
|
284
|
+
if isinstance(payload, (bytes, bytearray, memoryview)):
|
|
285
|
+
try:
|
|
286
|
+
payload = json.loads(bytes(payload).decode("utf-8"))
|
|
287
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
288
|
+
return None
|
|
289
|
+
elif isinstance(payload, str):
|
|
290
|
+
try:
|
|
291
|
+
payload = json.loads(payload)
|
|
292
|
+
except json.JSONDecodeError:
|
|
293
|
+
return None
|
|
294
|
+
return payload if isinstance(payload, Mapping) else None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def resolve_webtransport_program(
|
|
298
|
+
plan: KernelPlan,
|
|
299
|
+
*,
|
|
300
|
+
path: str,
|
|
301
|
+
message: Mapping[str, Any],
|
|
302
|
+
) -> WebTransportProgramSelection:
|
|
303
|
+
event_type = str(message.get("type") or "")
|
|
304
|
+
rows = _webtransport_route_rows(plan, path)
|
|
305
|
+
if event_type in {
|
|
306
|
+
"webtransport.connect",
|
|
307
|
+
"webtransport.disconnect",
|
|
308
|
+
"transport.emit.complete",
|
|
309
|
+
"transport.emit.failed",
|
|
310
|
+
}:
|
|
311
|
+
return WebTransportProgramSelection(disposition="lifecycle")
|
|
312
|
+
|
|
313
|
+
lane: str | None = None
|
|
314
|
+
rpc_envelope: Mapping[str, Any] | None = None
|
|
315
|
+
rpc_method: str | None = None
|
|
316
|
+
if event_type == "webtransport.stream.receive":
|
|
317
|
+
direction = str(message.get("stream_direction") or "bidi")
|
|
318
|
+
lane = (
|
|
319
|
+
"unidi_client_stream" if direction == "client_to_server" else "bidi_stream"
|
|
320
|
+
)
|
|
321
|
+
if lane == "bidi_stream":
|
|
322
|
+
rpc_envelope = _webtransport_rpc_envelope(message)
|
|
323
|
+
method = rpc_envelope.get("method") if rpc_envelope is not None else None
|
|
324
|
+
rpc_method = method if isinstance(method, str) and method else None
|
|
325
|
+
elif event_type == "webtransport.datagram.receive":
|
|
326
|
+
lane = "datagram"
|
|
327
|
+
else:
|
|
328
|
+
return WebTransportProgramSelection(disposition="unsupported_event")
|
|
329
|
+
|
|
330
|
+
candidates = [
|
|
331
|
+
(row, params)
|
|
332
|
+
for row, params in rows
|
|
333
|
+
if str(row.get("lane") or row.get("profile") or "") == lane
|
|
334
|
+
]
|
|
335
|
+
if lane == "bidi_stream":
|
|
336
|
+
if rpc_method is None:
|
|
337
|
+
return WebTransportProgramSelection(
|
|
338
|
+
lane=lane,
|
|
339
|
+
rpc_envelope=rpc_envelope,
|
|
340
|
+
disposition="invalid_jsonrpc",
|
|
341
|
+
)
|
|
342
|
+
candidates = [
|
|
343
|
+
(row, params)
|
|
344
|
+
for row, params in candidates
|
|
345
|
+
if row.get("rpc_method") == rpc_method
|
|
346
|
+
]
|
|
347
|
+
if not candidates:
|
|
348
|
+
return WebTransportProgramSelection(
|
|
349
|
+
lane=lane,
|
|
350
|
+
rpc_envelope=rpc_envelope,
|
|
351
|
+
disposition="method_not_found",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if len(candidates) != 1:
|
|
355
|
+
return WebTransportProgramSelection(
|
|
356
|
+
lane=lane,
|
|
357
|
+
rpc_envelope=rpc_envelope,
|
|
358
|
+
disposition="ambiguous" if candidates else "no_match",
|
|
359
|
+
)
|
|
360
|
+
row, path_params = candidates[0]
|
|
361
|
+
program_id = row.get("meta_index")
|
|
362
|
+
if not isinstance(program_id, int):
|
|
363
|
+
return WebTransportProgramSelection(
|
|
364
|
+
lane=lane,
|
|
365
|
+
rpc_envelope=rpc_envelope,
|
|
366
|
+
disposition="invalid_plan",
|
|
367
|
+
)
|
|
368
|
+
return WebTransportProgramSelection(
|
|
369
|
+
program_id=program_id,
|
|
370
|
+
path_params=dict(path_params),
|
|
371
|
+
lane=lane,
|
|
372
|
+
rpc_envelope=rpc_envelope,
|
|
373
|
+
disposition="dispatch",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
237
377
|
def normalize_jsonrpc_mount_path(path: str) -> str:
|
|
238
378
|
normalized = str(path or "").rstrip("/")
|
|
239
379
|
return normalized or "/"
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Mapping
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from tigrbl_atoms.atoms.framing import websocket_jsonrpc as _jsonrpc_framing
|
|
7
|
+
from tigrbl_atoms.atoms.transport import websocket as _websocket_transport
|
|
8
|
+
from tigrbl_typing.status.converters import http_exc_to_rpc
|
|
9
|
+
from tigrbl_typing.status.exceptions import HTTPException
|
|
10
|
+
|
|
6
11
|
ANCHORS: tuple[str, ...] = (
|
|
7
12
|
"transport.accept",
|
|
8
13
|
"transport.receive",
|
|
@@ -13,22 +18,243 @@ ANCHORS: tuple[str, ...] = (
|
|
|
13
18
|
"transport.close",
|
|
14
19
|
)
|
|
15
20
|
|
|
21
|
+
SESSION_LIFECYCLE_PHASES: tuple[str, ...] = (
|
|
22
|
+
"SESSION_OPEN",
|
|
23
|
+
"POST_SESSION_OPEN",
|
|
24
|
+
"MESSAGE_RECEIVE",
|
|
25
|
+
"INGRESS_PARSE",
|
|
26
|
+
"INGRESS_DISPATCH",
|
|
27
|
+
"PRE_TX_BEGIN",
|
|
28
|
+
"START_TX",
|
|
29
|
+
"PRE_HANDLER",
|
|
30
|
+
"HANDLER",
|
|
31
|
+
"POST_HANDLER",
|
|
32
|
+
"PRE_COMMIT",
|
|
33
|
+
"TX_COMMIT",
|
|
34
|
+
"POST_COMMIT",
|
|
35
|
+
"EGRESS_SHAPE",
|
|
36
|
+
"EGRESS_FINALIZE",
|
|
37
|
+
"POST_RESPONSE",
|
|
38
|
+
"PRE_SESSION_CLOSE",
|
|
39
|
+
"SESSION_CLOSE",
|
|
40
|
+
"POST_SESSION_CLOSE",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
SESSION_STRUCTURAL_PHASES: tuple[str, ...] = (
|
|
44
|
+
"SESSION_OPEN",
|
|
45
|
+
"MESSAGE_RECEIVE",
|
|
46
|
+
"SESSION_CLOSE",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
SESSION_HOOK_PHASES: tuple[str, ...] = (
|
|
50
|
+
"POST_SESSION_OPEN",
|
|
51
|
+
"PRE_SESSION_CLOSE",
|
|
52
|
+
"POST_SESSION_CLOSE",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
CORE_REALTIME_ATOMS: tuple[str, ...] = (
|
|
56
|
+
"transport.accept",
|
|
57
|
+
"transport.receive",
|
|
58
|
+
"transport.emit",
|
|
59
|
+
"transport.close",
|
|
60
|
+
"framing.decode",
|
|
61
|
+
"framing.encode",
|
|
62
|
+
"dispatch.binding.match",
|
|
63
|
+
"dispatch.binding.parse",
|
|
64
|
+
"subscription.register",
|
|
65
|
+
"subscription.unregister",
|
|
66
|
+
"publish.prepare",
|
|
67
|
+
"publish.enqueue",
|
|
68
|
+
"publish.fanout",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
ATOM_PHASE_PLACEMENT: dict[str, str] = {
|
|
72
|
+
"transport.accept": "SESSION_OPEN",
|
|
73
|
+
"transport.receive": "MESSAGE_RECEIVE",
|
|
74
|
+
"dispatch.binding.match": "INGRESS_DISPATCH",
|
|
75
|
+
"dispatch.binding.parse": "INGRESS_DISPATCH",
|
|
76
|
+
"framing.decode": "INGRESS_PARSE",
|
|
77
|
+
"subscription.register": "POST_COMMIT",
|
|
78
|
+
"subscription.unregister": "PRE_SESSION_CLOSE",
|
|
79
|
+
"publish.prepare": "POST_COMMIT",
|
|
80
|
+
"publish.enqueue": "POST_COMMIT",
|
|
81
|
+
"publish.fanout": "EGRESS_FINALIZE",
|
|
82
|
+
"framing.encode": "EGRESS_FINALIZE",
|
|
83
|
+
"transport.emit": "EGRESS_FINALIZE",
|
|
84
|
+
"transport.close": "SESSION_CLOSE",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
PUBLISH_POLICY: dict[str, object] = {
|
|
88
|
+
"automatic_for_oltp": False,
|
|
89
|
+
"publish_only_op": True,
|
|
90
|
+
"create_only_publishes": False,
|
|
91
|
+
"create_and_publish_requires_effect": True,
|
|
92
|
+
"rollback_suppresses_publish": True,
|
|
93
|
+
"placement": {
|
|
94
|
+
"HANDLER": ("publish.intent",),
|
|
95
|
+
"POST_COMMIT": ("publish.prepare", "publish.enqueue"),
|
|
96
|
+
"EGRESS_FINALIZE": ("publish.fanout", "framing.encode", "transport.emit"),
|
|
97
|
+
"POST_RESPONSE": ("cleanup.metrics",),
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
SUBSCRIBE_POLICY: dict[str, object] = {
|
|
102
|
+
"operation_shape": "finite_request_response",
|
|
103
|
+
"ack_framing": "jsonrpc",
|
|
104
|
+
"owns_receive_loop": False,
|
|
105
|
+
"registration": {"atom": "subscription.register", "phase": "POST_COMMIT"},
|
|
106
|
+
"cleanup": {"atom": "subscription.unregister", "phase": "PRE_SESSION_CLOSE"},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _binding_framing(binding: Mapping[str, Any]) -> str:
|
|
111
|
+
framing = (
|
|
112
|
+
binding.get("framing", "text")
|
|
113
|
+
if isinstance(binding, Mapping)
|
|
114
|
+
else getattr(binding, "framing", "text")
|
|
115
|
+
)
|
|
116
|
+
if isinstance(framing, str):
|
|
117
|
+
return framing.lower()
|
|
118
|
+
name = type(framing).__name__.lower()
|
|
119
|
+
if "jsonrpc" in name:
|
|
120
|
+
return "jsonrpc"
|
|
121
|
+
if "ndjson" in name:
|
|
122
|
+
return "ndjson"
|
|
123
|
+
if "text" in name:
|
|
124
|
+
return "text"
|
|
125
|
+
return name
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _binding_path(binding: Mapping[str, Any]) -> str | None:
|
|
129
|
+
if isinstance(binding, Mapping):
|
|
130
|
+
path = binding.get("path") or binding.get("connect_path")
|
|
131
|
+
else:
|
|
132
|
+
path = getattr(binding, "path", None) or getattr(
|
|
133
|
+
binding,
|
|
134
|
+
"connect_path",
|
|
135
|
+
None,
|
|
136
|
+
)
|
|
137
|
+
return str(path) if path is not None else None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _op_bindings(op: Any) -> tuple[Any, ...]:
|
|
141
|
+
bindings = (
|
|
142
|
+
op.get("bindings", ())
|
|
143
|
+
if isinstance(op, Mapping)
|
|
144
|
+
else getattr(op, "bindings", ())
|
|
145
|
+
)
|
|
146
|
+
return tuple(bindings or ())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _op_alias(op: Any) -> str:
|
|
150
|
+
alias = op.get("alias") if isinstance(op, Mapping) else getattr(op, "alias", None)
|
|
151
|
+
if not isinstance(alias, str) or not alias:
|
|
152
|
+
raise ValueError("websocket jsonrpc op requires alias")
|
|
153
|
+
return alias
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _op_method(op: Any) -> str:
|
|
157
|
+
if isinstance(op, Mapping):
|
|
158
|
+
method = (
|
|
159
|
+
op.get("op_method")
|
|
160
|
+
or op.get("op_id")
|
|
161
|
+
or op.get("identity")
|
|
162
|
+
or op.get("canonical_identity")
|
|
163
|
+
or op.get("name")
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
method = (
|
|
167
|
+
getattr(op, "op_method", None)
|
|
168
|
+
or getattr(op, "op_id", None)
|
|
169
|
+
or getattr(op, "identity", None)
|
|
170
|
+
or getattr(op, "canonical_identity", None)
|
|
171
|
+
or getattr(op, "name", None)
|
|
172
|
+
)
|
|
173
|
+
if method is None:
|
|
174
|
+
table = getattr(op, "table", None)
|
|
175
|
+
table_name = getattr(table, "__name__", None)
|
|
176
|
+
alias = getattr(op, "alias", None)
|
|
177
|
+
if table_name and alias:
|
|
178
|
+
method = f"{table_name}.{alias}"
|
|
179
|
+
elif alias:
|
|
180
|
+
method = str(alias)
|
|
181
|
+
if not isinstance(method, str) or not method:
|
|
182
|
+
raise ValueError("websocket jsonrpc op requires canonical op identity")
|
|
183
|
+
return method
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _table_identity(table: Any) -> str:
|
|
187
|
+
name = getattr(table, "name", None)
|
|
188
|
+
if isinstance(name, str) and name:
|
|
189
|
+
return name
|
|
190
|
+
model = getattr(table, "model", None)
|
|
191
|
+
model_name = getattr(model, "__name__", None)
|
|
192
|
+
if isinstance(model_name, str) and model_name:
|
|
193
|
+
return model_name
|
|
194
|
+
model_ref = getattr(table, "model_ref", None)
|
|
195
|
+
if isinstance(model_ref, str) and model_ref:
|
|
196
|
+
return model_ref.rsplit(":", 1)[-1].rsplit(".", 1)[-1]
|
|
197
|
+
resource = getattr(table, "resource", None)
|
|
198
|
+
if isinstance(resource, str) and resource:
|
|
199
|
+
return resource
|
|
200
|
+
raise ValueError("websocket jsonrpc table requires name, model, or model_ref")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _binding_proto(binding: Any) -> str:
|
|
204
|
+
if isinstance(binding, Mapping):
|
|
205
|
+
proto = binding.get("proto") or binding.get("protocol") or "ws"
|
|
206
|
+
else:
|
|
207
|
+
proto = getattr(binding, "proto", None) or getattr(binding, "protocol", None)
|
|
208
|
+
return str(proto or "ws").lower()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _register_jsonrpc_op(
|
|
212
|
+
endpoint: dict[str, Any],
|
|
213
|
+
*,
|
|
214
|
+
path: str,
|
|
215
|
+
method: str,
|
|
216
|
+
op: Any,
|
|
217
|
+
) -> None:
|
|
218
|
+
for binding in _op_bindings(op):
|
|
219
|
+
if _binding_path(binding) != path:
|
|
220
|
+
continue
|
|
221
|
+
if _binding_proto(binding) not in {"ws", "wss", "websocket"}:
|
|
222
|
+
continue
|
|
223
|
+
if _binding_framing(binding) != "jsonrpc":
|
|
224
|
+
continue
|
|
225
|
+
if method in endpoint:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"duplicate websocket jsonrpc registration for {path} {method}"
|
|
228
|
+
)
|
|
229
|
+
endpoint[method] = op
|
|
230
|
+
|
|
16
231
|
|
|
17
232
|
def compile_websocket_chain(binding: Mapping[str, Any]) -> dict[str, object]:
|
|
18
233
|
scheme = binding.get("scheme", "ws")
|
|
19
234
|
if scheme not in {"ws", "wss"}:
|
|
20
235
|
raise ValueError("websocket scheme must be ws or wss")
|
|
236
|
+
framing = _binding_framing(binding)
|
|
21
237
|
tls = dict((binding.get("extensions") or {}).get("tls") or {})
|
|
22
238
|
return {
|
|
23
239
|
"binding": "wss" if scheme == "wss" else "websocket",
|
|
24
240
|
"exchange": "bidirectional_stream",
|
|
25
241
|
"family": "message",
|
|
26
242
|
"path": binding.get("path"),
|
|
243
|
+
"framing": framing,
|
|
27
244
|
"anchors": ANCHORS,
|
|
245
|
+
"lifecycle_phases": SESSION_LIFECYCLE_PHASES,
|
|
246
|
+
"structural_phases": SESSION_STRUCTURAL_PHASES,
|
|
247
|
+
"hook_phases": SESSION_HOOK_PHASES,
|
|
248
|
+
"realtime_atoms": CORE_REALTIME_ATOMS,
|
|
249
|
+
"atom_phase_placement": ATOM_PHASE_PLACEMENT,
|
|
250
|
+
"subscribe_policy": SUBSCRIBE_POLICY,
|
|
251
|
+
"publish_policy": PUBLISH_POLICY,
|
|
28
252
|
"loop_region": {
|
|
29
253
|
"loop_id": "websocket.receive",
|
|
30
254
|
"role": "message",
|
|
31
255
|
"break_conditions": ("websocket.disconnect",),
|
|
256
|
+
"enter_phase": "MESSAGE_RECEIVE",
|
|
257
|
+
"exit_target": "PRE_SESSION_CLOSE",
|
|
32
258
|
},
|
|
33
259
|
"close": {"code": 1000, "reason": ""},
|
|
34
260
|
"scope_metadata": {
|
|
@@ -38,4 +264,270 @@ def compile_websocket_chain(binding: Mapping[str, Any]) -> dict[str, object]:
|
|
|
38
264
|
}
|
|
39
265
|
|
|
40
266
|
|
|
41
|
-
|
|
267
|
+
def compile_websocket_jsonrpc_dispatch_index(
|
|
268
|
+
*,
|
|
269
|
+
path: str,
|
|
270
|
+
ops: Iterable[Any] = (),
|
|
271
|
+
tables: Iterable[Any] = (),
|
|
272
|
+
) -> dict[str, object]:
|
|
273
|
+
endpoint: dict[str, Any] = {}
|
|
274
|
+
for table in tables:
|
|
275
|
+
table_name = _table_identity(table)
|
|
276
|
+
for op in tuple(getattr(table, "ops", ()) or ()):
|
|
277
|
+
_register_jsonrpc_op(
|
|
278
|
+
endpoint,
|
|
279
|
+
path=path,
|
|
280
|
+
method=f"{table_name}.{_op_alias(op)}",
|
|
281
|
+
op=op,
|
|
282
|
+
)
|
|
283
|
+
for op in ops:
|
|
284
|
+
_register_jsonrpc_op(
|
|
285
|
+
endpoint,
|
|
286
|
+
path=path,
|
|
287
|
+
method=_op_method(op),
|
|
288
|
+
op=op,
|
|
289
|
+
)
|
|
290
|
+
return {
|
|
291
|
+
"ws": {"exact": {path: "session_program"}},
|
|
292
|
+
"ws.jsonrpc": {"endpoints": {path: endpoint}},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _jsonrpc_error(
|
|
297
|
+
code: int,
|
|
298
|
+
message: str,
|
|
299
|
+
request_id: Any,
|
|
300
|
+
*,
|
|
301
|
+
data: Any | None = None,
|
|
302
|
+
) -> dict[str, Any]:
|
|
303
|
+
error: dict[str, Any] = {"code": code, "message": message}
|
|
304
|
+
if data is not None:
|
|
305
|
+
error["data"] = data
|
|
306
|
+
return {"jsonrpc": "2.0", "error": error, "id": request_id}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _jsonrpc_result(result: Any, request_id: Any) -> dict[str, Any]:
|
|
310
|
+
return {"jsonrpc": "2.0", "result": result, "id": request_id}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _ctx_get(ctx: Any, key: str, default: Any = None) -> Any:
|
|
314
|
+
getter = getattr(ctx, "get", None)
|
|
315
|
+
if callable(getter):
|
|
316
|
+
try:
|
|
317
|
+
return getter(key, default)
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
return getattr(ctx, key, default)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _session_rpc_context(
|
|
324
|
+
payload: Mapping[str, Any],
|
|
325
|
+
request_id: Any,
|
|
326
|
+
session_ctx: Any,
|
|
327
|
+
) -> dict[str, Any]:
|
|
328
|
+
rpc_ctx: dict[str, Any] = {
|
|
329
|
+
"jsonrpc": dict(payload),
|
|
330
|
+
"jsonrpc_request_id": request_id,
|
|
331
|
+
}
|
|
332
|
+
if session_ctx is None:
|
|
333
|
+
return rpc_ctx
|
|
334
|
+
temp = _ctx_get(session_ctx, "temp", {})
|
|
335
|
+
if isinstance(temp, Mapping):
|
|
336
|
+
realtime = temp.get("realtime")
|
|
337
|
+
websocket = temp.get("websocket")
|
|
338
|
+
if isinstance(realtime, Mapping):
|
|
339
|
+
rpc_ctx["realtime"] = realtime
|
|
340
|
+
if isinstance(websocket, Mapping):
|
|
341
|
+
rpc_ctx["websocket"] = websocket
|
|
342
|
+
channel = _ctx_get(session_ctx, "channel")
|
|
343
|
+
if channel is not None:
|
|
344
|
+
rpc_ctx["channel"] = channel
|
|
345
|
+
return rpc_ctx
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
async def _maybe_await(value: Any) -> Any:
|
|
349
|
+
if hasattr(value, "__await__"):
|
|
350
|
+
return await value
|
|
351
|
+
return value
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
async def _cleanup_realtime_session(ctx: Any) -> None:
|
|
355
|
+
temp = _ctx_get(ctx, "temp", {})
|
|
356
|
+
if not isinstance(temp, Mapping):
|
|
357
|
+
return
|
|
358
|
+
realtime = temp.get("realtime")
|
|
359
|
+
if not isinstance(realtime, Mapping):
|
|
360
|
+
return
|
|
361
|
+
broker = realtime.get("broker")
|
|
362
|
+
session_id = realtime.get("session_id")
|
|
363
|
+
unregister = getattr(broker, "unsubscribe_session", None)
|
|
364
|
+
if callable(unregister) and session_id is not None:
|
|
365
|
+
await _maybe_await(unregister(str(session_id)))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _resolve_rpc_target(router: Any, method: str) -> tuple[Any, str] | None:
|
|
369
|
+
table_name, sep, alias = method.partition(".")
|
|
370
|
+
if not sep or not table_name or not alias:
|
|
371
|
+
return None
|
|
372
|
+
tables = getattr(router, "tables", None)
|
|
373
|
+
if isinstance(tables, Mapping):
|
|
374
|
+
model = tables.get(table_name)
|
|
375
|
+
if isinstance(model, type):
|
|
376
|
+
return model, alias
|
|
377
|
+
for candidate in tables.values():
|
|
378
|
+
if (
|
|
379
|
+
isinstance(candidate, type)
|
|
380
|
+
and getattr(candidate, "__name__", None) == table_name
|
|
381
|
+
):
|
|
382
|
+
return candidate, alias
|
|
383
|
+
return table_name, alias
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _op_tx_scope(model: Any, alias: str) -> str:
|
|
387
|
+
ops = getattr(getattr(model, "ops", None), "all", ())
|
|
388
|
+
for spec in tuple(ops or ()):
|
|
389
|
+
if getattr(spec, "alias", None) == alias:
|
|
390
|
+
return str(getattr(spec, "tx_scope", "inherit") or "inherit")
|
|
391
|
+
return "inherit"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
async def _dispatch_jsonrpc_payload(
|
|
395
|
+
router: Any,
|
|
396
|
+
payload: Any,
|
|
397
|
+
*,
|
|
398
|
+
session_ctx: Any = None,
|
|
399
|
+
) -> Any:
|
|
400
|
+
if not isinstance(payload, Mapping) or payload.get("jsonrpc") != "2.0":
|
|
401
|
+
return _jsonrpc_error(-32600, "Invalid Request", None)
|
|
402
|
+
request_id = payload.get("id")
|
|
403
|
+
method = payload.get("method")
|
|
404
|
+
if not isinstance(method, str) or not method:
|
|
405
|
+
return _jsonrpc_error(-32601, "Method not found", request_id)
|
|
406
|
+
target = _resolve_rpc_target(router, method)
|
|
407
|
+
if target is None:
|
|
408
|
+
return _jsonrpc_error(-32601, "Method not found", request_id)
|
|
409
|
+
params = payload.get("params", {})
|
|
410
|
+
if params is None:
|
|
411
|
+
params = {}
|
|
412
|
+
if not isinstance(params, (dict, list, tuple)):
|
|
413
|
+
return _jsonrpc_error(-32602, "Invalid params", request_id)
|
|
414
|
+
model, alias = target
|
|
415
|
+
rpc_call = getattr(router, "rpc_call", None)
|
|
416
|
+
if not callable(rpc_call):
|
|
417
|
+
return _jsonrpc_error(-32603, "Internal error", request_id)
|
|
418
|
+
kwargs: dict[str, Any] = {
|
|
419
|
+
"ctx": _session_rpc_context(payload, request_id, session_ctx)
|
|
420
|
+
}
|
|
421
|
+
if _op_tx_scope(model, alias) == "none":
|
|
422
|
+
kwargs["db"] = {}
|
|
423
|
+
try:
|
|
424
|
+
result = await rpc_call(model, alias, params, **kwargs)
|
|
425
|
+
except AttributeError:
|
|
426
|
+
return _jsonrpc_error(-32601, "Method not found", request_id)
|
|
427
|
+
except HTTPException as exc:
|
|
428
|
+
code, message, data = http_exc_to_rpc(exc)
|
|
429
|
+
return _jsonrpc_error(code, message, request_id, data=data)
|
|
430
|
+
except Exception as exc:
|
|
431
|
+
return _jsonrpc_error(
|
|
432
|
+
-32603,
|
|
433
|
+
"Internal error",
|
|
434
|
+
request_id,
|
|
435
|
+
data={"detail": str(exc)},
|
|
436
|
+
)
|
|
437
|
+
if "id" not in payload:
|
|
438
|
+
return None
|
|
439
|
+
return _jsonrpc_result(result, request_id)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
async def _dispatch_jsonrpc_decoded(
|
|
443
|
+
router: Any,
|
|
444
|
+
payload: Any,
|
|
445
|
+
*,
|
|
446
|
+
session_ctx: Any = None,
|
|
447
|
+
) -> Any:
|
|
448
|
+
if isinstance(payload, list):
|
|
449
|
+
responses = []
|
|
450
|
+
for item in payload:
|
|
451
|
+
response = await _dispatch_jsonrpc_payload(
|
|
452
|
+
router,
|
|
453
|
+
item,
|
|
454
|
+
session_ctx=session_ctx,
|
|
455
|
+
)
|
|
456
|
+
if response is not None:
|
|
457
|
+
responses.append(response)
|
|
458
|
+
return responses or None
|
|
459
|
+
return await _dispatch_jsonrpc_payload(router, payload, session_ctx=session_ctx)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def build_websocket_jsonrpc_session_handler(
|
|
463
|
+
router: Any,
|
|
464
|
+
*,
|
|
465
|
+
subprotocol: str = "jsonrpc",
|
|
466
|
+
realtime_broker: Any = None,
|
|
467
|
+
) -> Any:
|
|
468
|
+
"""Build a kernel-owned WebSocket JSON-RPC session handler step."""
|
|
469
|
+
|
|
470
|
+
async def _websocket_jsonrpc_session(ctx: Any) -> None:
|
|
471
|
+
await _websocket_transport.accept(ctx, subprotocol=subprotocol)
|
|
472
|
+
channel = ctx.get("channel")
|
|
473
|
+
channel_state = getattr(channel, "state", None)
|
|
474
|
+
if isinstance(channel_state, dict):
|
|
475
|
+
channel_state["transport_sent"] = True
|
|
476
|
+
temp = ctx.get("temp", {})
|
|
477
|
+
if isinstance(temp, dict):
|
|
478
|
+
ws_state = temp.setdefault("websocket", {})
|
|
479
|
+
realtime_state = temp.setdefault("realtime", {})
|
|
480
|
+
session_id = ws_state.setdefault("session_id", f"ws:{id(ctx)}")
|
|
481
|
+
realtime_state.setdefault("session_id", session_id)
|
|
482
|
+
realtime_state.setdefault("sink", ctx.get("channel"))
|
|
483
|
+
if realtime_broker is not None:
|
|
484
|
+
realtime_state.setdefault("broker", realtime_broker)
|
|
485
|
+
try:
|
|
486
|
+
while True:
|
|
487
|
+
await _websocket_transport.receive(ctx)
|
|
488
|
+
ws_state = ctx.get("temp", {}).setdefault("websocket", {})
|
|
489
|
+
if isinstance(ws_state, dict) and ws_state.get("disconnected"):
|
|
490
|
+
break
|
|
491
|
+
try:
|
|
492
|
+
decoded = await _jsonrpc_framing.decode(ctx)
|
|
493
|
+
response = await _dispatch_jsonrpc_decoded(
|
|
494
|
+
router,
|
|
495
|
+
decoded,
|
|
496
|
+
session_ctx=ctx,
|
|
497
|
+
)
|
|
498
|
+
except ValueError:
|
|
499
|
+
response = _jsonrpc_error(-32700, "Parse error", None)
|
|
500
|
+
if response is None:
|
|
501
|
+
continue
|
|
502
|
+
ws_state["response"] = response
|
|
503
|
+
await _jsonrpc_framing.encode(ctx)
|
|
504
|
+
await _websocket_transport.emit(ctx)
|
|
505
|
+
finally:
|
|
506
|
+
await _cleanup_realtime_session(ctx)
|
|
507
|
+
await _websocket_transport.close(ctx)
|
|
508
|
+
|
|
509
|
+
setattr(
|
|
510
|
+
_websocket_jsonrpc_session,
|
|
511
|
+
"__tigrbl_label",
|
|
512
|
+
"kernel:websocket:jsonrpc:session",
|
|
513
|
+
)
|
|
514
|
+
setattr(
|
|
515
|
+
_websocket_jsonrpc_session,
|
|
516
|
+
"__tigrbl_atom_name__",
|
|
517
|
+
"websocket.jsonrpc.session",
|
|
518
|
+
)
|
|
519
|
+
return _websocket_jsonrpc_session
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
__all__ = [
|
|
523
|
+
"ATOM_PHASE_PLACEMENT",
|
|
524
|
+
"CORE_REALTIME_ATOMS",
|
|
525
|
+
"PUBLISH_POLICY",
|
|
526
|
+
"SESSION_HOOK_PHASES",
|
|
527
|
+
"SESSION_LIFECYCLE_PHASES",
|
|
528
|
+
"SESSION_STRUCTURAL_PHASES",
|
|
529
|
+
"SUBSCRIBE_POLICY",
|
|
530
|
+
"build_websocket_jsonrpc_session_handler",
|
|
531
|
+
"compile_websocket_chain",
|
|
532
|
+
"compile_websocket_jsonrpc_dispatch_index",
|
|
533
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
import struct
|
|
8
|
+
import json
|
|
9
|
+
import inspect
|
|
10
|
+
|
|
11
|
+
from tigrbl_atoms.atoms.framing import WebTransportRecordDecoder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class WebTransportSessionPlan:
|
|
16
|
+
"""Session-scoped carrier plan for framed control records.
|
|
17
|
+
|
|
18
|
+
The plan owns record decoder state across ASGI receive calls. One inbound
|
|
19
|
+
carrier chunk may yield zero, one, or several operation-dispatch events;
|
|
20
|
+
completing any one operation does not complete the bidirectional stream.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
pending: deque[dict[str, Any]] = field(default_factory=deque)
|
|
24
|
+
control_decoders: dict[str, WebTransportRecordDecoder] = field(
|
|
25
|
+
default_factory=dict
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def has_pending(self) -> bool:
|
|
29
|
+
return bool(self.pending)
|
|
30
|
+
|
|
31
|
+
def pop_pending(self) -> dict[str, Any]:
|
|
32
|
+
return self.pending.popleft()
|
|
33
|
+
|
|
34
|
+
def project_receive(self, message: Mapping[str, Any]) -> None:
|
|
35
|
+
event = dict(message)
|
|
36
|
+
if (
|
|
37
|
+
event.get("type") != "webtransport.stream.receive"
|
|
38
|
+
or str(event.get("stream_direction") or "bidi") != "bidi"
|
|
39
|
+
):
|
|
40
|
+
self.pending.append(event)
|
|
41
|
+
return
|
|
42
|
+
data = event.get("data")
|
|
43
|
+
if data is None:
|
|
44
|
+
data = event.get("bytes")
|
|
45
|
+
if not isinstance(data, (bytes, bytearray, memoryview)):
|
|
46
|
+
self.pending.append(event)
|
|
47
|
+
return
|
|
48
|
+
raw = bytes(data)
|
|
49
|
+
stream_id = str(event.get("stream_id"))
|
|
50
|
+
decoder = self.control_decoders.get(stream_id)
|
|
51
|
+
if decoder is None and raw.lstrip().startswith((b"{", b"[")):
|
|
52
|
+
# Compatibility for unframed peers while declared framed bindings
|
|
53
|
+
# migrate. New peers must use uint32 big-endian record boundaries.
|
|
54
|
+
self.pending.append(event)
|
|
55
|
+
return
|
|
56
|
+
if decoder is None and len(raw) >= 4:
|
|
57
|
+
declared_size = struct.unpack_from("!I", raw, 0)[0]
|
|
58
|
+
if declared_size > 3 * 1024 * 1024:
|
|
59
|
+
self.pending.append(event)
|
|
60
|
+
return
|
|
61
|
+
if decoder is None:
|
|
62
|
+
decoder = WebTransportRecordDecoder()
|
|
63
|
+
self.control_decoders[stream_id] = decoder
|
|
64
|
+
more = event.get("more")
|
|
65
|
+
records = decoder.feed(raw, final=more is False)
|
|
66
|
+
if more is False:
|
|
67
|
+
self.control_decoders.pop(stream_id, None)
|
|
68
|
+
for record in records:
|
|
69
|
+
projected = dict(event)
|
|
70
|
+
projected.pop("bytes", None)
|
|
71
|
+
projected["data"] = json.dumps(
|
|
72
|
+
record, separators=(",", ":")
|
|
73
|
+
).encode("utf-8")
|
|
74
|
+
projected["framing"] = "jsonrpc"
|
|
75
|
+
projected["jsonrpc_complete"] = True
|
|
76
|
+
self.pending.append(projected)
|
|
77
|
+
|
|
78
|
+
def close(self) -> None:
|
|
79
|
+
self.pending.clear()
|
|
80
|
+
self.control_decoders.clear()
|
|
81
|
+
|
|
82
|
+
async def cleanup(self, *callbacks: Any) -> None:
|
|
83
|
+
self.close()
|
|
84
|
+
for callback in callbacks:
|
|
85
|
+
if not callable(callback):
|
|
86
|
+
continue
|
|
87
|
+
result = callback()
|
|
88
|
+
if inspect.isawaitable(result):
|
|
89
|
+
await result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = ["WebTransportSessionPlan"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrbl-kernel
|
|
3
|
-
Version: 0.4.5.
|
|
3
|
+
Version: 0.4.5.dev8
|
|
4
4
|
Summary: Kernel orchestration for composing Tigrbl runtime plans, bindings, operation dispatch, and optimized ASGI execution.
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -225,14 +225,14 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
225
225
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
226
226
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
227
227
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
228
|
-
Requires-Dist: tigrbl-atoms
|
|
229
|
-
Requires-Dist: tigrbl-core
|
|
228
|
+
Requires-Dist: tigrbl-atoms (>=0.4.5.dev5)
|
|
229
|
+
Requires-Dist: tigrbl-core (>=0.4.5.dev6)
|
|
230
230
|
Requires-Dist: tigrbl-typing
|
|
231
231
|
Project-URL: Discord, https://discord.gg/K4YTAPapjR
|
|
232
232
|
Project-URL: Homepage, https://github.com/tigrbl/tigrbl
|
|
233
233
|
Project-URL: Issues, https://github.com/tigrbl/tigrbl/issues
|
|
234
234
|
Project-URL: Organization, https://github.com/tigrbl
|
|
235
|
-
Project-URL: Repository, https://github.com/tigrbl/tigrbl/tree/master/pkgs/
|
|
235
|
+
Project-URL: Repository, https://github.com/tigrbl/tigrbl/tree/master/pkgs/45_kernel/tigrbl_kernel
|
|
236
236
|
Description-Content-Type: text/markdown
|
|
237
237
|
|
|
238
238
|
<div align="center">
|
|
@@ -242,7 +242,7 @@ Description-Content-Type: text/markdown
|
|
|
242
242
|
<a href="https://pypi.org/project/tigrbl-kernel/"><img src="https://img.shields.io/pypi/v/tigrbl-kernel?label=PyPI" alt="PyPI version for tigrbl-kernel"/></a>
|
|
243
243
|
<a href="https://pypi.org/project/tigrbl-kernel/"><img src="https://static.pepy.tech/badge/tigrbl-kernel" alt="Downloads for tigrbl-kernel"/></a>
|
|
244
244
|
<a href="https://discord.gg/K4YTAPapjR"><img src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord community for tigrbl-kernel"/></a>
|
|
245
|
-
<a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/
|
|
245
|
+
<a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/45_kernel/tigrbl_kernel/README.md"><img src="https://hits.sh/github.com/tigrbl/tigrbl/blob/master/pkgs/45_kernel/tigrbl_kernel/README.md.svg?label=hits" alt="Repository hits for tigrbl-kernel README"/></a>
|
|
246
246
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-525252" alt="Apache 2.0 license"/></a>
|
|
247
247
|
<a href="pyproject.toml"><img src="https://img.shields.io/badge/python-3.10%2C%203.11%2C%203.12%2C%203.13%2C%203.14-3776ab" alt="Python versions 3.10 | 3.11 | 3.12 | 3.13 | 3.14 for tigrbl-kernel"/></a>
|
|
248
248
|
<a href="https://github.com/tigrbl/tigrbl/blob/master/docs/README.md"><img src="https://img.shields.io/badge/workspace-core-1f6feb" alt="Workspace group for tigrbl-kernel"/></a>
|
|
@@ -266,7 +266,7 @@ Framework maintainers, extension authors, and advanced users composing Tigrbl fr
|
|
|
266
266
|
|
|
267
267
|
## Where does tigrbl-kernel fit?
|
|
268
268
|
|
|
269
|
-
`tigrbl-kernel` lives at `pkgs/
|
|
269
|
+
`tigrbl-kernel` lives at `pkgs/45_kernel/tigrbl_kernel` and serves a focused layer in the split Tigrbl framework.
|
|
270
270
|
|
|
271
271
|
## How does tigrbl-kernel work?
|
|
272
272
|
|
|
@@ -288,7 +288,7 @@ pip install tigrbl-kernel
|
|
|
288
288
|
| Surface | Value |
|
|
289
289
|
|---|---|
|
|
290
290
|
| PyPI package | [`tigrbl-kernel`](https://pypi.org/project/tigrbl-kernel/) |
|
|
291
|
-
| Repository path | [`pkgs/
|
|
291
|
+
| Repository path | [`pkgs/45_kernel/tigrbl_kernel`](https://github.com/tigrbl/tigrbl/tree/master/pkgs/45_kernel/tigrbl_kernel) |
|
|
292
292
|
| Python import root | `tigrbl_kernel` |
|
|
293
293
|
| Console scripts | none declared |
|
|
294
294
|
| Entry points | none declared |
|
|
@@ -444,7 +444,7 @@ Choose `tigrbl-kernel` when the quick-answer table matches your use case. Choose
|
|
|
444
444
|
|
|
445
445
|
- Community: [Discord](https://discord.gg/K4YTAPapjR).
|
|
446
446
|
- Issues: [GitHub Issues](https://github.com/tigrbl/tigrbl/issues).
|
|
447
|
-
- Repository: [pkgs/
|
|
447
|
+
- Repository: [pkgs/45_kernel/tigrbl_kernel](https://github.com/tigrbl/tigrbl/tree/master/pkgs/45_kernel/tigrbl_kernel).
|
|
448
448
|
|
|
449
449
|
## Package-local Boundary
|
|
450
450
|
|
|
@@ -455,7 +455,7 @@ This file is a package-local distribution entry point. This README is the packag
|
|
|
455
455
|
- Package status: governed package in the `tigrbl/tigrbl` workspace.
|
|
456
456
|
- Governance source: [SSOT registry](https://github.com/tigrbl/tigrbl/blob/master/.ssot/registry.json).
|
|
457
457
|
- Release evidence: [publish workflow](https://github.com/tigrbl/tigrbl/actions/workflows/publish.yml) validates package builds, tests, GitHub release assets, and PyPI publication for managed packages.
|
|
458
|
-
- Local certification guard: `pkgs/
|
|
458
|
+
- Local certification guard: `pkgs/97_tests/tigrbl_tests/tests/unit/test_package_badges_and_notices.py` verifies every package README keeps the Discord badge, Apache 2.0 badge, explicit Python-version badge, `LICENSE`, and `NOTICE`.
|
|
459
459
|
- Scope note: this README documents the package boundary. Runtime feature support remains governed by `.ssot/` entities and the conformance docs linked below.
|
|
460
460
|
|
|
461
461
|
## License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
tigrbl_kernel/__init__.py,sha256=
|
|
2
|
-
tigrbl_kernel/_build.py,sha256=
|
|
3
|
-
tigrbl_kernel/_compile.py,sha256=
|
|
1
|
+
tigrbl_kernel/__init__.py,sha256=61zqMAzNVSEJKPyYNpiUfoJkkTVYJ-XSYUI3hyOTY7g,2283
|
|
2
|
+
tigrbl_kernel/_build.py,sha256=h2-bpRVXRoHzbhyAAuFM4D_Mq6N75sTHb4kjwxXIgTc,51151
|
|
3
|
+
tigrbl_kernel/_compile.py,sha256=i-p_DB__Fem-f0G4CX_K1s3gSRYmXl3ewEKp4FWF6FU,16601
|
|
4
4
|
tigrbl_kernel/atoms.py,sha256=ZNGj2PBUMu1BvPjNYlz4_vu4vhx4gD20C7zNE_GKfe8,12253
|
|
5
5
|
tigrbl_kernel/cache.py,sha256=NxuqvH03xYwcQoY86ie1hmtDeA36XdiLmNyx2ImLQ6k,2596
|
|
6
6
|
tigrbl_kernel/callbacks.py,sha256=r_ZI19nbAoDM-uf-FMUmWWAF9WaEKJK3Fpg-58ui8a4,772
|
|
@@ -19,12 +19,12 @@ tigrbl_kernel/lifecycle_matrix.py,sha256=SCpcCY8csYyepNGmElYzretQcdM9Yud-XYPcYH8
|
|
|
19
19
|
tigrbl_kernel/loop_modes.py,sha256=HnT3upEUK75R_sf92TrmOyszic0Kt-rDCDU93NhPL9M,1636
|
|
20
20
|
tigrbl_kernel/loop_regions.py,sha256=M4GbV5hujPA6vTGDoW_r-ElGlwPgorB8xNjP-22Xpy0,2224
|
|
21
21
|
tigrbl_kernel/measure.py,sha256=eK_ceLmF_Waao3v2KHjiaZQmbvRaji9GLrvnjXo9pmw,52310
|
|
22
|
-
tigrbl_kernel/models.py,sha256=
|
|
22
|
+
tigrbl_kernel/models.py,sha256=Uau8gfwl5U2mctWyApyrnSYlPPYpS8nBcJd4Wxl5PAo,14703
|
|
23
23
|
tigrbl_kernel/opchannel_capabilities.py,sha256=5zM9-1fc-W8BXyXF4VVWrIsmdwCH5xLraGyO_AJwRrs,1541
|
|
24
|
-
tigrbl_kernel/opview_compiler.py,sha256=
|
|
24
|
+
tigrbl_kernel/opview_compiler.py,sha256=jR2Oe8chSGpYI_-IFFDtip9TZkDrDEkW03UWGdgPJn4,11284
|
|
25
25
|
tigrbl_kernel/ordering.py,sha256=C6fHkAwQA_1AY7sEAUy_FC5U3eFmsyqcls1li1qG5MA,13576
|
|
26
26
|
tigrbl_kernel/packed_access.py,sha256=KhvAt-X4_bsowYVQ47tWEIfQhpcvTmPG7BqzuEWF87U,3587
|
|
27
|
-
tigrbl_kernel/packed_selectors.py,sha256
|
|
27
|
+
tigrbl_kernel/packed_selectors.py,sha256=-ZOydklrxMdjITHjM05HlqKCl1hj0oqw3WOS-fQXvz4,13268
|
|
28
28
|
tigrbl_kernel/payload.py,sha256=eyHo-cAUXLWQQ9-xZVOl_rjFudA-djMZji9hoK-v9IE,2167
|
|
29
29
|
tigrbl_kernel/protocol_anchors.py,sha256=0jAXmSQ-OD3AGv7MM-2vF7hQmZtHG2bXwFM1ooi0Geg,863
|
|
30
30
|
tigrbl_kernel/protocol_bindings.py,sha256=oE50T0YIEDAPXqxpXnmSAmJWX_SbSPdXBL8FtFF7OgI,18760
|
|
@@ -34,7 +34,7 @@ tigrbl_kernel/protocol_chains/http_unary.py,sha256=7EABv0bUAJ5UI3TWP-S-8LtgwZJLj
|
|
|
34
34
|
tigrbl_kernel/protocol_chains/lifespan.py,sha256=kJdUuD-Yy4rJr5VeLv0Qs9WjDI_EcYlaJEKovc_LS7Y,609
|
|
35
35
|
tigrbl_kernel/protocol_chains/sse.py,sha256=GASXjoFSxLHQmP85z2BscEkOlc-uSojr5JMjv2XZN8M,1149
|
|
36
36
|
tigrbl_kernel/protocol_chains/static_files.py,sha256=KxtD5_7MOpl400oLutkVFawwgZsyPEh6gNtWy7Q34pc,807
|
|
37
|
-
tigrbl_kernel/protocol_chains/websocket.py,sha256=
|
|
37
|
+
tigrbl_kernel/protocol_chains/websocket.py,sha256=_pSAHPc9ZGIhtINpeqJkM_X7GfsBFN0u9rqiQyGg1ZE,16948
|
|
38
38
|
tigrbl_kernel/protocol_completion.py,sha256=VqQN54ZYZGbwxotZaLRj6Ai2r_HJFBdl2mCl1_LCICs,1075
|
|
39
39
|
tigrbl_kernel/protocol_fusion.py,sha256=uaj_CM3z68NohnV0qE872jrd-pTEgL8oPQkdHKsaz8k,2407
|
|
40
40
|
tigrbl_kernel/protocol_legality_matrix.py,sha256=71VbwQU4sR0MnB3Netwu7ooij6HSLLV6lzkRG5b-YjM,4128
|
|
@@ -52,8 +52,9 @@ tigrbl_kernel/transport_events.py,sha256=CyPhs69H7mnKU7gNgt4XtH-eRJcihSaXxQDzC0i
|
|
|
52
52
|
tigrbl_kernel/types.py,sha256=Pf2e6xvZAcxD_uJZ7inMETpmhNyz4YGT27YuWdSHZ-I,2431
|
|
53
53
|
tigrbl_kernel/utils.py,sha256=xf4lZAIH64DhpdHHyzPJ0LSAiUKWSaC5wIL1NJkHVro,6979
|
|
54
54
|
tigrbl_kernel/webtransport_events.py,sha256=SWADw4m2JsCHHvMfH7QcVPtjOZ0kfjiCwwoiLhCokmk,10820
|
|
55
|
-
tigrbl_kernel
|
|
56
|
-
tigrbl_kernel-0.4.5.
|
|
57
|
-
tigrbl_kernel-0.4.5.
|
|
58
|
-
tigrbl_kernel-0.4.5.
|
|
59
|
-
tigrbl_kernel-0.4.5.
|
|
55
|
+
tigrbl_kernel/webtransport_session_plan.py,sha256=9IeJMNqhnorv6L5EIA6Rc2nq21lBQv3DWQMO-GrTiZk,3213
|
|
56
|
+
tigrbl_kernel-0.4.5.dev8.dist-info/METADATA,sha256=GAUe1aS0NSTiUrbaphtpWt-rLYVLtOQQyZwCggdOi_4,28035
|
|
57
|
+
tigrbl_kernel-0.4.5.dev8.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
58
|
+
tigrbl_kernel-0.4.5.dev8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
59
|
+
tigrbl_kernel-0.4.5.dev8.dist-info/licenses/NOTICE,sha256=EvJMTshzsWz43LiK-DeN2ZuLtrP49cxvlrFlJ8F_buc,221
|
|
60
|
+
tigrbl_kernel-0.4.5.dev8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|