pyrpckit
Decorator-driven, transport-agnostic JSON-RPC 2.0 protocols for Python.
Declare your API once on plain handler classes with Pydantic models. pyrpckit
derives the protocol from those declarations, validates and dispatches incoming
requests against it, and renders the same definition as JSON Schema and OpenRPC
so clients can be generated from it.
Declaring handlers
from enum import StrEnum
import pyrpckit as rpc
from pydantic import BaseModel
class AutomationRpcMethod(StrEnum):
LIST = "automation.list"
GET = "automation.get"
class GetAutomationParams(BaseModel):
automation_id: str
class AutomationResponse(BaseModel):
id: str
name: str
class AutomationNotFound(rpc.RpcError):
code = -32004
message = "Automation not found"
class AutomationRpcMethods(rpc.RpcHandler):
def __init__(self, service: AutomationService) -> None:
self._service = service
@rpc.method(AutomationRpcMethod.GET, errors=(AutomationNotFound,))
async def get_automation(self, params: GetAutomationParams) -> AutomationResponse:
"""Get an automation."""
job = await self._service.get(params.automation_id)
return AutomationResponse(id=job.id, name=job.name)
Handler classes inherit from RpcHandler. A decorated method must accept exactly
self and one Pydantic params model, and must annotate its return type with a
Pydantic model or None. Violations are reported as a
ProtocolDefinitionError when the protocol is assembled — never at request time.
The summary is optional: without one, the first line of the docstring is used,
and a method with neither simply carries no summary into the generated contract.
Assembling the protocol
Group handlers into features, then combine the features into one protocol. The feature name tags its methods in the generated contract:
AUTOMATION = rpc.feature("automation", handlers=(AutomationRpcMethods,))
protocol = rpc.RpcProtocol(AUTOMATION, version=1)
Features are worth it once the API has more than one area to group. For a small protocol, or for a quick round-trip test, assemble one straight from the handler classes:
protocol = rpc.RpcProtocol.of(AutomationRpcMethods, version=1)
Serving requests
RpcServer turns a decoded JSON payload into a response envelope, so it fits any
transport — WebSocket, HTTP, stdio, a message queue:
server = rpc.RpcServer(AutomationRpcMethods(service))
response = await server.handle(await socket.receive_json())
if response is not None:
await socket.send_json(response.model_dump(mode="json"))
The protocol is derived from the handlers you pass, so nothing is registered
twice. Pass protocol= to serve one you assembled yourself; the server then
checks the two against each other.
Unknown methods, malformed envelopes, and invalid params become the matching JSON-RPC failures.
Reporting errors
A handler reports a failure by raising an RpcError subclass. It goes on the wire
with the code and message it declares — the same class the method lists in
errors=, so the contract and the implementation cannot drift:
raise AutomationNotFound(f"No automation {params.automation_id}")
Exceptions you cannot make into an RpcError — from a library, say — are
translated by an optional error_mapper:
def to_rpc_error(error: Exception) -> rpc.RpcError | None:
if isinstance(error, HttpxTimeout):
return rpc.RpcError("Upstream timed out", code=-32005)
return None
server = rpc.RpcServer(AutomationRpcMethods(service), error_mapper=to_rpc_error)
Anything neither declared nor mapped becomes an internal error, so handler internals never leak to clients.
RpcDispatcher is available if you would rather build responses yourself: it
exposes parse_request and execute and raises the errors above.
Server-initiated notifications
Notifications carry a payload that is either a decorated event model or a union of
them. Each event pins a type field to a literal, so clients can narrow the
union — and that literal is the event name:
@rpc.event
class AutomationStarted(BaseModel):
type: Literal["automation.started"] = "automation.started"
automation_id: str
type AutomationEvent = AutomationStarted | AutomationFinished
AUTOMATION = rpc.feature(
"automation",
handlers=(AutomationRpcMethods,),
notifications=(
rpc.notification(
"automation.event",
AutomationEvent,
summary="Publish an automation lifecycle event.",
),
),
)
Send one with the RpcNotification envelope.
Generating the contract
from pyrpckit.schema import render_json_schema, render_openrpc
render_json_schema(protocol, title="Automation Protocol")
render_openrpc(
protocol,
title="Automation",
servers=({"name": "local", "url": "ws://127.0.0.1:8000/rpc"},),
)
The JSON Schema document lists every frame on the wire — one request schema per
method, the success and failure envelopes, and one envelope per notification —
under a single oneOf, and indexes the protocol in x-rpc-methods,
x-rpc-notifications, and x-rpc-events. The OpenRPC document describes the same
methods with their summaries and declared errors, and tags each one with the
feature it came from.
Generating a client
The OpenRPC document is the input to the client generator. It writes a typed, ready-to-use package into the repository that consumes the API:
pyrpckit generate python schema/greeting.openrpc.json --output src/greeting_client
The generated package holds no hand-written code and is meant to be committed:
models.py— every schema as a Pydantic model, plus anRpcMethodenumnamespaces/<name>.py— one class per method prefix, one typedasync defper methodclient.py— the facade that wires the namespaces together, plus the typed notification stream__init__.py— the package exports
async with GreetingClient(transport) as client:
greeting = await client.greeting.say(name="Mathis") # -> SayResult
async for notification in client.notifications(): # -> GreetingChangedNotification
print(notification.params)
Only the schemas the client actually reaches are emitted — request and response
envelopes stay out of the generated models. The transport is not generated: the
client is constructed with anything satisfying the pyrpckit.client.RpcTransport
protocol, so it works over a WebSocket, HTTP, or a queue.
Run the generator with --check in CI to fail the build when the committed
client no longer matches the server schema:
pyrpckit generate python schema/greeting.openrpc.json --output src/greeting_client --check
Development
Small, direct library examples live in examples/. For a complete,
runnable server-to-generated-client walkthrough, see the
FastAPI showcase.
This project uses uv for dependency management.
uv sync --all-groups
uv run pre-commit install
uv run ruff check .
uv run ruff format .
uv run pytest
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyrpckit-0.1.0.tar.gz.
File metadata
- Download URL: pyrpckit-0.1.0.tar.gz
- Upload date:
- Size: 77.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66ae55904fd9457a8f52aa0ef69b36c397e0acb49f25acc8aa7898512a1e0b05
|
|
| MD5 |
45fe9738702cc9ed2d0518c514fd51e3
|
|
| BLAKE2b-256 |
8e73c56f9d96f453ffc7d535ff6facb685dcace5f2b965fa78e1130e79934dfc
|
File details
Details for the file pyrpckit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyrpckit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a291f3488d83aa653029a641b19d4ad69ab18d6ce480428a7324fa1cab4090f0
|
|
| MD5 |
b106651efd3de7dbb0ad25efb5c6c7bb
|
|
| BLAKE2b-256 |
1d49d517de30754af614ee7926139d9ac489b0c3ed43f7dfc09f3e4d0bf69974
|