187 lines
5.6 KiB
Markdown
187 lines
5.6 KiB
Markdown
# mizan-fastapi
|
|
|
|
FastAPI backend adapter for the Mizan protocol. One decorator on a server
|
|
function. Typed React client generated. Invalidation automatic.
|
|
|
|
## Scope
|
|
|
|
mizan-fastapi targets the **AFI-common subset** — RPC dispatch, context
|
|
bundling, JSON-body invalidation, and auth gating. Forms, Channels, Shapes,
|
|
and SSR are out of scope for the FastAPI adapter — FastAPI projects use
|
|
native equivalents (Pydantic, native WebSockets, ORM-of-choice, FastAPI's
|
|
own SSR ecosystem).
|
|
|
|
## Install
|
|
|
|
```bash
|
|
uv add mizan-fastapi
|
|
```
|
|
|
|
## Setup
|
|
|
|
```python
|
|
# main.py
|
|
from fastapi import FastAPI
|
|
from fastapi.exceptions import RequestValidationError
|
|
|
|
from mizan_fastapi import (
|
|
MizanError,
|
|
mizan_exception_handler,
|
|
mizan_validation_handler,
|
|
router as mizan_router,
|
|
)
|
|
|
|
|
|
app = FastAPI()
|
|
app.include_router(mizan_router, prefix="/api/mizan")
|
|
app.add_exception_handler(MizanError, mizan_exception_handler)
|
|
app.add_exception_handler(RequestValidationError, mizan_validation_handler)
|
|
```
|
|
|
|
The exception handlers render every error path through the Mizan envelope
|
|
(`{"error": {"code", "message", "details"}}`) so the kernel's `MizanError`
|
|
parses status + code on the frontend regardless of which failure happened.
|
|
|
|
## Define server functions
|
|
|
|
```python
|
|
from mizan_core.client.function import client
|
|
from mizan_core.registry import register
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class EchoOutput(BaseModel):
|
|
message: str
|
|
|
|
|
|
@client
|
|
def echo(request, text: str) -> EchoOutput:
|
|
return EchoOutput(message=text)
|
|
|
|
|
|
register(echo, "echo")
|
|
```
|
|
|
|
mizan-fastapi has no auto-discovery (FastAPI doesn't have an app registry
|
|
to walk). Register every `@client`-decorated function explicitly. A typical
|
|
project keeps registrations in `main.py` (alongside the FastAPI app) or in
|
|
a dedicated `clients.py` imported during startup.
|
|
|
|
## `@client` parameters
|
|
|
|
```python
|
|
@client # plain RPC function
|
|
@client(context="global") # singleton context — fetched once, SSR-hydrated
|
|
@client(context="user") # named context — fetched per provider mount
|
|
@client(affects="user") # mutation — invalidates the user context
|
|
@client(affects=user_profile) # mutation — invalidates a specific function
|
|
@client(auth=True) # requires authentication
|
|
@client(auth="staff") # requires is_staff
|
|
@client(auth="superuser") # requires is_superuser
|
|
@client(auth=lambda req: ...) # custom predicate
|
|
@client(rev=2) # cache revision (busts on bump)
|
|
```
|
|
|
|
`websocket=True`, Forms, and Channels parameters are accepted by the
|
|
decorator (they're a `mizan-core` primitive) but ignored by mizan-fastapi —
|
|
those features only have effect when paired with mizan-django.
|
|
|
|
## Auth integration
|
|
|
|
The executor expects `request.state.user` to be populated by your FastAPI
|
|
middleware or dependency tree before dispatch:
|
|
|
|
```python
|
|
from fastapi import Request
|
|
|
|
|
|
@app.middleware("http")
|
|
async def attach_user(request: Request, call_next):
|
|
request.state.user = await resolve_user_from_token(request)
|
|
return await call_next(request)
|
|
```
|
|
|
|
Where `resolve_user_from_token` returns either a user object with
|
|
`is_authenticated`, `is_staff`, `is_superuser` attributes, or `None` for an
|
|
anonymous request. The executor branches on those for `auth=True`,
|
|
`auth="staff"`, `auth="superuser"` requirements.
|
|
|
|
## Generate the frontend
|
|
|
|
The codegen is the `mizan-generate` Rust binary (source at
|
|
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is a thin npm
|
|
launcher that dispatches to the platform binary). Point a `mizan.toml` at
|
|
your FastAPI app and run the CLI:
|
|
|
|
```toml
|
|
# frontend/mizan.toml
|
|
output = "src/api"
|
|
targets = ["react"]
|
|
|
|
[source.fastapi]
|
|
module = "main" # module to import for @client side effects
|
|
cwd = "../backend" # python cwd for module resolution
|
|
command = ["uv", "run", "python"] # optional — defaults to ["python"]
|
|
```
|
|
|
|
```bash
|
|
mizan-generate --config mizan.toml
|
|
```
|
|
|
|
The codegen drives `python -m mizan_fastapi.ir <module>` under the hood,
|
|
parses the emitted KDL IR, then emits Stage 1 (typed `callXxx`/`fetchXxx`
|
|
over the runtime kernel) + Stage 2 (`<MizanContext>` provider, per-context
|
|
providers, `use{Hook}()` hooks) into `src/api/`.
|
|
|
|
```tsx
|
|
// app.tsx
|
|
import { MizanContext } from "./api"
|
|
|
|
export default function App({ children }) {
|
|
return <MizanContext baseUrl="/api/mizan">{children}</MizanContext>
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// any component
|
|
import { useEcho, useCurrentUser } from "./api"
|
|
|
|
const echo = useEcho()
|
|
echo.mutate({ text: "hi" }).then(r => console.log(r.message))
|
|
|
|
const user = useCurrentUser() // global context — auto-fetched, auto-refreshed on mutation
|
|
```
|
|
|
|
## Running tests
|
|
|
|
```bash
|
|
uv sync --extra dev
|
|
uv run pytest
|
|
```
|
|
|
|
## Schema export CLI
|
|
|
|
For codegen consumption (or any tooling that wants the Mizan schema):
|
|
|
|
```bash
|
|
python -m mizan_fastapi.ir <module>
|
|
```
|
|
|
|
Imports the named module (which must register every `@client` function as
|
|
import-time side effects), then prints the Mizan KDL IR to stdout.
|
|
Mirrors mizan-django's `manage.py export_mizan_ir` so the codegen consumes
|
|
either backend the same subprocess way.
|
|
|
|
## Architecture
|
|
|
|
mizan-fastapi is one of two reference backend adapters (the other is
|
|
`backends/mizan-django`). Both implement the same Mizan protocol on top of
|
|
the shared `cores/mizan-python` core (`@client`, registry, MWT, HMAC cache
|
|
keys). The AFI conformance suite at `tests/afi/` gates that the two adapters
|
|
emit equivalent schemas for the same registered functions. See
|
|
`docs/AFI_ARCHITECTURE.md`.
|
|
|
|
A live e2e harness exercises this adapter end-to-end at
|
|
`examples/fastapi-react-site/` (real Chromium → React with generated hooks
|
|
→ FastAPI server, 14/14 Playwright tests).
|