pyrpckit
Define your realtime API once in Python. Get the server, the contract, and typed clients for Python and TypeScript — none of which can drift apart.
Agents, browser automation, live dashboards and voice need more than request/response over HTTP: server-pushed events, binary streams, one long-lived connection. So the JSON-RPC envelope gets hand-written, the dispatch table grows by hand, and the frontend client is maintained separately — until the two disagree in production.
pyrpckit makes the Python definition the single source of truth:
| You write | pyrpckit gives you |
|---|---|
| an async function on a channel | validated dispatch, injection, concurrency, shutdown |
| a payload model | an OpenRPC contract as a build-time artifact |
| an error class | typed exceptions in every generated client |
| an async iterator | server-pushed events and binary streams |
| nothing else | Python and TypeScript clients, regenerated in CI |
The core has no HTTP or WebSocket dependency — a FastAPI adapter ships with it, and any transport you already have can serve a pyrpckit service.
The idea
You define each operation once, on the server:
@tasks.server.method()
async def create(params: CreateTask, store: Inject[TaskStore]) -> Task:
return await store.create(params.title)
@tasks.server.event()
async def updated(store: Inject[TaskStore]) -> AsyncIterator[TaskUpdated]:
async for task in store.watch():
yield TaskUpdated(task=task)
One command turns that into an OpenRPC document and clients in both languages:
pyrpckit generate --config rpcgen.toml
And your frontend gets the whole API fully typed — no schema written twice, no client kept in sync by hand, no stringly-typed method names:
const task = await client.tasks.create({ title: "Ship 0.6" }); // Task
for await (const update of client.tasks.updated()) { // TaskUpdated
render(update.task);
}
Run --check in CI and a definition that outgrew its clients fails the build
instead of shipping.
Install
uv add pyrpckit
uv add "pyrpckit[fastapi]" # FastAPI adapter
uv add "pyrpckit[codegen]" # client generation
Python 3.12 or newer. Pydantic is the only required dependency.
Quickstart
Channels group related operations and provide their namespace; a service mounts them on a socket. Their dispatch logic can also be called directly:
from pyrpckit import Inject, RpcChannel, RpcModel, RpcService, RpcSuccess
class CreateTask(RpcModel):
title: str
class Task(RpcModel):
id: int
title: str
class TaskStore:
def __init__(self) -> None:
self._tasks: list[Task] = []
async def create(self, title: str) -> Task:
task = Task(id=len(self._tasks) + 1, title=title)
self._tasks.append(task)
return task
tasks = RpcChannel("tasks")
@tasks.server.method()
async def create(params: CreateTask, store: Inject[TaskStore]) -> Task:
"""Create a task."""
return await store.create(params.title)
app = RpcService(version=1)
app.socket("/rpc", channels=(tasks,))
async def test_create() -> None:
server = tasks.create_server(context=TaskStore())
response = await server.handle(
{
"jsonrpc": "2.0",
"id": 1,
"method": "tasks.create",
"params": {"title": "Ship 0.6"},
}
)
assert isinstance(response, RpcSuccess)
assert response.result == Task(id=1, title="Ship 0.6")
tasks.create is the wire name, the docstring becomes the contract summary,
and Inject[TaskStore] is resolved on the server — it never appears in the
public schema.
Documentation
- Services and channels — methods, namespaces, parameter styles, sockets, protocol versions
- Dependency injection —
Inject[T], resolvers, scopes, Dishka - Connections and events — the
connecthook, rejecting handshakes, server-pushed events, limits - Typed errors — stable codes, typed details, generated exception classes
- Binary streams — upload, download, and bidirectional byte streams beside JSON-RPC
- Contract and clients —
rpcgen.toml, the CLI, the shape of generated clients - Transports — FastAPI, custom sockets, testing
Examples
examples/ holds standalone runnable scripts, and
examples/generated_clients contains real
generated Python and TypeScript output you can read before installing
anything.
Development
uv sync --all-groups
uv run ruff check .
uv run ruff format .
uv run pytest
Release files for pyrpckit 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyrpckit-0.7.0.tar.gz | 218.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyrpckit-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 333.7 kB
Release files / pyrpckit-0.7.0.tar.gz
| Download URL | pyrpckit-0.7.0.tar.gz |
|---|---|
| Size | 218.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ae2c927f26fd64564ff3f11a30ffe5515d485bcb66c2e06ef39b605d6ee6e2e6
|
|
BLAKE2b-256 checksum How to use checksums |
9ec0d3e6db4200a15e8188ea0ccbe63313cabc7a27bf3cd39f127f1d2aa37d14
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / pyrpckit-0.7.0-py3-none-any.whl
| Download URL | pyrpckit-0.7.0-py3-none-any.whl |
|---|---|
| Size | 115.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4363e1569d8aef1a9ef1ce8b73b69c1e2bb0c7ab69696a8db20688e7bf535526
|
|
BLAKE2b-256 checksum How to use checksums |
0629fc6a283227f67f57c1cd3f4b148ed5b3a48d800760e7c9b5a72964366dd0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|