tigrbl-runtime 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.
Files changed (81) hide show
  1. tigrbl_runtime/__init__.py +0 -27
  2. tigrbl_runtime/callbacks.py +113 -12
  3. tigrbl_runtime/channel/_asgi_completion.py +33 -0
  4. tigrbl_runtime/channel/_asgi_context.py +96 -0
  5. tigrbl_runtime/channel/_asgi_jsonrpc.py +38 -0
  6. tigrbl_runtime/channel/_asgi_receive.py +63 -0
  7. tigrbl_runtime/channel/_asgi_scope.py +75 -0
  8. tigrbl_runtime/channel/_asgi_send.py +242 -0
  9. tigrbl_runtime/channel/_asgi_webtransport.py +335 -0
  10. tigrbl_runtime/channel/asgi.py +72 -860
  11. tigrbl_runtime/executors/__init__.py +2 -0
  12. tigrbl_runtime/executors/_invoke_support.py +193 -0
  13. tigrbl_runtime/executors/ctx/__init__.py +15 -0
  14. tigrbl_runtime/executors/ctx/context.py +392 -0
  15. tigrbl_runtime/executors/ctx/hot.py +35 -0
  16. tigrbl_runtime/executors/ctx/hot_namespaces.py +421 -0
  17. tigrbl_runtime/executors/ctx/hot_state.py +226 -0
  18. tigrbl_runtime/executors/invoke.py +70 -174
  19. tigrbl_runtime/executors/kernel_executor.py +1 -1
  20. tigrbl_runtime/executors/packed.py +154 -603
  21. tigrbl_runtime/executors/types.py +34 -970
  22. tigrbl_runtime/handle.py +9 -1
  23. tigrbl_runtime/runtime/__init__.py +0 -7
  24. tigrbl_runtime/runtime/exceptions.py +2 -12
  25. tigrbl_runtime/runtime/runtime.py +35 -109
  26. tigrbl_runtime/runtime/system.py +2 -2
  27. tigrbl_runtime/rust/__init__.py +5 -12
  28. tigrbl_runtime/rust/_fallback.py +31 -278
  29. tigrbl_runtime/rust/_load_rust.py +3 -9
  30. tigrbl_runtime/rust/backend.py +25 -1
  31. tigrbl_runtime/rust/callbacks.py +16 -33
  32. tigrbl_runtime/rust/codec.py +18 -5
  33. tigrbl_runtime/rust/compile.py +5 -25
  34. tigrbl_runtime/rust/errors.py +27 -2
  35. tigrbl_runtime/rust/request.py +8 -0
  36. tigrbl_runtime/rust/response.py +8 -0
  37. tigrbl_runtime/rust/runtime.py +26 -91
  38. tigrbl_runtime/rust/trace.py +8 -43
  39. tigrbl_runtime/semantics.py +319 -0
  40. {tigrbl_runtime-0.4.2.dev4.dist-info → tigrbl_runtime-0.4.3.dev4.dist-info}/METADATA +14 -5
  41. tigrbl_runtime-0.4.3.dev4.dist-info/RECORD +58 -0
  42. tigrbl_runtime/channel/state.py +0 -11
  43. tigrbl_runtime/executors/helpers.py +0 -3
  44. tigrbl_runtime/executors/loop_regions.py +0 -46
  45. tigrbl_runtime/protocol/__init__.py +0 -19
  46. tigrbl_runtime/protocol/_iterators.py +0 -3
  47. tigrbl_runtime/protocol/anchors.py +0 -38
  48. tigrbl_runtime/protocol/app_frame_codec.py +0 -111
  49. tigrbl_runtime/protocol/completion_fence.py +0 -41
  50. tigrbl_runtime/protocol/dispatch_atoms.py +0 -46
  51. tigrbl_runtime/protocol/framing_atoms.py +0 -50
  52. tigrbl_runtime/protocol/http_stream.py +0 -3
  53. tigrbl_runtime/protocol/http_unary.py +0 -8
  54. tigrbl_runtime/protocol/lifespan_chain.py +0 -3
  55. tigrbl_runtime/protocol/loop_modes.py +0 -24
  56. tigrbl_runtime/protocol/scope_schemas.py +0 -63
  57. tigrbl_runtime/protocol/sse.py +0 -3
  58. tigrbl_runtime/protocol/static_files.py +0 -3
  59. tigrbl_runtime/protocol/subevent_handlers.py +0 -3
  60. tigrbl_runtime/protocol/transport_atoms.py +0 -3
  61. tigrbl_runtime/protocol/websocket.py +0 -3
  62. tigrbl_runtime/protocol/webtransport.py +0 -9
  63. tigrbl_runtime/protocol/webtransport_session.py +0 -129
  64. tigrbl_runtime/runtime/events.py +0 -97
  65. tigrbl_runtime/runtime/executor/__init__.py +0 -6
  66. tigrbl_runtime/runtime/executor/invoke.py +0 -72
  67. tigrbl_runtime/runtime/kernel.py +0 -35
  68. tigrbl_runtime/runtime/labels.py +0 -3
  69. tigrbl_runtime/runtime/status/__init__.py +0 -1
  70. tigrbl_runtime/runtime/status/converters.py +0 -1
  71. tigrbl_runtime/runtime/status/exceptions.py +0 -1
  72. tigrbl_runtime/runtime/status/mappings.py +0 -1
  73. tigrbl_runtime/runtime/status/utils.py +0 -1
  74. tigrbl_runtime/rust/_parity_contract.py +0 -161
  75. tigrbl_runtime/rust/parity.py +0 -72
  76. tigrbl_runtime/transactions.py +0 -3
  77. tigrbl_runtime/webhooks.py +0 -116
  78. tigrbl_runtime-0.4.2.dev4.dist-info/RECORD +0 -80
  79. {tigrbl_runtime-0.4.2.dev4.dist-info → tigrbl_runtime-0.4.3.dev4.dist-info}/WHEEL +0 -0
  80. {tigrbl_runtime-0.4.2.dev4.dist-info → tigrbl_runtime-0.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
  81. {tigrbl_runtime-0.4.2.dev4.dist-info → tigrbl_runtime-0.4.3.dev4.dist-info}/licenses/NOTICE +0 -0
@@ -5,44 +5,17 @@ from __future__ import annotations
5
5
  from importlib import import_module
6
6
  from typing import Any
7
7
 
8
- from .rust import (
9
- ExecutionBackend,
10
- RustBackendConfig,
11
- RustBindingsUnavailableError,
12
- )
13
-
14
8
  _LAZY_EXPORTS = {
15
- "RustRuntimeHandleRef": "handle",
16
9
  "Runtime": "runtime",
17
10
  "RuntimeBase": "runtime",
18
11
  }
19
12
 
20
- _RUST_EXPORTS = {
21
- "RustRuntimeHandle": "RustRuntimeHandle",
22
- "clear_rust_boundary_events": "clear_ffi_boundary_events",
23
- "compiled_extension_available": "compiled_extension_available",
24
- "rust_available": "rust_available",
25
- "rust_boundary_events": "ffi_boundary_events",
26
- "rust_transport_trace": "rust_transport_trace",
27
- }
28
-
29
13
  __all__ = [
30
- "ExecutionBackend",
31
- "RustBackendConfig",
32
- "RustBindingsUnavailableError",
33
14
  *_LAZY_EXPORTS,
34
- *_RUST_EXPORTS,
35
15
  ]
36
16
 
37
17
 
38
18
  def __getattr__(name: str) -> Any:
39
- rust_name = _RUST_EXPORTS.get(name)
40
- if rust_name is not None:
41
- module = import_module(f"{__name__}.rust")
42
- value = getattr(module, rust_name)
43
- globals()[name] = value
44
- return value
45
-
46
19
  module_name = _LAZY_EXPORTS.get(name)
47
20
  if module_name is None:
48
21
  raise AttributeError(name)
@@ -1,15 +1,116 @@
1
- from tigrbl_atoms.runtime_callbacks import (
2
- CallbackRuntimeError,
3
- decode_callback_descriptor,
4
- encode_callback_descriptor,
5
- register_callback,
6
- run_callback_fence,
7
- )
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import hmac
5
+ import json
6
+ from collections.abc import Callable, Mapping
7
+ from typing import Any
8
+
9
+
10
+ def compile_callback_delivery_plan(
11
+ *,
12
+ event: str,
13
+ endpoint: str,
14
+ signing_secret_ref: str | None = None,
15
+ retry: Mapping[str, Any] | None = None,
16
+ ) -> dict[str, object]:
17
+ return {
18
+ "event": event,
19
+ "endpoint": endpoint,
20
+ "signing": {"algorithm": "hmac-sha256", "secret_ref": signing_secret_ref},
21
+ "retry": dict(retry or {"max_attempts": 1}),
22
+ "completion_subevent": "callback.delivery.emit_complete",
23
+ }
24
+
25
+
26
+ def build_callback_payload(
27
+ *, event: str, data: Mapping[str, Any], idempotency_key: str
28
+ ) -> dict[str, object]:
29
+ return {
30
+ "event": event,
31
+ "data": dict(data),
32
+ "idempotency_key": idempotency_key,
33
+ }
34
+
35
+
36
+ def sign_callback_payload(payload: Mapping[str, Any], *, secret: str) -> dict[str, object]:
37
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
38
+ digest = hmac.new(secret.encode("utf-8"), canonical, hashlib.sha256).hexdigest()
39
+ return {"payload": dict(payload), "signature": f"sha256={digest}"}
40
+
41
+
42
+ def deliver_callback(
43
+ payload: Mapping[str, Any],
44
+ *,
45
+ send: Callable[[Mapping[str, Any]], Mapping[str, Any]],
46
+ trace: Callable[[str], None] | None = None,
47
+ retry: Mapping[str, Any] | None = None,
48
+ dead_letter: bool = False,
49
+ ) -> dict[str, object]:
50
+ max_attempts = int((retry or {}).get("max_attempts", 1))
51
+ attempts = 0
52
+ last_response: Mapping[str, Any] | None = None
53
+
54
+ for attempt in range(1, max_attempts + 1):
55
+ attempts = attempt
56
+ if trace and attempt == 1:
57
+ trace("callback.delivery.emit")
58
+ try:
59
+ response = send(payload)
60
+ except Exception as exc:
61
+ if attempt < max_attempts:
62
+ continue
63
+ return {
64
+ "status": "failed",
65
+ "attempts": attempts,
66
+ "completed": False,
67
+ "error_ctx": {
68
+ "subevent": "callback.delivery.emit",
69
+ "message": str(exc),
70
+ },
71
+ }
72
+ last_response = response
73
+ status_code = int(response.get("status_code", 0))
74
+ if 200 <= status_code < 300:
75
+ if trace:
76
+ trace("callback.delivery.emit_complete")
77
+ return {
78
+ "status": "delivered",
79
+ "attempts": attempts,
80
+ "completed": True,
81
+ "response": dict(response),
82
+ }
83
+ if not _is_transient(status_code):
84
+ break
85
+
86
+ if dead_letter and last_response is not None:
87
+ status_code = int(last_response.get("status_code", 0))
88
+ return {
89
+ "status": "dead_lettered",
90
+ "attempts": attempts,
91
+ "completed": False,
92
+ "dead_letter": {
93
+ "event": payload.get("event"),
94
+ "idempotency_key": payload.get("idempotency_key"),
95
+ "status_code": status_code,
96
+ "body": last_response.get("body"),
97
+ },
98
+ }
99
+ return {
100
+ "status": "failed",
101
+ "attempts": attempts,
102
+ "completed": False,
103
+ "response": dict(last_response or {}),
104
+ }
105
+
106
+
107
+ def _is_transient(status_code: int) -> bool:
108
+ return status_code == 429 or 500 <= status_code < 600
109
+
8
110
 
9
111
  __all__ = [
10
- "CallbackRuntimeError",
11
- "decode_callback_descriptor",
12
- "encode_callback_descriptor",
13
- "register_callback",
14
- "run_callback_fence",
112
+ "build_callback_payload",
113
+ "compile_callback_delivery_plan",
114
+ "deliver_callback",
115
+ "sign_callback_payload",
15
116
  ]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from tigrbl_atoms.atoms.transport.asgi_channel import (
6
+ complete_channel_state as _complete_channel_state,
7
+ )
8
+ from tigrbl_typing.channel import OpChannel
9
+
10
+ from ._asgi_scope import build_asgi_channel
11
+ from ._asgi_send import send_transport_via_channel
12
+ from .websocket import RuntimeWebSocket
13
+
14
+
15
+ def channel_senders():
16
+ from tigrbl_atoms.atoms.egress.asgi_send import _send_json
17
+
18
+ return _send_json, send_transport_via_channel
19
+
20
+
21
+ async def complete_channel(env: Any, ctx: Any) -> None:
22
+ channel = ctx.get("channel")
23
+ if channel is None:
24
+ channel = build_asgi_channel(env)
25
+ ctx["channel"] = channel
26
+ if isinstance(getattr(channel, "state", None), dict):
27
+ _complete_channel_state(channel.state)
28
+ ctx["transport_completed"] = True
29
+ ctx["current_phase"] = "POST_EMIT"
30
+
31
+
32
+ def websocket_adapter(channel: OpChannel) -> RuntimeWebSocket:
33
+ return RuntimeWebSocket(channel)
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Mapping
5
+
6
+ from tigrbl_kernel.channel_taxonomy import normalize_exchange
7
+ from tigrbl_typing.channel import OpChannel
8
+
9
+ from ._asgi_jsonrpc import _resolve_jsonrpc_endpoint
10
+ from ._asgi_receive import _receive_session_message
11
+ from ._asgi_scope import _scheme, build_asgi_channel
12
+ from ._asgi_webtransport import (
13
+ _receive_webtransport_session_messages,
14
+ _webtransport_scope_state,
15
+ )
16
+
17
+
18
+ async def prepare_channel_context(env: Any, ctx: Any) -> OpChannel:
19
+ temp = ctx.get("temp")
20
+ if not isinstance(temp, dict):
21
+ ctx["temp"] = {}
22
+ temp = ctx["temp"]
23
+
24
+ route = temp.setdefault("route", {})
25
+ exchange = str(
26
+ route.get("exchange")
27
+ or getattr(ctx, "tigrbl_exchange", None)
28
+ or "request_response"
29
+ )
30
+ exchange = normalize_exchange(exchange)
31
+ protocol = str(route.get("protocol") or _scheme(getattr(env, "scope", {}) or {}))
32
+ framing = route.get("framing")
33
+ channel = build_asgi_channel(
34
+ env,
35
+ exchange=exchange,
36
+ protocol=protocol or None,
37
+ framing=str(framing) if isinstance(framing, str) else None,
38
+ )
39
+ ctx["channel"] = channel
40
+ ctx["path"] = channel.path
41
+ ctx["method"] = channel.method or channel.protocol.upper()
42
+
43
+ dispatch = temp.setdefault("dispatch", {})
44
+ if isinstance(dispatch, dict):
45
+ dispatch.setdefault("channel_protocol", channel.protocol)
46
+ dispatch.setdefault("channel_selector", channel.selector)
47
+ dispatch.setdefault("path_params", dict(channel.path_params))
48
+ endpoint = _resolve_jsonrpc_endpoint(ctx, channel)
49
+ if endpoint:
50
+ dispatch.setdefault("endpoint", endpoint)
51
+
52
+ scope = getattr(env, "scope", {}) or {}
53
+ scope_type = str(scope.get("type") or "http")
54
+ if scope_type == "webtransport":
55
+ await _receive_webtransport_session_messages(env, channel, ctx)
56
+ wt_state = _webtransport_scope_state(env)
57
+ trace = wt_state.get("trace")
58
+ if isinstance(trace, list):
59
+ ctx["webtransport_trace"] = trace
60
+ channel.state["webtransport_trace"] = trace
61
+ hook_trace = wt_state.get("hook_trace")
62
+ if isinstance(hook_trace, list):
63
+ ctx["webtransport_hook_trace"] = hook_trace
64
+ channel.state["webtransport_hook_trace"] = hook_trace
65
+ route.setdefault("protocol", dispatch.get("binding_protocol"))
66
+ route.setdefault("selector", channel.path)
67
+ route.setdefault("path_params", dict(channel.path_params))
68
+ route.setdefault("endpoint", dispatch.get("endpoint"))
69
+ elif scope_type == "websocket":
70
+ await _receive_session_message(
71
+ env,
72
+ channel,
73
+ ctx,
74
+ connect_type="websocket.connect",
75
+ receive_type="websocket.receive",
76
+ disconnect_type="websocket.disconnect",
77
+ eager_payload_after_connect=False,
78
+ )
79
+ message = ctx.get("channel_message")
80
+ if isinstance(message, Mapping) and message.get("text") is not None:
81
+ try:
82
+ parsed = json.loads(str(message.get("text")))
83
+ except Exception:
84
+ parsed = None
85
+ if isinstance(parsed, Mapping) and parsed.get("jsonrpc") == "2.0":
86
+ dispatch["binding_protocol"] = (
87
+ "wss.jsonrpc" if channel.protocol == "wss" else "ws.jsonrpc"
88
+ )
89
+ dispatch["rpc"] = dict(parsed)
90
+ dispatch["rpc_method"] = parsed.get("method")
91
+ route.setdefault("protocol", dispatch.get("binding_protocol"))
92
+ route.setdefault("selector", channel.path)
93
+ route.setdefault("path_params", dict(channel.path_params))
94
+ route.setdefault("endpoint", dispatch.get("endpoint"))
95
+
96
+ return channel
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+
5
+ from tigrbl_core.config.constants import __JSONRPC_DEFAULT_ENDPOINT_MAPPINGS__
6
+ from tigrbl_typing.channel import OpChannel
7
+
8
+
9
+ def _normalize_path(path: str) -> str:
10
+ return path.rstrip("/") or "/"
11
+
12
+
13
+ def _resolve_jsonrpc_endpoint(ctx: Any, channel: OpChannel) -> str | None:
14
+ if channel.kind != "http" or str(channel.method or "").upper() != "POST":
15
+ return None
16
+
17
+ path = _normalize_path(channel.path)
18
+ route = {}
19
+ temp = ctx.get("temp")
20
+ if isinstance(temp, dict):
21
+ route = temp.setdefault("route", {})
22
+ if isinstance(route, Mapping):
23
+ endpoint = route.get("endpoint")
24
+ if isinstance(endpoint, str) and endpoint:
25
+ return endpoint
26
+
27
+ for owner_key in ("router", "app"):
28
+ owner = ctx.get(owner_key)
29
+ mounts = getattr(owner, "_jsonrpc_endpoint_mounts", None)
30
+ if isinstance(mounts, Mapping):
31
+ endpoint = mounts.get(path) or mounts.get(channel.path)
32
+ if isinstance(endpoint, str) and endpoint:
33
+ return endpoint
34
+
35
+ for endpoint, mapped_path in __JSONRPC_DEFAULT_ENDPOINT_MAPPINGS__.items():
36
+ if path == _normalize_path(mapped_path):
37
+ return endpoint
38
+ return None
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from typing import Any
5
+
6
+ from tigrbl_typing.channel import OpChannel
7
+
8
+
9
+ async def _receive_session_message(
10
+ env: Any,
11
+ channel: OpChannel,
12
+ ctx: Any,
13
+ *,
14
+ connect_type: str,
15
+ receive_type: str | tuple[str, ...],
16
+ disconnect_type: str,
17
+ eager_payload_after_connect: bool = True,
18
+ ) -> None:
19
+ receive = getattr(env, "receive", None)
20
+ if not callable(receive):
21
+ return
22
+ message = await receive()
23
+ state = channel.state
24
+ state["last_event"] = message
25
+ if message.get("type") == connect_type:
26
+ state["connected"] = True
27
+ if message.get("session_id") is not None:
28
+ state["session_id"] = message.get("session_id")
29
+ if not eager_payload_after_connect:
30
+ ctx["channel_message"] = message
31
+ return
32
+ message = await receive()
33
+ state["last_event"] = message
34
+ receive_types = (receive_type,) if isinstance(receive_type, str) else receive_type
35
+ if message.get("type") in receive_types:
36
+ queue = state.get("receive_queue")
37
+ if isinstance(queue, deque):
38
+ queue.append(message)
39
+ else:
40
+ next_queue = deque()
41
+ if isinstance(queue, list):
42
+ next_queue.extend(queue)
43
+ next_queue.append(message)
44
+ state["receive_queue"] = next_queue
45
+ payload = message.get("bytes")
46
+ if payload is None:
47
+ payload = message.get("data")
48
+ if payload is None and message.get("text") is not None:
49
+ payload = str(message.get("text")).encode("utf-8")
50
+ ctx["body"] = payload
51
+ ctx["channel_message"] = message
52
+ for key in (
53
+ "session_id",
54
+ "stream_id",
55
+ "stream_direction",
56
+ "datagram_id",
57
+ "framing",
58
+ ):
59
+ if message.get(key) is not None:
60
+ state[key] = message.get(key)
61
+ elif message.get("type") == disconnect_type:
62
+ state["disconnected"] = True
63
+ ctx["channel_message"] = message
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Mapping
4
+ from urllib.parse import parse_qs
5
+
6
+ from tigrbl_kernel.channel_taxonomy import (
7
+ channel_family as _channel_family,
8
+ channel_kind as _channel_kind,
9
+ channel_subevents as _subevents,
10
+ normalize_exchange,
11
+ )
12
+ from tigrbl_typing.channel import OpChannel
13
+
14
+
15
+ def _headers(scope: Mapping[str, Any]) -> dict[str, str]:
16
+ pairs = scope.get("headers", ())
17
+ out: dict[str, str] = {}
18
+ for key, value in pairs or ():
19
+ try:
20
+ out[bytes(key).decode("latin-1").lower()] = bytes(value).decode("latin-1")
21
+ except Exception:
22
+ continue
23
+ return out
24
+
25
+
26
+ def _query(scope: Mapping[str, Any]) -> dict[str, list[str]]:
27
+ raw = scope.get("query_string", b"")
28
+ if isinstance(raw, str):
29
+ raw = raw.encode("utf-8")
30
+ if not isinstance(raw, (bytes, bytearray)):
31
+ return {}
32
+ return {
33
+ key: [str(item) for item in values]
34
+ for key, values in parse_qs(
35
+ bytes(raw).decode("utf-8"), keep_blank_values=True
36
+ ).items()
37
+ }
38
+
39
+
40
+ def _scheme(scope: Mapping[str, Any]) -> str:
41
+ return str(scope.get("scheme") or "").lower()
42
+
43
+
44
+ def build_asgi_channel(
45
+ env: Any,
46
+ *,
47
+ exchange: str = "request_response",
48
+ protocol: str | None = None,
49
+ framing: str | None = None,
50
+ ) -> OpChannel:
51
+ exchange = normalize_exchange(exchange)
52
+ scope = getattr(env, "scope", {}) or {}
53
+ scope_type = str(scope.get("type") or "http")
54
+ path = str(scope.get("path") or "/")
55
+ method = scope.get("method")
56
+ protocol_name = protocol or _scheme(scope) or scope_type
57
+ selector = path
58
+ if scope_type == "http" and isinstance(method, str):
59
+ selector = f"{method.upper()} {path}"
60
+ return OpChannel(
61
+ kind=_channel_kind(scope_type, exchange),
62
+ family=_channel_family(scope_type, exchange),
63
+ exchange=exchange,
64
+ protocol=protocol_name,
65
+ path=path,
66
+ method=str(method).upper() if isinstance(method, str) else None,
67
+ selector=selector,
68
+ framing=framing,
69
+ subevents=_subevents(scope_type, exchange),
70
+ headers=_headers(scope),
71
+ query=_query(scope),
72
+ path_params=scope.get("path_params", {}) or {},
73
+ send=getattr(env, "send", None),
74
+ receive=getattr(env, "receive", None),
75
+ )