From f0f7a93ed2a22d3354a292cf29d5319b3f24965e Mon Sep 17 00:00:00 2001 From: Ryth Azhur Date: Sun, 10 May 2026 00:10:01 -0400 Subject: [PATCH] =?UTF-8?q?Backend=20adapter=20READMEs=20=E2=80=94=20DX=20?= =?UTF-8?q?surface=20+=20codegen=20invocation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /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 ` 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) --- backends/mizan-django/README.md | 172 ++++++++++++++++++++++----- backends/mizan-fastapi/README.md | 194 +++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+), 32 deletions(-) create mode 100644 backends/mizan-fastapi/README.md diff --git a/backends/mizan-django/README.md b/backends/mizan-django/README.md index 7738bf8..4dd7d4b 100644 --- a/backends/mizan-django/README.md +++ b/backends/mizan-django/README.md @@ -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 ```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 ```python # 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 -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 + application = wrap_asgi(get_asgi_application()) ``` -## Define Functions +## Define server functions ```python +# myapp/clients.py from mizan.client import client -from mizan_core.registry import register +from mizan.setup import register from pydantic import BaseModel -class Output(BaseModel): + +class EchoOutput(BaseModel): message: str + @client -def echo(request, text: str) -> Output: - return Output(message=text) +def echo(request, text: str) -> EchoOutput: + return EchoOutput(message=text) + register(echo, "echo") ``` -Register in `apps.py`: +Auto-discover `clients.py` modules from each Django app: ```python -def ready(self): - import myapp.mizan_clients +# myapp/apps.py +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 -@client(auth=True) # requires authentication -@client(auth='staff') # requires is_staff -@client(auth='superuser') # requires is_superuser -@client(auth=my_callable) # custom check -``` - -## Contexts - -```python -@client(context='global') # fetched once, SSR-hydrated, becomes useCurrentUser() -@client(context='local') # fetched with params, becomes +@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(websocket=True) # WebSocket transport (requires channels) +@client(auth=True) # requires authentication +@client(auth="staff") # requires is_staff +@client(auth="superuser") # requires is_superuser +@client(auth=lambda req: ...) # custom predicate +@client(route="/profile//") # view-path function (returns HttpResponse) +@client(rev=2) # cache revision (busts on bump) ``` ## Forms +Django Forms become server functions + typed React hooks with Zod validation: + ```python +from django import forms from mizan.forms import mizanFormMixin, mizanFormMeta + class ContactForm(mizanFormMixin, forms.Form): - mizan = mizanFormMeta(name="contact", title="Contact Us") - name = forms.CharField() - email = forms.EmailField() + mizan = mizanFormMeta(name="contact", title="Contact Us", submit_label="Send") + + name = forms.CharField() + email = forms.EmailField() + message = forms.CharField(widget=forms.Textarea) def on_submit_success(self, request): + send_email(self.cleaned_data) 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 +WebSocket-native RPC via a flag flip: + ```python +from pydantic import BaseModel from mizan.channels import ReactChannel + class ChatChannel(ReactChannel): class Params(BaseModel): room: str + class DjangoMessage(BaseModel): text: str + user: str def authorize(self, params): return self.user.is_authenticated + def group(self, params): 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 (`` provider, per-context providers, +`use{Hook}()` hooks) into `src/api/`. + +```tsx +// app.tsx +import { MizanContext } from "./api" + +export default function App({ children }) { + return {children} +} +``` + +```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 --extra channels 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`. diff --git a/backends/mizan-fastapi/README.md b/backends/mizan-fastapi/README.md new file mode 100644 index 0000000..751d07a --- /dev/null +++ b/backends/mizan-fastapi/README.md @@ -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 ` under the hood, +then emits Stage 1 (typed `callXxx`/`fetchXxx` over the runtime kernel) + +Stage 2 (`` provider, per-context providers, `use{Hook}()` +hooks) into `src/api/`. + +```tsx +// app.tsx +import { MizanContext } from "./api" + +export default function App({ children }) { + return {children} +} +``` + +```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 +``` + +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).