Files
openpilot-evo/openpilot/system/athena/rpc.py
T
Adeeb Shihadeh 60924556f6 athena: minimal json-rpc implementation (#38318)
* athena: minimal json-rpc implementation

* lil more
2026-07-09 16:55:49 -07:00

117 lines
3.2 KiB
Python

import json
from collections.abc import Callable, Mapping
from typing import Any
# a minimal implementation of json-rpc 2.0 https://www.jsonrpc.org/specification
JSONRPC_VERSION = "2.0"
# JSON-RPC 2.0 reserved / application error codes
PARSE_ERROR = -32700
INVALID_REQUEST = -32600
METHOD_NOT_FOUND = -32601
INVALID_PARAMS = -32602
SERVER_ERROR = -32000
JsonDict = dict[str, Any]
MethodMap = Mapping[str, Callable[..., Any]]
class Dispatcher(dict[str, Callable[..., Any]]):
def add_method(self, f: Callable[..., Any] | None = None, *, name: str | None = None):
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
self[name or fn.__name__] = fn
return fn
return decorator(f) if f is not None else decorator
dispatcher = Dispatcher()
def dumps_call(method: str, params: Any = None, request_id: Any = None) -> str:
msg: JsonDict = {"jsonrpc": JSONRPC_VERSION, "method": method, "id": request_id}
if params is not None:
msg["params"] = params
return json.dumps(msg)
def dumps_result(request_id: Any, result: Any) -> str:
return json.dumps({"jsonrpc": JSONRPC_VERSION, "id": request_id, "result": result})
def dumps_error(request_id: Any, message: str, code: int = SERVER_ERROR) -> str:
return json.dumps({
"jsonrpc": JSONRPC_VERSION,
"id": request_id,
"error": {"code": code, "message": message},
})
def loads(raw: str | bytes) -> JsonDict:
if isinstance(raw, bytes):
raw = raw.decode()
data = json.loads(raw)
if not isinstance(data, dict):
raise ValueError("message must be a JSON object")
return data
def is_call(msg: JsonDict) -> bool:
return "method" in msg
def is_response(msg: JsonDict) -> bool:
return "id" in msg and ("result" in msg or "error" in msg)
def error_message(err: Any) -> str:
"""Normalize JSON-RPC object errors and plain-string errors."""
if isinstance(err, str):
return err
if isinstance(err, dict):
data = err.get("data")
if isinstance(data, dict) and data.get("message"):
return str(data["message"])
if err.get("message") is not None:
return str(err["message"])
return str(err)
def _invoke(fn: Callable[..., Any], params: Any) -> Any:
if params is None:
return fn()
if isinstance(params, dict):
return fn(**params)
if isinstance(params, (list, tuple)):
return fn(*params)
raise TypeError("params must be a list, object, or omitted")
def handle(raw: str | bytes | JsonDict, methods: MethodMap | None = None) -> str:
methods = dispatcher if methods is None else methods
try:
msg = raw if isinstance(raw, dict) else loads(raw)
except (TypeError, ValueError, UnicodeDecodeError):
return dumps_error(None, "parse error", PARSE_ERROR)
if not is_call(msg):
raise ValueError("not a call")
req_id = msg.get("id")
name = msg.get("method")
if not isinstance(name, str):
return dumps_error(req_id, "invalid request", INVALID_REQUEST)
try:
fn = methods[name]
except KeyError:
return dumps_error(req_id, f"method not found: {name}", METHOD_NOT_FOUND)
try:
return dumps_result(req_id, _invoke(fn, msg.get("params")))
except TypeError as e:
return dumps_error(req_id, str(e), INVALID_PARAMS)
except Exception as e:
return dumps_error(req_id, str(e), SERVER_ERROR)