Backend adapter READMEs — DX surface + codegen invocation

mizan-django/README.md:
- Updated install path (was pointing at the old `subdirectory=django` git
  layout from before the backends/ restructure).
- Dropped the dead "monorepo root README" link (the root README was
  removed earlier in the substrate-restoration work).
- Fixed the apps.py example — convention is `clients.py` per MIZAN.md,
  not `mizan_clients.py`.
- Added the `mizan_clients()` auto-discovery pattern (it was missing).
- Added a Generate-the-frontend section: config shape + CLI invocation
  + the resulting <MizanContext>/use{Hook}() React surface.
- Tightened decorator-parameter overview to a single block covering the
  full @client surface.

mizan-fastapi/README.md (new):
- Mirrors mizan-django's structure for consistency.
- Opens with the AFI-common scope: forms/channels/shapes/SSR are out of
  scope on the FastAPI side; FastAPI projects use native equivalents.
- Setup shows app.add_exception_handler wiring for MizanError +
  RequestValidationError so every error surface goes through the same
  envelope the kernel parses.
- Calls out explicit register() (no AppConfig.ready() analog on FastAPI;
  registrations live in main.py or an imported clients.py).
- Auth-integration section explains the request.state.user middleware
  contract the executor expects.
- Codegen section shows the source.fastapi config shape that points at
  the new `python -m mizan_fastapi.cli <module>` schema export.
- Closes with pointers to AFI conformance + the e2e harness so a reader
  can verify the adapter's claims.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 00:10:01 -04:00
parent 255e10cb21
commit f0f7a93ed2
2 changed files with 334 additions and 32 deletions

View File

@@ -1,105 +1,213 @@
# mizan (Python) # mizan-django
Django server functions framework. See the [monorepo root](../README.md) for full documentation. Django backend adapter for the Mizan protocol. One decorator on a server
function. Typed React client generated. Invalidation automatic.
## Install ## Install
```bash ```bash
uv add "mizan[channels,allauth] @ git+https://git.impactsoundworks.com/isw/mizan.git#subdirectory=django" uv add "mizan[channels]"
# or with allauth integration:
uv add "mizan[channels,allauth]"
``` ```
## Setup ## Setup
```python ```python
# settings.py # settings.py
INSTALLED_APPS = ["mizan", ...] INSTALLED_APPS = ["mizan", "myapp", ...]
MIZAN_CACHE_SECRET = "..." # 32-byte HMAC signing key
MIZAN_CACHE_REDIS_URL = "redis://localhost:6379/0"
MIZAN_MWT_SECRET = "..." # MWT signing key (separate from cache + JWT)
```
```python
# urls.py # urls.py
path("api/mizan/", include("mizan.urls")) from django.urls import include, path
# asgi.py (optional, for WebSocket) urlpatterns = [
path("api/mizan/", include("mizan.urls")),
]
```
```python
# asgi.py — for WebSocket / Channels support
from django.core.asgi import get_asgi_application
from mizan import wrap_asgi from mizan import wrap_asgi
application = wrap_asgi(get_asgi_application()) application = wrap_asgi(get_asgi_application())
``` ```
## Define Functions ## Define server functions
```python ```python
# myapp/clients.py
from mizan.client import client from mizan.client import client
from mizan_core.registry import register from mizan.setup import register
from pydantic import BaseModel from pydantic import BaseModel
class Output(BaseModel):
class EchoOutput(BaseModel):
message: str message: str
@client @client
def echo(request, text: str) -> Output: def echo(request, text: str) -> EchoOutput:
return Output(message=text) return EchoOutput(message=text)
register(echo, "echo") register(echo, "echo")
``` ```
Register in `apps.py`: Auto-discover `clients.py` modules from each Django app:
```python ```python
def ready(self): # myapp/apps.py
import myapp.mizan_clients from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self) -> None:
from mizan.setup import mizan_clients
mizan_clients("myapp") # imports myapp/clients.py — triggers @client side effects
``` ```
## Auth ## `@client` parameters
```python ```python
@client(auth=True) # requires authentication @client # plain RPC function
@client(auth='staff') # requires is_staff @client(context="global") # singleton context — fetched once, SSR-hydrated
@client(auth='superuser') # requires is_superuser @client(context="user") # named context — fetched per provider mount
@client(auth=my_callable) # custom check @client(affects="user") # mutation — invalidates the user context
``` @client(affects=user_profile) # mutation — invalidates a specific function
@client(websocket=True) # WebSocket transport (requires channels)
## Contexts @client(auth=True) # requires authentication
@client(auth="staff") # requires is_staff
```python @client(auth="superuser") # requires is_superuser
@client(context='global') # fetched once, SSR-hydrated, becomes useCurrentUser() @client(auth=lambda req: ...) # custom predicate
@client(context='local') # fetched with params, becomes <GreetProvider> @client(route="/profile/<id>/") # view-path function (returns HttpResponse)
@client(rev=2) # cache revision (busts on bump)
``` ```
## Forms ## Forms
Django Forms become server functions + typed React hooks with Zod validation:
```python ```python
from django import forms
from mizan.forms import mizanFormMixin, mizanFormMeta from mizan.forms import mizanFormMixin, mizanFormMeta
class ContactForm(mizanFormMixin, forms.Form): class ContactForm(mizanFormMixin, forms.Form):
mizan = mizanFormMeta(name="contact", title="Contact Us") mizan = mizanFormMeta(name="contact", title="Contact Us", submit_label="Send")
name = forms.CharField()
email = forms.EmailField() name = forms.CharField()
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
def on_submit_success(self, request): def on_submit_success(self, request):
send_email(self.cleaned_data)
return {"sent": True} return {"sent": True}
``` ```
Auto-registers `contact.schema`, `contact.validate`, `contact.submit`. Generates `useContactForm()` with Zod validation. Auto-registers `contact.schema`, `contact.validate`, `contact.submit`. Frontend
gets `useContactForm()`.
## Channels ## Channels
WebSocket-native RPC via a flag flip:
```python ```python
from pydantic import BaseModel
from mizan.channels import ReactChannel from mizan.channels import ReactChannel
class ChatChannel(ReactChannel): class ChatChannel(ReactChannel):
class Params(BaseModel): class Params(BaseModel):
room: str room: str
class DjangoMessage(BaseModel): class DjangoMessage(BaseModel):
text: str text: str
user: str
def authorize(self, params): def authorize(self, params):
return self.user.is_authenticated return self.user.is_authenticated
def group(self, params): def group(self, params):
return f"chat_{params.room}" return f"chat_{params.room}"
``` ```
Generates `useChatChannel({ room })`. Frontend gets `useChatChannel({ room })`.
## Running Tests ## Generate the frontend
The codegen lives in `backends/mizan-django/generate/`. From your frontend
project, point a config at the Django backend and run the CLI:
```js
// frontend/django.config.mjs
import path from "path"
import { fileURLToPath } from "url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, "..")
export default {
source: {
django: {
managePath: path.join(root, "backend/manage.py"),
command: ["uv", "run", "python"],
env: {
PYTHONPATH: path.join(root, "backend"),
DJANGO_SETTINGS_MODULE: "myproject.settings",
},
},
},
output: "src/api",
}
```
```bash
node path/to/mizan-django/generate/generator/cli.mjs --config django.config.mjs
```
The codegen drives Django's management command (`export_mizan_schema`) under
the hood, 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 ```bash
uv sync --extra dev --extra channels uv sync --extra dev --extra channels
uv run pytest uv run pytest
``` ```
## Architecture
mizan-django is one of two reference backend adapters (the other is
`backends/mizan-fastapi`). Both implement the same Mizan protocol on top of
the shared `cores/mizan-python` core (`@client`, registry, MWT, HMAC cache
keys). See `docs/AFI_ARCHITECTURE.md`.

View File

@@ -0,0 +1,194 @@
# 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 lives in `backends/mizan-django/generate/` (the codegen package
is framework-agnostic; the directory name is historical). Point a config at
your FastAPI app and run the CLI:
```js
// frontend/fastapi.config.mjs
import path from "path"
import { fileURLToPath } from "url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, "..")
export default {
source: {
fastapi: {
module: "main", // module to import for @client side effects
cwd: path.join(root, "backend"), // python cwd for module resolution
command: ["uv", "run", "python"], // optional — defaults to ["python"]
},
},
output: "src/api",
}
```
```bash
node path/to/mizan-django/generate/generator/cli.mjs --config fastapi.config.mjs
```
The codegen drives `python -m mizan_fastapi.cli <module>` under the hood,
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.cli <module>
```
Imports the named module (which must register every `@client` function as
import-time side effects), then prints the OpenAPI schema as JSON to stdout.
Mirrors mizan-django's `manage.py export_mizan_schema` 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).