airbus-client 0.1.0__tar.gz
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.
- airbus_client-0.1.0/.gitignore +10 -0
- airbus_client-0.1.0/PKG-INFO +29 -0
- airbus_client-0.1.0/README.md +17 -0
- airbus_client-0.1.0/pyproject.toml +26 -0
- airbus_client-0.1.0/src/airbus_client/__init__.py +34 -0
- airbus_client-0.1.0/src/airbus_client/__main__.py +10 -0
- airbus_client-0.1.0/src/airbus_client/binary.py +20 -0
- airbus_client-0.1.0/src/airbus_client/contracts.py +5 -0
- airbus_client-0.1.0/src/airbus_client/endpoint.py +36 -0
- airbus_client-0.1.0/src/airbus_client/payloads.py +164 -0
- airbus_client-0.1.0/src/airbus_client/protocol.py +185 -0
- airbus_client-0.1.0/src/airbus_client/queue.py +125 -0
- airbus_client-0.1.0/src/airbus_client/rpc.py +441 -0
- airbus_client-0.1.0/src/airbus_client/server.py +57 -0
- airbus_client-0.1.0/tests/conftest.py +14 -0
- airbus_client-0.1.0/tests/test_endpoint.py +48 -0
- airbus_client-0.1.0/tests/test_hello.py +16 -0
- airbus_client-0.1.0/tests/test_protocol_guards.py +205 -0
- airbus_client-0.1.0/tests/test_queue.py +95 -0
- airbus_client-0.1.0/tests/test_rpc.py +357 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: airbus-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Airbus JSON-RPC event bus
|
|
5
|
+
Project-URL: Homepage, https://github.com/ghhwer/airbus
|
|
6
|
+
Project-URL: Repository, https://github.com/ghhwer/airbus
|
|
7
|
+
Author: Airbus contributors
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Requires-Dist: jsonschema>=4.26.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# airbus-client
|
|
14
|
+
|
|
15
|
+
Python SDK for the [Airbus](https://github.com/ghhwer/airbus) JSON-RPC event bus.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install airbus-client
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from airbus_client import RpcClient, Queue
|
|
23
|
+
|
|
24
|
+
client = RpcClient() # AIRBUS_URL / AIRBUS_HOST:AIRBUS_PORT
|
|
25
|
+
assert client.ping() == "pong"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The daemon is distributed separately (container image or `cargo`/`make build`).
|
|
29
|
+
See the [repository README](https://github.com/ghhwer/airbus) for run instructions.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# airbus-client
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Airbus](https://github.com/ghhwer/airbus) JSON-RPC event bus.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install airbus-client
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from airbus_client import RpcClient, Queue
|
|
11
|
+
|
|
12
|
+
client = RpcClient() # AIRBUS_URL / AIRBUS_HOST:AIRBUS_PORT
|
|
13
|
+
assert client.ping() == "pong"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The daemon is distributed separately (container image or `cargo`/`make build`).
|
|
17
|
+
See the [repository README](https://github.com/ghhwer/airbus) for run instructions.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "airbus-client"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client for the Airbus JSON-RPC event bus"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = ["jsonschema>=4.26.0"]
|
|
9
|
+
authors = [{ name = "Airbus contributors" }]
|
|
10
|
+
urls = { Homepage = "https://github.com/ghhwer/airbus", Repository = "https://github.com/ghhwer/airbus" }
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["hatchling"]
|
|
14
|
+
build-backend = "hatchling.build"
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["src/airbus_client"]
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"datamodel-code-generator==0.78.0",
|
|
22
|
+
"pytest>=9.1.1",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Public airbus_client API.
|
|
2
|
+
|
|
3
|
+
Import from this package root for application use. Wire ``*Params`` / ``*Result``
|
|
4
|
+
types live under ``airbus_client.payloads`` for raw RPC. ``protocol``, ``contracts``,
|
|
5
|
+
``binary``, and ``endpoint`` are operational internals.
|
|
6
|
+
|
|
7
|
+
Endpoint defaults: ``AIRBUS_URL`` or ``AIRBUS_HOST``/``AIRBUS_PORT`` (else
|
|
8
|
+
``127.0.0.1:9097``). ``RpcClient()`` and ``Queue(name)`` use that resolution;
|
|
9
|
+
pass ``client=`` for ephemeral test servers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from airbus_client.binary import run_airbus
|
|
13
|
+
from airbus_client.payloads import (
|
|
14
|
+
DispatchStrategy,
|
|
15
|
+
DuplexSide,
|
|
16
|
+
ListenerEventParams,
|
|
17
|
+
QueueMode,
|
|
18
|
+
)
|
|
19
|
+
from airbus_client.queue import Queue
|
|
20
|
+
from airbus_client.rpc import EventListener, RpcClient, RpcError
|
|
21
|
+
from airbus_client.server import start_server
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"DispatchStrategy",
|
|
25
|
+
"DuplexSide",
|
|
26
|
+
"EventListener",
|
|
27
|
+
"ListenerEventParams",
|
|
28
|
+
"Queue",
|
|
29
|
+
"QueueMode",
|
|
30
|
+
"RpcClient",
|
|
31
|
+
"RpcError",
|
|
32
|
+
"run_airbus",
|
|
33
|
+
"start_server",
|
|
34
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def airbus_bin() -> Path:
|
|
7
|
+
env = os.environ.get("AIRBUS_BIN")
|
|
8
|
+
if env:
|
|
9
|
+
return Path(env)
|
|
10
|
+
return Path(__file__).resolve().parents[3] / "out" / "airbus"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_airbus(*args: str, input_text: str | None = None) -> subprocess.CompletedProcess[str]:
|
|
14
|
+
return subprocess.run(
|
|
15
|
+
[str(airbus_bin()), *args],
|
|
16
|
+
input=input_text,
|
|
17
|
+
capture_output=True,
|
|
18
|
+
text=True,
|
|
19
|
+
check=True,
|
|
20
|
+
)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @generated by scripts/generate_payloads.py — do not edit by hand.
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
SCHEMAS = json.loads('{"add_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "items": {"type": "number"}, "maxItems": 2, "minItems": 2, "title": "AddParams", "type": "array"}, "add_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "title": "AddResult", "type": "number"}, "attach_listener_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"exhaustion_timeout_ms": {"minimum": 0, "type": "integer"}, "host": {"type": "string"}, "max_retries": {"minimum": 0, "type": "integer"}, "port": {"maximum": 65535, "minimum": 1, "type": "integer"}, "queue": {"$ref": "#/$defs/QueueName"}, "side": {"$ref": "#/$defs/DuplexSide", "description": "Listener side for full-duplex queues; required when mode is full-duplex, rejected otherwise"}}, "required": ["queue", "port"], "title": "AttachListenerParams", "type": "object"}, "attach_listener_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"listener_id": {"minLength": 1, "type": "string"}, "queue": {"$ref": "#/$defs/QueueName"}, "status": {"type": "string"}}, "required": ["listener_id", "queue", "status"], "title": "AttachListenerResult", "type": "object"}, "create_queue_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"dispatch_strategy": {"$ref": "#/$defs/DispatchStrategy", "description": "Fifo only; rejected for broadcast and full-duplex. Default when omitted on fifo: round_robin"}, "mode": {"$ref": "#/$defs/QueueMode"}, "queue": {"$ref": "#/$defs/QueueName"}}, "required": ["queue"], "title": "CreateQueueParams", "type": "object"}, "create_queue_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"created": {"type": "boolean"}, "mode": {"$ref": "#/$defs/QueueMode"}, "queue": {"$ref": "#/$defs/QueueName"}}, "required": ["queue", "mode", "created"], "title": "CreateQueueResult", "type": "object"}, "detach_listener_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"listener_id": {"minLength": 1, "type": "string"}}, "required": ["listener_id"], "title": "DetachListenerParams", "type": "object"}, "detach_listener_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"detached": {"type": "boolean"}, "listener_id": {"minLength": 1, "type": "string"}}, "required": ["listener_id", "detached"], "title": "DetachListenerResult", "type": "object"}, "list_listeners_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"queue": {"$ref": "#/$defs/QueueName"}}, "title": "ListListenersParams", "type": "object"}, "list_listeners_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"listeners": {"items": {"additionalProperties": false, "properties": {"active": {"type": "boolean"}, "failure_count": {"minimum": 0, "type": "integer"}, "host": {"type": "string"}, "id": {"minLength": 1, "type": "string"}, "mode": {"$ref": "#/$defs/QueueMode"}, "port": {"maximum": 65535, "minimum": 1, "type": "integer"}, "queue": {"$ref": "#/$defs/QueueName"}, "side": {"$ref": "#/$defs/DuplexSide", "description": "Present for full-duplex listeners"}}, "required": ["id", "queue", "host", "port", "mode", "failure_count", "active"], "type": "object"}, "type": "array"}}, "required": ["listeners"], "title": "ListListenersResult", "type": "object"}, "list_queues_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"queues": {"items": {"$ref": "#/$defs/QueueInfo"}, "type": "array"}}, "required": ["queues"], "title": "ListQueuesResult", "type": "object"}, "listener_event_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"event": {"$ref": "#/$defs/EventObject"}, "id": {"minLength": 1, "type": "string"}, "queue": {"$ref": "#/$defs/QueueName"}}, "required": ["queue", "id", "event"], "title": "ListenerEventParams", "type": "object"}, "listener_event_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"status": {"enum": ["ok"], "type": "string"}}, "required": ["status"], "title": "ListenerEventResult", "type": "object"}, "peek_events_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"count": {"default": 1, "type": "integer"}, "queue": {"$ref": "#/$defs/QueueName"}}, "required": ["queue"], "title": "PeekEventsParams", "type": "object"}, "peek_events_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"events": {"items": {"additionalProperties": false, "properties": {"event": {"$ref": "#/$defs/EventObject"}, "id": {"minLength": 1, "type": "string"}}, "required": ["id", "event"], "type": "object"}, "type": "array"}, "queue": {"$ref": "#/$defs/QueueName"}}, "required": ["queue", "events"], "title": "PeekEventsResult", "type": "object"}, "ping_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "title": "PingResult", "type": "string"}, "post_event_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"event": {"$ref": "#/$defs/EventObject"}, "queue": {"$ref": "#/$defs/QueueName"}, "side": {"$ref": "#/$defs/DuplexSide", "description": "Publisher side for full-duplex queues; required when mode is full-duplex, rejected otherwise"}}, "required": ["queue", "event"], "title": "PostEventParams", "type": "object"}, "post_event_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"id": {"minLength": 1, "type": "string"}, "queue": {"$ref": "#/$defs/QueueName"}}, "required": ["id", "queue"], "title": "PostEventResult", "type": "object"}, "queue_ready_params.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"queue": {"$ref": "#/$defs/QueueName"}}, "required": ["queue"], "title": "QueueReadyParams", "type": "object"}, "queue_ready_result.schema.json": {"$defs": {"DispatchStrategy": {"enum": ["round_robin", "single_node"], "title": "DispatchStrategy", "type": "string"}, "DuplexSide": {"enum": ["host", "device"], "title": "DuplexSide", "type": "string"}, "EventObject": {"additionalProperties": true, "title": "EventObject", "type": "object"}, "QueueInfo": {"additionalProperties": false, "properties": {"depth": {"minimum": 0, "type": "integer"}, "listener_count": {"minimum": 0, "type": "integer"}, "mode": {"$ref": "#/$defs/QueueMode"}, "name": {"$ref": "#/$defs/QueueName"}}, "required": ["name", "depth", "mode", "listener_count"], "title": "QueueInfo", "type": "object"}, "QueueMode": {"enum": ["broadcast", "fifo", "full-duplex"], "title": "QueueMode", "type": "string"}, "QueueName": {"minLength": 1, "title": "QueueName", "type": "string"}}, "additionalProperties": false, "properties": {"queue": {"$ref": "#/$defs/QueueName"}, "ready": {"description": "Mode-aware readiness: exists for fifo/broadcast; both duplex sides attached for full-duplex", "type": "boolean"}}, "required": ["queue", "ready"], "title": "QueueReadyResult", "type": "object"}}')
|
|
5
|
+
METHODS = json.loads('{"add": {"params": "add_params.schema.json", "params_required": true, "receiver": "server", "result": "add_result.schema.json"}, "attach_listener": {"params": "attach_listener_params.schema.json", "params_required": true, "receiver": "server", "result": "attach_listener_result.schema.json"}, "create_queue": {"params": "create_queue_params.schema.json", "params_required": true, "receiver": "server", "result": "create_queue_result.schema.json"}, "detach_listener": {"params": "detach_listener_params.schema.json", "params_required": true, "receiver": "server", "result": "detach_listener_result.schema.json"}, "list_listeners": {"params": "list_listeners_params.schema.json", "params_required": false, "receiver": "server", "result": "list_listeners_result.schema.json"}, "list_queues": {"params": null, "params_required": false, "receiver": "server", "result": "list_queues_result.schema.json"}, "on_event": {"params": "listener_event_params.schema.json", "params_required": true, "receiver": "listener", "result": "listener_event_result.schema.json"}, "peek_events": {"params": "peek_events_params.schema.json", "params_required": true, "receiver": "server", "result": "peek_events_result.schema.json"}, "ping": {"params": null, "params_required": false, "receiver": "server", "result": "ping_result.schema.json"}, "post_event": {"params": "post_event_params.schema.json", "params_required": true, "receiver": "server", "result": "post_event_result.schema.json"}, "queue_ready": {"params": "queue_ready_params.schema.json", "params_required": true, "receiver": "server", "result": "queue_ready_result.schema.json"}}')
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Resolve the Airbus TCP endpoint from the environment.
|
|
2
|
+
|
|
3
|
+
Precedence: ``AIRBUS_URL`` (``host:port``), else ``AIRBUS_HOST`` / ``AIRBUS_PORT``,
|
|
4
|
+
else ``127.0.0.1:9097``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
_DEFAULT_HOST = "127.0.0.1"
|
|
12
|
+
_DEFAULT_PORT = 9097
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_endpoint(url: str) -> tuple[str, int]:
|
|
16
|
+
"""Parse ``host:port`` into a host/port pair."""
|
|
17
|
+
host, _, port_s = url.rpartition(":")
|
|
18
|
+
if not host or not port_s.isdigit():
|
|
19
|
+
raise ValueError(f"Airbus endpoint must be host:port (got {url!r}); example: 127.0.0.1:9097")
|
|
20
|
+
return host, int(port_s)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def airbus_endpoint() -> tuple[str, int]:
|
|
24
|
+
"""Resolve host/port from ``AIRBUS_URL`` or ``AIRBUS_HOST`` / ``AIRBUS_PORT``."""
|
|
25
|
+
url = os.environ.get("AIRBUS_URL", "").strip()
|
|
26
|
+
if url:
|
|
27
|
+
return parse_endpoint(url)
|
|
28
|
+
host = os.environ.get("AIRBUS_HOST", _DEFAULT_HOST)
|
|
29
|
+
port = int(os.environ.get("AIRBUS_PORT", str(_DEFAULT_PORT)))
|
|
30
|
+
return host, port
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def airbus_url() -> str:
|
|
34
|
+
"""Return ``host:port`` for the resolved endpoint."""
|
|
35
|
+
host, port = airbus_endpoint()
|
|
36
|
+
return f"{host}:{port}"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @generated by scripts/generate_payloads.py — do not edit by hand.
|
|
2
|
+
# Source of truth: schema/payloads/*.schema.json
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
type PingResult = str
|
|
11
|
+
type AddResult = float
|
|
12
|
+
|
|
13
|
+
class QueueMode(StrEnum):
|
|
14
|
+
broadcast = 'broadcast'
|
|
15
|
+
fifo = 'fifo'
|
|
16
|
+
full_duplex = 'full-duplex'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DispatchStrategy(StrEnum):
|
|
20
|
+
round_robin = 'round_robin'
|
|
21
|
+
single_node = 'single_node'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DuplexSide(StrEnum):
|
|
25
|
+
host = 'host'
|
|
26
|
+
device = 'device'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class QueueInfo:
|
|
31
|
+
name: str
|
|
32
|
+
depth: int
|
|
33
|
+
mode: QueueMode
|
|
34
|
+
listener_count: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
type AddParams = list[float]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class PostEventParams:
|
|
42
|
+
queue: str
|
|
43
|
+
event: dict[str, Any]
|
|
44
|
+
side: DuplexSide | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class PostEventResult:
|
|
49
|
+
id: str
|
|
50
|
+
queue: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ListQueuesResult:
|
|
55
|
+
queues: list[QueueInfo]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class PeekEventsParams:
|
|
60
|
+
queue: str
|
|
61
|
+
count: int | None = 1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Event:
|
|
66
|
+
id: str
|
|
67
|
+
event: dict[str, Any]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class PeekEventsResult:
|
|
72
|
+
queue: str
|
|
73
|
+
events: list[Event]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class CreateQueueParams:
|
|
78
|
+
queue: str
|
|
79
|
+
mode: QueueMode | None = None
|
|
80
|
+
dispatch_strategy: DispatchStrategy | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class CreateQueueResult:
|
|
85
|
+
queue: str
|
|
86
|
+
mode: QueueMode
|
|
87
|
+
created: bool
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class AttachListenerParams:
|
|
92
|
+
queue: str
|
|
93
|
+
port: int
|
|
94
|
+
host: str | None = None
|
|
95
|
+
exhaustion_timeout_ms: int | None = None
|
|
96
|
+
max_retries: int | None = None
|
|
97
|
+
side: DuplexSide | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class AttachListenerResult:
|
|
102
|
+
listener_id: str
|
|
103
|
+
queue: str
|
|
104
|
+
status: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class DetachListenerParams:
|
|
109
|
+
listener_id: str
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class DetachListenerResult:
|
|
114
|
+
listener_id: str
|
|
115
|
+
detached: bool
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class ListListenersParams:
|
|
120
|
+
queue: str | None = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class Listener:
|
|
125
|
+
id: str
|
|
126
|
+
queue: str
|
|
127
|
+
host: str
|
|
128
|
+
port: int
|
|
129
|
+
mode: QueueMode
|
|
130
|
+
failure_count: int
|
|
131
|
+
active: bool
|
|
132
|
+
side: DuplexSide | None = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class ListListenersResult:
|
|
137
|
+
listeners: list[Listener]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class QueueReadyParams:
|
|
142
|
+
queue: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class QueueReadyResult:
|
|
147
|
+
queue: str
|
|
148
|
+
ready: bool
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class ListenerEventParams:
|
|
153
|
+
queue: str
|
|
154
|
+
id: str
|
|
155
|
+
event: dict[str, Any]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Status(StrEnum):
|
|
159
|
+
ok = 'ok'
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class ListenerEventResult:
|
|
164
|
+
status: Status
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""JSON-RPC envelopes and schema-backed payload codecs; no sockets or threads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import types
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import fields, is_dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any, get_args, get_origin, get_type_hints
|
|
11
|
+
|
|
12
|
+
from jsonschema import Draft202012Validator, ValidationError
|
|
13
|
+
|
|
14
|
+
from airbus_client import payloads
|
|
15
|
+
from airbus_client.contracts import METHODS, SCHEMAS
|
|
16
|
+
|
|
17
|
+
VERSION = "2.0"
|
|
18
|
+
_ABSENT = object()
|
|
19
|
+
_VALIDATORS = {name: Draft202012Validator(schema) for name, schema in SCHEMAS.items()}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RpcError(Exception):
|
|
23
|
+
def __init__(self, code: int, message: str, data: Any = None, *, id: int | str | None = None):
|
|
24
|
+
super().__init__(f"{code}: {message}")
|
|
25
|
+
self.code, self.message, self.data, self.id = code, message, data, id
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_wire(value: Any) -> Any:
|
|
29
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
30
|
+
# Omit absent optional fields, but preserve nulls inside user event dictionaries.
|
|
31
|
+
return {
|
|
32
|
+
field.name: to_wire(item)
|
|
33
|
+
for field in fields(value)
|
|
34
|
+
if (item := getattr(value, field.name)) is not None
|
|
35
|
+
}
|
|
36
|
+
if isinstance(value, Enum):
|
|
37
|
+
return value.value
|
|
38
|
+
if isinstance(value, dict):
|
|
39
|
+
return {key: to_wire(item) for key, item in value.items()}
|
|
40
|
+
if isinstance(value, list):
|
|
41
|
+
return [to_wire(item) for item in value]
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_payload(method: str, kind: str, value: Any) -> None:
|
|
46
|
+
if kind == "params" and value is None and not METHODS[method]["params_required"]:
|
|
47
|
+
return
|
|
48
|
+
schema = METHODS[method][kind]
|
|
49
|
+
if schema is None:
|
|
50
|
+
if value not in (None, {}):
|
|
51
|
+
raise ValueError(f"{method} takes no parameters")
|
|
52
|
+
return
|
|
53
|
+
try:
|
|
54
|
+
_VALIDATORS[schema].validate(value)
|
|
55
|
+
except ValidationError as error:
|
|
56
|
+
raise ValueError(f"{method} {kind}: {error.message}") from error
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _decode(model: Any, value: Any) -> Any:
|
|
60
|
+
if value is None:
|
|
61
|
+
return None
|
|
62
|
+
origin, args = get_origin(model), get_args(model)
|
|
63
|
+
if origin is types.UnionType:
|
|
64
|
+
model = next(choice for choice in args if choice is not type(None))
|
|
65
|
+
return _decode(model, value)
|
|
66
|
+
if origin is list:
|
|
67
|
+
return [_decode(args[0], item) for item in value]
|
|
68
|
+
if is_dataclass(model):
|
|
69
|
+
hints = get_type_hints(model)
|
|
70
|
+
return model(
|
|
71
|
+
**{
|
|
72
|
+
field.name: _decode(hints[field.name], value[field.name])
|
|
73
|
+
for field in fields(model)
|
|
74
|
+
if field.name in value
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
if isinstance(model, type) and issubclass(model, Enum):
|
|
78
|
+
return model(value)
|
|
79
|
+
if model is float:
|
|
80
|
+
return float(value)
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def decode_result(method: str, model: Any, value: Any) -> Any:
|
|
85
|
+
validate_payload(method, "result", value)
|
|
86
|
+
return _decode(model, value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def request(method: str, params: Any = None, id: Any = _ABSENT) -> dict[str, Any]:
|
|
90
|
+
document = {"jsonrpc": VERSION, "method": method}
|
|
91
|
+
if id is not _ABSENT:
|
|
92
|
+
document["id"] = id
|
|
93
|
+
if params is not None:
|
|
94
|
+
document["params"] = to_wire(params)
|
|
95
|
+
return document
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _valid_id(value: Any) -> bool:
|
|
99
|
+
return value is None or type(value) in (int, str)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_response(response: Any, expected_id: Any) -> dict[str, Any]:
|
|
103
|
+
if not isinstance(response, dict) or response.get("jsonrpc") != VERSION:
|
|
104
|
+
raise ValueError("invalid JSON-RPC response version")
|
|
105
|
+
if "id" not in response or not _valid_id(response["id"]):
|
|
106
|
+
raise ValueError("invalid JSON-RPC response id")
|
|
107
|
+
if type(response["id"]) is not type(expected_id) or response["id"] != expected_id:
|
|
108
|
+
raise ValueError("JSON-RPC response id does not match request")
|
|
109
|
+
if ("result" in response) == ("error" in response):
|
|
110
|
+
raise ValueError("response must contain exactly one of result or error")
|
|
111
|
+
if "error" in response:
|
|
112
|
+
error = response["error"]
|
|
113
|
+
if (
|
|
114
|
+
not isinstance(error, dict)
|
|
115
|
+
or type(error.get("code")) is not int
|
|
116
|
+
or not isinstance(error.get("message"), str)
|
|
117
|
+
):
|
|
118
|
+
raise ValueError("invalid JSON-RPC error object")
|
|
119
|
+
return response
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def response_result(response: dict[str, Any]) -> Any:
|
|
123
|
+
if "error" in response:
|
|
124
|
+
error = response["error"]
|
|
125
|
+
raise RpcError(error["code"], error["message"], error.get("data"), id=response["id"])
|
|
126
|
+
return response["result"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def batch_responses(requests: list[dict], response: Any) -> list[dict | None]:
|
|
130
|
+
expected = {item["id"] for item in requests if "id" in item}
|
|
131
|
+
if not expected:
|
|
132
|
+
if response is not None:
|
|
133
|
+
raise ValueError("notifications must not receive a response")
|
|
134
|
+
return [None] * len(requests)
|
|
135
|
+
if not isinstance(response, list):
|
|
136
|
+
raise ValueError("batch response must be an array")
|
|
137
|
+
by_id = {}
|
|
138
|
+
for item in response:
|
|
139
|
+
if not isinstance(item, dict) or not _valid_id(item.get("id")):
|
|
140
|
+
raise ValueError("invalid batch response")
|
|
141
|
+
id = item.get("id")
|
|
142
|
+
if id not in expected or id in by_id:
|
|
143
|
+
raise ValueError("unexpected or duplicate batch response id")
|
|
144
|
+
by_id[id] = validate_response(item, next(value for value in expected if value == id))
|
|
145
|
+
if set(by_id) != expected:
|
|
146
|
+
raise ValueError("missing batch response")
|
|
147
|
+
return [by_id[item["id"]] if "id" in item else None for item in requests]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _error_response(id: Any, code: int, message: str) -> dict[str, Any]:
|
|
151
|
+
return {"jsonrpc": VERSION, "id": id, "error": {"code": code, "message": message}}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def handle_event(body: str, callback: Callable[[dict[str, Any]], Any]) -> dict[str, Any] | None:
|
|
155
|
+
try:
|
|
156
|
+
document = json.loads(body)
|
|
157
|
+
except ValueError:
|
|
158
|
+
return _error_response(None, -32700, "invalid JSON")
|
|
159
|
+
if not isinstance(document, dict):
|
|
160
|
+
return _error_response(None, -32600, "invalid request")
|
|
161
|
+
id = document.get("id")
|
|
162
|
+
if (
|
|
163
|
+
document.get("jsonrpc") != VERSION
|
|
164
|
+
or not _valid_id(id)
|
|
165
|
+
or not isinstance(document.get("method"), str)
|
|
166
|
+
):
|
|
167
|
+
return _error_response(id if _valid_id(id) else None, -32600, "invalid request")
|
|
168
|
+
notification = "id" not in document
|
|
169
|
+
if document["method"] != "on_event":
|
|
170
|
+
return None if notification else _error_response(id, -32601, "method not found")
|
|
171
|
+
try:
|
|
172
|
+
validate_payload("on_event", "params", document.get("params"))
|
|
173
|
+
event = _decode(payloads.ListenerEventParams, document["params"])
|
|
174
|
+
except ValueError as error:
|
|
175
|
+
return None if notification else _error_response(id, -32602, str(error))
|
|
176
|
+
try:
|
|
177
|
+
callback(to_wire(event))
|
|
178
|
+
except Exception as error:
|
|
179
|
+
return None if notification else _error_response(id, -32603, str(error))
|
|
180
|
+
if notification:
|
|
181
|
+
return None
|
|
182
|
+
acknowledgment = payloads.ListenerEventResult(status=payloads.Status.ok)
|
|
183
|
+
result = to_wire(acknowledgment)
|
|
184
|
+
validate_payload("on_event", "result", result)
|
|
185
|
+
return {"jsonrpc": VERSION, "id": id, "result": result}
|