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.method()
async def create(params: CreateTask, store: Inject[TaskStore]) -> Task:
return await store.create(params.title)
@tasks.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. Nothing here needs a running server to test:
from pyrpckit import Inject, RpcChannel, RpcModel, RpcService
from pyrpckit.testing import RpcTestClient
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.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:
async with RpcTestClient(app, "/rpc", context={TaskStore: TaskStore()}) as client:
assert await client.request("tasks.create", {"title": "Ship 0.6"}) == {
"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.6.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.6.0.tar.gz | 197.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyrpckit-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 306.7 kB
Release files / pyrpckit-0.6.0.tar.gz
| Download URL | pyrpckit-0.6.0.tar.gz |
|---|---|
| Size | 197.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4820c2b455a67a570db4568b4515fe9f90f817c5e43f251c7f0709c81db0f587
|
|
BLAKE2b-256 checksum How to use checksums |
65c263eec9f6a030236b8dc301b9490fdcb64e83f25577a9f9b659085a9b1e1f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / pyrpckit-0.6.0-py3-none-any.whl
| Download URL | pyrpckit-0.6.0-py3-none-any.whl |
|---|---|
| Size | 109.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3e48ae00f168d62ed0d4a88a118972e01782b11b4f3cfc1e9595ab4c540464b9
|
|
BLAKE2b-256 checksum How to use checksums |
d2299b7cece3b7b9c0001ae0a170d591da6f9dc0cd201c0fed17bd6bad7c3b12
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|