Metadata-Version: 2.5
Name: fastapi-rpckit
Version: 0.1.0
Summary: Typed WebSocket RPC for FastAPI with OpenRPC and generated clients
Project-URL: Repository, https://github.com/mathisarends/canon
Project-URL: Source, https://github.com/mathisarends/canon/tree/main/packages/fastapi-rpckit
Project-URL: Documentation, https://github.com/mathisarends/canon/tree/main/packages/fastapi-rpckit/docs
Project-URL: Issues, https://github.com/mathisarends/canon/issues
Author-email: Mathis Arends <mathisarends27@gmail.com>
License-Expression: MIT
License-File: LICENSE
Requires-Python: <3.15,>=3.12
Requires-Dist: fastapi<1,>=0.115
Requires-Dist: pydantic<3,>=2.11
Provides-Extra: codegen
Requires-Dist: jinja2>=3.1; extra == 'codegen'
Provides-Extra: dishka
Requires-Dist: dishka>=1.7; extra == 'dishka'
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.30; extra == 'otel'
Description-Content-Type: text/markdown

# fastapi-rpckit

**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.

fastapi_rpckit makes the Python definition the single source of truth:

| You write | fastapi_rpckit 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 |

`fastapi-rpckit` is the FastAPI-first WebSocket RPC successor to
[`pyrpckit`](https://github.com/mathisarends/pyrpckit), developed in the
[Canon workspace](https://github.com/mathisarends/canon). FastAPI is a required
dependency. REST and SSE stay with FastAPI/Canon and `fastapi-streams`.
The existing dispatch engine, OpenRPC contracts, binary/raw channels, and client
generators are retained. The old repository remains available.

Version 0.1.0 is the initial successor release; see the [migration guide](docs/migration.md).

## The idea

You define each operation once, on the server:

```python
@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:

```bash
fastapi-rpckit 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:

```ts
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

```bash
uv add fastapi-rpckit
uv add "fastapi-rpckit[codegen]"  # client generation
uv add "fastapi-rpckit[otel]"     # OpenTelemetry integration
```

Python 3.12–3.14. FastAPI and Pydantic are required; Dishka, codegen, and
OpenTelemetry remain optional extras.

## Serve with FastAPI

```python
from fastapi import FastAPI
from fastapi_rpckit import RpcChannel, RpcService, create_router

greetings = RpcChannel("greetings")


@greetings.method("hello")
async def hello() -> str:
    return "Hello"


rpc = RpcService()
rpc.socket("/rpc", channels=(greetings,))
app = FastAPI()
app.include_router(create_router(rpc))
```

For feature-level mounting on an existing `APIRouter`, use `RpcRoutes`. For
Canon/Dishka composition, see the [three-transport example](../../examples/stack_service.py).
Both reuse the same WebSocket runtime; no second RPC router model is introduced.

## Quickstart

Channels group related operations and provide their namespace; a service mounts
them on a socket. Their dispatch logic can also be called directly:

```python
from fastapi_rpckit 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.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.

Operations on a channel are implemented by the server unless you say otherwise:
`@tasks.method()`, `@tasks.event()`, `@tasks.subscription()`, and
`@tasks.stream()` cover the usual client-to-server API. Only when the server
also calls into the client do you name both sides, `tasks.server` and
`tasks.client` — see [Client methods](docs/client-methods.md).

## Documentation

Read the [hardening stack and SDK migration guide](docs/hardening-overview.md) for all
15 ticket PRs, changed defaults, and integration steps.

- [Services and channels](docs/services.md) — methods, namespaces, parameter
  styles, sockets, protocol versions
- [Dependency injection](docs/dependencies.md) — `Inject[T]`, resolvers,
  scopes, Dishka
- [Connections and events](docs/connections-and-events.md) — the `connect`
  hook, rejecting handshakes, server-pushed events, limits
- [Typed errors](docs/errors.md) — stable codes, typed details, generated
  exception classes
- [Binary streams](docs/streams.md) — upload, download, and bidirectional byte streams beside JSON-RPC
- [Raw sockets](docs/raw-sockets.md) — proxy native protocols such as CDP with generated socket clients
- [Contract and clients](docs/clients.md) — `rpcgen.toml`, the CLI, the shape
  of generated clients
- [Transports](docs/transports.md) — FastAPI, custom sockets, testing

## Examples

[`examples/`](examples) holds standalone runnable scripts, and
[`examples/generated_clients`](examples/generated_clients) contains real
generated Python and TypeScript output you can read before installing
anything.

## Development

```bash
uv sync --all-groups
uv run ruff check .
uv run ruff format .
uv run pytest
```
