The codegen consumes a schema from any backend and emits typed client code for any frontend — it doesn't belong inside a backend adapter. That placement was historical sediment from when there was only a Django backend; it predates the AFI generalization. New top-level slot: `protocol/` for protocol-level tooling. Tree is now: backends/ server protocol adapters frontends/ client kernel + per-framework adapters cores/ shared language-level primitives protocol/ protocol-level tooling workers/ runtime workers / bridges Codegen moves to `protocol/mizan-generate/`. Same file layout under `generator/` (cli.mjs, lib/), preserved via git mv. Package metadata cleaned up: - name: "generate" (placeholder) → "mizan-generate" - description filled in - type: module (cli.mjs is .mjs ESM, was previously declared "commonjs") - bin entry added so `npx mizan-generate --config <config.mjs>` works once the package is published, instead of `node path/to/cli.mjs`. Path-reference fixups: - backends/mizan-django/README.md: `node path/to/...` → `npx mizan-generate` - backends/mizan-fastapi/README.md: same - ISSUES.md: file paths in three issue entries - CLAUDE.md: codegen description + Package Layout section refreshed (added protocol/, mizan-fastapi entry, mizan-python entry) - docs/AFI_ARCHITECTURE.md: Package Layout refreshed identically Verified codegen runs from new location: regenerated the FastAPI example harness's api/ output, identical to pre-move. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
214 lines
5.3 KiB
Markdown
214 lines
5.3 KiB
Markdown
# mizan-django
|
|
|
|
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]"
|
|
# or with allauth integration:
|
|
uv add "mizan[channels,allauth]"
|
|
```
|
|
|
|
## Setup
|
|
|
|
```python
|
|
# settings.py
|
|
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
|
|
from django.urls import include, path
|
|
|
|
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 server functions
|
|
|
|
```python
|
|
# myapp/clients.py
|
|
from mizan.client import client
|
|
from mizan.setup 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")
|
|
```
|
|
|
|
Auto-discover `clients.py` modules from each Django app:
|
|
|
|
```python
|
|
# 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
|
|
```
|
|
|
|
## `@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(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/<id>/") # 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", 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`. 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}"
|
|
```
|
|
|
|
Frontend gets `useChatChannel({ room })`.
|
|
|
|
## Generate the frontend
|
|
|
|
The codegen is `mizan-generate` (in `protocol/mizan-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
|
|
npx mizan-generate --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
|
|
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`.
|