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:
@@ -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 <GreetProvider>
|
||||
@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")
|
||||
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 (`<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`.
|
||||
|
||||
Reference in New Issue
Block a user