tigrbl-runtime 0.4.2.dev3__py3-none-any.whl → 0.4.3.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl_runtime/__init__.py +0 -27
- tigrbl_runtime/callbacks.py +113 -12
- tigrbl_runtime/channel/_asgi_completion.py +33 -0
- tigrbl_runtime/channel/_asgi_context.py +96 -0
- tigrbl_runtime/channel/_asgi_jsonrpc.py +38 -0
- tigrbl_runtime/channel/_asgi_receive.py +63 -0
- tigrbl_runtime/channel/_asgi_scope.py +75 -0
- tigrbl_runtime/channel/_asgi_send.py +242 -0
- tigrbl_runtime/channel/_asgi_webtransport.py +335 -0
- tigrbl_runtime/channel/asgi.py +72 -860
- tigrbl_runtime/executors/__init__.py +2 -0
- tigrbl_runtime/executors/_invoke_support.py +193 -0
- tigrbl_runtime/executors/ctx/__init__.py +15 -0
- tigrbl_runtime/executors/ctx/context.py +392 -0
- tigrbl_runtime/executors/ctx/hot.py +35 -0
- tigrbl_runtime/executors/ctx/hot_namespaces.py +421 -0
- tigrbl_runtime/executors/ctx/hot_state.py +226 -0
- tigrbl_runtime/executors/invoke.py +70 -174
- tigrbl_runtime/executors/kernel_executor.py +1 -1
- tigrbl_runtime/executors/packed.py +154 -603
- tigrbl_runtime/executors/types.py +34 -970
- tigrbl_runtime/handle.py +9 -1
- tigrbl_runtime/runtime/__init__.py +0 -7
- tigrbl_runtime/runtime/exceptions.py +2 -12
- tigrbl_runtime/runtime/runtime.py +35 -109
- tigrbl_runtime/runtime/system.py +2 -2
- tigrbl_runtime/rust/__init__.py +5 -12
- tigrbl_runtime/rust/_fallback.py +31 -278
- tigrbl_runtime/rust/_load_rust.py +3 -9
- tigrbl_runtime/rust/backend.py +25 -1
- tigrbl_runtime/rust/callbacks.py +16 -33
- tigrbl_runtime/rust/codec.py +18 -5
- tigrbl_runtime/rust/compile.py +5 -25
- tigrbl_runtime/rust/errors.py +27 -2
- tigrbl_runtime/rust/request.py +8 -0
- tigrbl_runtime/rust/response.py +8 -0
- tigrbl_runtime/rust/runtime.py +26 -91
- tigrbl_runtime/rust/trace.py +8 -43
- tigrbl_runtime/semantics.py +319 -0
- {tigrbl_runtime-0.4.2.dev3.dist-info → tigrbl_runtime-0.4.3.dev4.dist-info}/METADATA +121 -28
- tigrbl_runtime-0.4.3.dev4.dist-info/RECORD +58 -0
- tigrbl_runtime-0.4.3.dev4.dist-info/licenses/NOTICE +7 -0
- tigrbl_runtime/channel/state.py +0 -11
- tigrbl_runtime/executors/helpers.py +0 -3
- tigrbl_runtime/executors/loop_regions.py +0 -46
- tigrbl_runtime/protocol/__init__.py +0 -19
- tigrbl_runtime/protocol/_iterators.py +0 -3
- tigrbl_runtime/protocol/anchors.py +0 -38
- tigrbl_runtime/protocol/app_frame_codec.py +0 -111
- tigrbl_runtime/protocol/completion_fence.py +0 -41
- tigrbl_runtime/protocol/dispatch_atoms.py +0 -46
- tigrbl_runtime/protocol/framing_atoms.py +0 -50
- tigrbl_runtime/protocol/http_stream.py +0 -3
- tigrbl_runtime/protocol/http_unary.py +0 -8
- tigrbl_runtime/protocol/lifespan_chain.py +0 -3
- tigrbl_runtime/protocol/loop_modes.py +0 -24
- tigrbl_runtime/protocol/scope_schemas.py +0 -63
- tigrbl_runtime/protocol/sse.py +0 -3
- tigrbl_runtime/protocol/static_files.py +0 -3
- tigrbl_runtime/protocol/subevent_handlers.py +0 -3
- tigrbl_runtime/protocol/transport_atoms.py +0 -3
- tigrbl_runtime/protocol/websocket.py +0 -3
- tigrbl_runtime/protocol/webtransport.py +0 -9
- tigrbl_runtime/protocol/webtransport_session.py +0 -129
- tigrbl_runtime/runtime/events.py +0 -97
- tigrbl_runtime/runtime/executor/__init__.py +0 -6
- tigrbl_runtime/runtime/executor/invoke.py +0 -72
- tigrbl_runtime/runtime/kernel.py +0 -35
- tigrbl_runtime/runtime/labels.py +0 -3
- tigrbl_runtime/runtime/status/__init__.py +0 -1
- tigrbl_runtime/runtime/status/converters.py +0 -1
- tigrbl_runtime/runtime/status/exceptions.py +0 -1
- tigrbl_runtime/runtime/status/mappings.py +0 -1
- tigrbl_runtime/runtime/status/utils.py +0 -1
- tigrbl_runtime/rust/_parity_contract.py +0 -161
- tigrbl_runtime/rust/parity.py +0 -72
- tigrbl_runtime/transactions.py +0 -3
- tigrbl_runtime/webhooks.py +0 -116
- tigrbl_runtime-0.4.2.dev3.dist-info/RECORD +0 -79
- {tigrbl_runtime-0.4.2.dev3.dist-info → tigrbl_runtime-0.4.3.dev4.dist-info}/WHEEL +0 -0
- {tigrbl_runtime-0.4.2.dev3.dist-info → tigrbl_runtime-0.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
tigrbl_runtime/__init__.py
CHANGED
|
@@ -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)
|
tigrbl_runtime/callbacks.py
CHANGED
|
@@ -1,15 +1,116 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
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
|
+
)
|