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.
@@ -1,415 +1,32 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
- from collections.abc import Mapping, Sequence
5
- from types import SimpleNamespace
6
- from typing import Any
3
+ import warnings
4
+ from typing import Any, NoReturn
7
5
 
8
- from tigrbl_core.config.constants import (
9
- TIGRBL_DEFAULT_ROOT_ALIAS,
10
- TIGRBL_DEFAULT_ROOT_METHOD,
11
- TIGRBL_DEFAULT_ROOT_PATH,
12
- )
13
6
 
14
-
15
- def _table_iter(app: Any) -> tuple[Any, ...]:
16
- tables = getattr(app, "tables", None)
17
- if isinstance(tables, dict):
18
- return tuple(v for v in tables.values() if isinstance(v, type))
19
- if isinstance(tables, Sequence) and not isinstance(
20
- tables, (str, bytes, bytearray)
21
- ):
22
- return tuple(tables)
23
- return ()
24
-
25
-
26
- def _opspecs(model: Any) -> tuple[Any, ...]:
27
- ops = getattr(getattr(model, "opspecs", SimpleNamespace()), "all", ()) or ()
28
- if ops:
29
- return tuple(ops)
30
-
31
- table_ops = getattr(model, "ops", None)
32
- if table_ops is not None:
33
- by_alias = getattr(table_ops, "by_alias", None)
34
- if isinstance(by_alias, Mapping) and by_alias:
35
- flattened: list[Any] = []
36
- for specs in by_alias.values():
37
- if specs is None:
38
- continue
39
- if isinstance(specs, Sequence) and not isinstance(
40
- specs, (str, bytes, bytearray)
41
- ):
42
- flattened.extend(tuple(specs))
43
- continue
44
- alias = getattr(specs, "alias", None)
45
- target = getattr(specs, "target", None)
46
- if alias is not None and target is not None:
47
- flattened.append(specs)
48
- if flattened:
49
- return tuple(flattened)
50
- all_ops = getattr(table_ops, "all", ()) or ()
51
- if all_ops:
52
- return tuple(all_ops)
53
- if isinstance(table_ops, Sequence) and not isinstance(
54
- table_ops, (str, bytes, bytearray)
55
- ):
56
- return tuple(table_ops)
57
-
58
- declared_ops = getattr(model, "__tigrbl_ops__", ()) or ()
59
- if declared_ops:
60
- return tuple(declared_ops)
61
-
62
- return ()
63
-
64
-
65
- def _as_mapping(app: Any) -> dict[str, Any]:
66
- if isinstance(app, str):
67
- payload = json.loads(app)
68
- if not isinstance(payload, dict):
69
- raise TypeError("rust runtime expects a mapping or JSON object")
70
- return payload
71
- if isinstance(app, Mapping):
72
- return dict(app)
73
- return {
74
- "name": getattr(
75
- app,
76
- "name",
77
- getattr(
78
- app,
79
- "title",
80
- getattr(app, "TITLE", app.__class__.__name__),
81
- ),
82
- ),
83
- "title": getattr(
84
- app,
85
- "title",
86
- getattr(
87
- app,
88
- "name",
89
- getattr(app, "TITLE", app.__class__.__name__),
90
- ),
91
- ),
92
- "version": getattr(app, "version", getattr(app, "VERSION", "0.1.0")),
93
- "jsonrpc_prefix": getattr(app, "jsonrpc_prefix", "/rpc"),
94
- "system_prefix": getattr(app, "system_prefix", "/system"),
95
- "bindings": list(getattr(app, "bindings", []) or []),
96
- "tables": list(getattr(app, "tables", []) or []),
97
- "engines": list(getattr(app, "engines", []) or []),
98
- "callbacks": list(getattr(app, "callbacks", []) or []),
99
- "runtime": dict(getattr(app, "runtime_config", {}) or {}),
100
- "metadata": dict(getattr(app, "metadata", {}) or {}),
101
- "engine": getattr(app, "engine", None),
102
- }
103
-
104
-
105
- def _machine_name(source: dict[str, Any], app: Any) -> str:
106
- raw = (
107
- source.get("name")
108
- or getattr(app, "name", None)
109
- or source.get("title")
110
- or getattr(app, "title", None)
111
- or getattr(app, "TITLE", None)
112
- or app.__class__.__name__
113
- )
114
- return str(raw).strip()
115
-
116
-
117
- def _table_name(table: Any) -> str:
118
- for attr in ("__tablename__", "__resource__", "resource_name", "name"):
119
- value = getattr(table, attr, None)
120
- if callable(value):
121
- try:
122
- value = value()
123
- except Exception:
124
- value = None
125
- if isinstance(value, str) and value:
126
- return value
127
- model = getattr(table, "model", None)
128
- if isinstance(model, type):
129
- return _table_name(model)
130
- if isinstance(model, str) and model:
131
- return model.rsplit(":", 1)[-1].rsplit(".", 1)[-1].lower()
132
- model_ref = getattr(table, "model_ref", None)
133
- if isinstance(model_ref, str) and model_ref:
134
- return model_ref.rsplit(":", 1)[-1].rsplit(".", 1)[-1].lower()
135
- return getattr(table, "__name__", table.__class__.__name__).lower()
136
-
137
-
138
- def _iter_tables(source: dict[str, Any], app: Any) -> tuple[Any, ...]:
139
- raw = source.get("tables")
140
- if raw is None:
141
- return _table_iter(app)
142
- if isinstance(raw, Mapping):
143
- return tuple(raw.values())
144
- if isinstance(raw, Sequence) and not isinstance(raw, (str, bytes, bytearray)):
145
- return tuple(raw)
146
- raise TypeError("rust runtime expects app.tables to be a mapping or sequence")
147
-
148
-
149
- def _normalize_transport(proto: str) -> str:
150
- mapping = {
151
- "http.rest": "rest",
152
- "https.rest": "rest",
153
- "http.jsonrpc": "jsonrpc",
154
- "https.jsonrpc": "jsonrpc",
155
- "http.stream": "stream",
156
- "https.stream": "stream",
157
- "http.sse": "sse",
158
- "https.sse": "sse",
159
- "ws": "ws",
160
- "wss": "ws",
161
- "webtransport": "webtransport",
162
- }
163
- return mapping.get(proto, proto or "rest")
164
-
165
-
166
- def _normalize_path(path: Any) -> str:
167
- return str(path or "/").rstrip("/") or "/"
168
-
169
-
170
- def _is_default_root_binding(binding: Mapping[str, Any]) -> bool:
171
- return (
172
- binding.get("transport") == "rest"
173
- and _normalize_path(binding.get("path")) == "/"
7
+ def _raise_deprecated(action: str) -> NoReturn:
8
+ warnings.warn(
9
+ "tigrbl_kernel Rust spec serialization is deprecated; "
10
+ "Tigrbl runtime execution is Python-only.",
11
+ DeprecationWarning,
12
+ stacklevel=3,
174
13
  )
175
-
176
-
177
- def _has_root_binding(bindings: list[dict[str, Any]]) -> bool:
178
- return any(_is_default_root_binding(binding) for binding in bindings)
179
-
180
-
181
- def _default_root_binding() -> dict[str, Any]:
182
- return {
183
- "alias": TIGRBL_DEFAULT_ROOT_ALIAS,
184
- "transport": "rest",
185
- "family": "rest",
186
- "framing": None,
187
- "path": TIGRBL_DEFAULT_ROOT_PATH,
188
- "methods": (TIGRBL_DEFAULT_ROOT_METHOD,),
189
- "op": {
190
- "name": TIGRBL_DEFAULT_ROOT_ALIAS,
191
- "kind": "read",
192
- "route": TIGRBL_DEFAULT_ROOT_PATH,
193
- "exchange": "request_response",
194
- "tx_scope": "inherit",
195
- "subevents": [],
196
- },
197
- "table": {"name": TIGRBL_DEFAULT_ROOT_ALIAS},
198
- }
199
-
200
-
201
- def _binding_path(binding: Any, *, jsonrpc_prefix: str) -> str:
202
- path = getattr(binding, "path", None)
203
- if isinstance(path, str) and path:
204
- return path
205
- rpc_method = getattr(binding, "rpc_method", None)
206
- if isinstance(rpc_method, str) and rpc_method:
207
- prefix = jsonrpc_prefix.rstrip("/") or "/rpc"
208
- return f"{prefix}/{rpc_method}"
209
- return "/"
210
-
211
-
212
- def _binding_methods(binding: Any, target: str) -> tuple[str, ...]:
213
- methods = tuple(
214
- str(item).upper() for item in (getattr(binding, "methods", ()) or ())
215
- )
216
- if methods:
217
- return methods
218
- default = {
219
- "create": ("POST",),
220
- "read": ("GET",),
221
- "list": ("GET",),
222
- "delete": ("DELETE",),
223
- "replace": ("PUT",),
224
- "update": ("PATCH",),
225
- "merge": ("PATCH",),
226
- }
227
- return default.get(target, ("POST",))
228
-
229
-
230
- def _lower_declared_bindings(source: dict[str, Any], app: Any) -> list[dict[str, Any]]:
231
- raw = source.get("bindings")
232
- if raw is not None:
233
- if not (
234
- isinstance(raw, Sequence) and not isinstance(raw, (str, bytes, bytearray))
235
- ):
236
- raise TypeError("rust runtime expects app.bindings to be a sequence")
237
- if raw and all(isinstance(item, Mapping) for item in raw):
238
- payload: list[dict[str, Any]] = [dict(item) for item in raw]
239
- if not _has_root_binding(payload):
240
- payload.append(_default_root_binding())
241
- return payload
242
-
243
- payload: list[dict[str, Any]] = []
244
- jsonrpc_prefix = str(source.get("jsonrpc_prefix", "/rpc") or "/rpc")
245
-
246
- for table in _iter_tables(source, app):
247
- table_name = _table_name(table)
248
- for spec in _opspecs(table):
249
- alias = str(getattr(spec, "alias", getattr(spec, "target", "")) or "")
250
- target = str(getattr(spec, "target", alias) or alias)
251
- exchange = str(
252
- getattr(spec, "exchange", "request_response") or "request_response"
253
- )
254
- tx_scope = str(getattr(spec, "tx_scope", "inherit") or "inherit")
255
- subevents = [
256
- str(item) for item in tuple(getattr(spec, "subevents", ()) or ())
257
- ]
258
- for binding in tuple(getattr(spec, "bindings", ()) or ()):
259
- proto = str(getattr(binding, "proto", "") or "")
260
- transport = _normalize_transport(proto)
261
- path = _binding_path(binding, jsonrpc_prefix=jsonrpc_prefix)
262
- methods = _binding_methods(binding, target)
263
- payload.append(
264
- {
265
- "alias": alias,
266
- "transport": transport,
267
- "family": "bidirectional"
268
- if transport in {"ws", "webtransport"}
269
- else transport,
270
- "framing": getattr(binding, "framing", None),
271
- "path": path,
272
- "methods": methods,
273
- "op": {
274
- "name": alias,
275
- "kind": target,
276
- "route": path,
277
- "exchange": str(
278
- getattr(binding, "exchange", exchange) or exchange
279
- ),
280
- "tx_scope": tx_scope,
281
- "subevents": subevents,
282
- },
283
- "table": {"name": table_name},
284
- }
285
- )
286
- if not _has_root_binding(payload):
287
- payload.append(_default_root_binding())
288
- return payload
289
-
290
-
291
- def _lower_declared_tables(source: dict[str, Any], app: Any) -> list[dict[str, str]]:
292
- raw = source.get("tables")
293
- if raw is not None:
294
- if not (
295
- isinstance(raw, Sequence) and not isinstance(raw, (str, bytes, bytearray))
296
- ):
297
- raise TypeError("rust runtime expects app.tables to be a sequence")
298
- if raw and all(isinstance(item, Mapping) for item in raw):
299
- return [dict(item) for item in raw]
300
- return [{"name": _table_name(table)} for table in _iter_tables(source, app)]
301
-
302
-
303
- def _lower_declared_engines(source: dict[str, Any], app: Any) -> list[dict[str, Any]]:
304
- raw = source.get("engines")
305
- if raw is not None:
306
- if not (
307
- isinstance(raw, Sequence) and not isinstance(raw, (str, bytes, bytearray))
308
- ):
309
- raise TypeError("rust runtime expects app.engines to be a sequence")
310
- if raw:
311
- return [
312
- dict(item)
313
- if isinstance(item, Mapping)
314
- else {"name": "default", "kind": str(item)}
315
- for item in raw
316
- ]
317
-
318
- engine = source.get("engine", getattr(app, "engine", None))
319
- if engine is None:
320
- return [{"name": "default", "kind": "inmemory", "language": "rust"}]
321
-
322
- try:
323
- from tigrbl_core._spec.engine_spec import EngineSpec
324
-
325
- spec = EngineSpec.from_any(engine)
326
- except Exception:
327
- spec = None
328
-
329
- kind = "inmemory"
330
- options: dict[str, Any] = {}
331
- if spec is not None and getattr(spec, "kind", None):
332
- if spec.kind == "sqlite" and getattr(spec, "memory", False):
333
- kind = "inmemory"
334
- else:
335
- kind = str(spec.kind)
336
- path = getattr(spec, "path", None)
337
- if isinstance(path, str) and path:
338
- options["path"] = path
339
- dsn = getattr(spec, "dsn", None)
340
- if isinstance(dsn, str) and dsn:
341
- options["dsn"] = dsn
342
- options["memory"] = bool(getattr(spec, "memory", False))
343
-
344
- return [{"name": "default", "kind": kind, "language": "rust", "options": options}]
345
-
346
-
347
- def _callable_name(value: Any) -> str:
348
- module = getattr(value, "__module__", None)
349
- qualname = getattr(value, "__qualname__", getattr(value, "__name__", None))
350
- if module and qualname:
351
- return f"{module}.{qualname}"
352
- return str(value)
353
-
354
-
355
- def _lower_callbacks(source: dict[str, Any]) -> list[dict[str, Any]]:
356
- raw = source.get("callbacks", ()) or ()
357
- if not (
358
- isinstance(raw, Sequence) and not isinstance(raw, (str, bytes, bytearray))
359
- ):
360
- raise TypeError("rust runtime expects app.callbacks to be a sequence")
361
- payload: list[dict[str, Any]] = []
362
- for item in raw:
363
- if isinstance(item, Mapping):
364
- payload.append(dict(item))
365
- continue
366
- payload.append(
367
- {
368
- "name": _callable_name(item),
369
- "kind": "callback",
370
- "language": "python",
371
- }
372
- )
373
- return payload
14
+ raise RuntimeError(f"{action} is unavailable; Tigrbl runtime execution is Python-only.")
374
15
 
375
16
 
376
17
  def build_rust_app_spec(app: Any) -> dict[str, Any]:
377
- source = _as_mapping(app)
378
- name = _machine_name(source, app)
379
- payload: dict[str, Any] = {
380
- "name": name,
381
- "title": str(source.get("title", name)).strip(),
382
- "version": str(source.get("version", "0.1.0")).strip(),
383
- "jsonrpc_prefix": str(source.get("jsonrpc_prefix", "/rpc")).strip() or "/rpc",
384
- "system_prefix": str(source.get("system_prefix", "/system")).strip()
385
- or "/system",
386
- "metadata": dict(source.get("metadata", {}) or {}),
387
- "runtime": dict(source.get("runtime", {}) or {}),
388
- "dependencies": source.get("dependencies", {}) or {},
389
- "security": source.get("security", {}) or {},
390
- "bindings": _lower_declared_bindings(source, app),
391
- "tables": _lower_declared_tables(source, app),
392
- "engines": _lower_declared_engines(source, app),
393
- "callbacks": _lower_callbacks(source),
394
- }
395
- if not payload["name"]:
396
- raise ValueError("rust runtime requires a non-empty app name")
397
- return payload
18
+ del app
19
+ _raise_deprecated("build_rust_app_spec")
398
20
 
399
21
 
400
22
  def coerce_rust_spec_dict(app: Any) -> dict[str, Any]:
401
- if isinstance(app, str):
402
- payload = json.loads(app)
403
- if not isinstance(payload, dict):
404
- raise TypeError("rust runtime expects a mapping or JSON object")
405
- return payload
406
- if isinstance(app, Mapping):
407
- return build_rust_app_spec(dict(app))
408
- return build_rust_app_spec(app)
23
+ del app
24
+ _raise_deprecated("coerce_rust_spec_dict")
409
25
 
410
26
 
411
27
  def coerce_rust_spec_json(app: Any) -> str:
412
- return json.dumps(coerce_rust_spec_dict(app), sort_keys=True)
28
+ del app
29
+ _raise_deprecated("coerce_rust_spec_json")
413
30
 
414
31
 
415
32
  __all__ = [
@@ -46,11 +46,14 @@ _KNOWN_TRANSPORTS = {
46
46
  },
47
47
  "callback": {
48
48
  "scope_type": None,
49
- "events": (("callback.emit", "outbound", False, "message.emit"),),
49
+ "events": (
50
+ ("callback.delivery.emit", "outbound", False, "message.emit"),
51
+ ("callback.delivery.emit_complete", "outbound", False, "message.processed"),
52
+ ),
50
53
  },
51
54
  "webhook": {
52
55
  "scope_type": "http",
53
- "events": (("webhook.emit", "outbound", False, "message.emit"),),
56
+ "events": (("webhook.receive", "inbound", False, "request.received"),),
54
57
  },
55
58
  }
56
59
 
tigrbl_kernel/types.py CHANGED
@@ -37,6 +37,7 @@ EFFECT_BY_ATOM_NAME = {
37
37
  "sys.handler_tail": EFFECT_WIRE,
38
38
  "sys.handler_upload": EFFECT_WIRE,
39
39
  "sys.handler_download": EFFECT_WIRE,
40
+ "sys.handler_well_known": EFFECT_WIRE,
40
41
  "sys.handler_append_chunk": EFFECT_WIRE,
41
42
  "sys.handler_send_datagram": EFFECT_WIRE,
42
43
  "sys.handler_checkpoint": EFFECT_WIRE,
@@ -109,6 +109,15 @@ _STREAM_DIRECTIONS = {
109
109
  "client_to_server": "unidi_client_stream",
110
110
  "server_to_client": "unidi_server_stream",
111
111
  }
112
+ _STREAM_DIRECTION_METADATA = {
113
+ "bidi": "bidirectional",
114
+ "client_to_server": "client_to_server",
115
+ "server_to_client": "server_to_client",
116
+ }
117
+ _UNIDI_INITIATORS = {
118
+ "client_to_server": "client",
119
+ "server_to_client": "server",
120
+ }
112
121
 
113
122
 
114
123
  def validate_webtransport_event_payload(
@@ -141,7 +150,15 @@ def _validate_session_payload(
141
150
  expected_channel = "receive" if event in {"webtransport.connect", "webtransport.disconnect"} else "send"
142
151
  if channel != expected_channel:
143
152
  raise ValueError(f"{event} is only valid on {expected_channel}")
144
- _forbid(payload, "stream_id", "stream_direction", "datagram_id", "framing")
153
+ _forbid(
154
+ payload,
155
+ "stream_id",
156
+ "stream_direction",
157
+ "stream_initiator",
158
+ "lane_id",
159
+ "datagram_id",
160
+ "framing",
161
+ )
145
162
  return {"family": "session", "lane": "session", "exchange": "request_response"}
146
163
 
147
164
 
@@ -166,6 +183,11 @@ def _validate_stream_payload(
166
183
  else:
167
184
  direction = str(direction) if direction in _STREAM_DIRECTIONS else "bidi"
168
185
  lane = _STREAM_DIRECTIONS[str(direction)]
186
+ stream_initiator = _stream_initiator(
187
+ payload.get("stream_initiator"),
188
+ stream_direction=str(direction),
189
+ channel=channel,
190
+ )
169
191
  if channel == "receive" and lane == "unidi_server_stream":
170
192
  raise ValueError("server_to_client unidirectional streams cannot be receive events")
171
193
  if channel == "send" and lane == "unidi_client_stream":
@@ -174,7 +196,7 @@ def _validate_stream_payload(
174
196
  lane=lane,
175
197
  inner_framing=payload.get("framing"),
176
198
  )
177
- return {
199
+ projection = {
178
200
  "family": "stream",
179
201
  "lane": lane,
180
202
  "exchange": {
@@ -182,7 +204,15 @@ def _validate_stream_payload(
182
204
  "unidi_client_stream": "client_stream",
183
205
  "unidi_server_stream": "server_stream",
184
206
  }[lane],
207
+ "stream_direction": str(direction),
208
+ "direction": _STREAM_DIRECTION_METADATA[str(direction)],
209
+ "stream_initiator": stream_initiator,
185
210
  }
211
+ _copy_int_field(payload, projection, "lane_id")
212
+ _copy_int_field(payload, projection, "stream_ordinal")
213
+ if payload.get("stream_id_width") is not None:
214
+ projection["stream_id_width"] = str(payload["stream_id_width"])
215
+ return projection
186
216
 
187
217
 
188
218
  def _validate_datagram_payload(
@@ -195,7 +225,7 @@ def _validate_datagram_payload(
195
225
  if channel != expected_channel:
196
226
  raise ValueError(f"{event} is only valid on {expected_channel}")
197
227
  _require(payload, "datagram_id")
198
- _forbid(payload, "stream_id", "stream_direction")
228
+ _forbid(payload, "stream_id", "stream_direction", "stream_initiator", "lane_id")
199
229
  validate_webtransport_inner_framing(
200
230
  lane="datagram",
201
231
  inner_framing=payload.get("framing"),
@@ -216,6 +246,38 @@ def _forbid(payload: dict[str, Any], *fields: str) -> None:
216
246
  raise ValueError(f"WebTransport payload field not valid for event: {joined}")
217
247
 
218
248
 
249
+ def _stream_initiator(
250
+ value: object,
251
+ *,
252
+ stream_direction: str,
253
+ channel: str,
254
+ ) -> str:
255
+ if stream_direction in _UNIDI_INITIATORS:
256
+ expected = _UNIDI_INITIATORS[stream_direction]
257
+ if value is not None and str(value) != expected:
258
+ raise ValueError(f"{stream_direction} stream_initiator must be {expected}")
259
+ return expected
260
+ if value is None:
261
+ return "client"
262
+ token = str(value)
263
+ if token not in {"client", "server"}:
264
+ raise ValueError("WebTransport stream_initiator must be client or server")
265
+ return token
266
+
267
+
268
+ def _copy_int_field(
269
+ payload: dict[str, Any],
270
+ projection: dict[str, object],
271
+ field: str,
272
+ ) -> None:
273
+ value = payload.get(field)
274
+ if value is None:
275
+ return
276
+ if isinstance(value, bool) or not isinstance(value, int):
277
+ raise ValueError(f"WebTransport payload {field} must be an integer")
278
+ projection[field] = value
279
+
280
+
219
281
  __all__ = [
220
282
  "compile_webtransport_chain",
221
283
  "compile_webtransport_events",