Files
mizan/backends/mizan-django
Ryth Azhur 9900f8a36f Mizan IR: cut over to KDL, delete OpenAPI envelope
Replaces the transitional OpenAPI 3.0 + `x-mizan-*` extensions
substrate with the canonical Mizan IR as KDL, per docs/AFI_ARCHITECTURE.md:
"KDL is the contract; everything else (REST envelopes, OpenAPI
documents, framework idioms) is sediment around it."

End-to-end cutover. No transitional path left on main.

Forward direction:
  cores/mizan-python/src/mizan_core/ir.py
    build_ir() walks mizan_core.registry, introspects Pydantic
    models directly (no JSON-Schema indirection), and emits the
    Mizan IR document. The KDL grammar is locked in this file's
    module docstring.

Backends emit KDL:
  backends/mizan-fastapi/src/mizan_fastapi/ir.py
    `python -m mizan_fastapi.ir <module>` — CLI entry point.
  backends/mizan-django/.../management/commands/export_mizan_ir.py
    `manage.py export_mizan_ir` — Django mgmt command.

Codegen consumes KDL:
  protocol/mizan-codegen/Cargo.toml: + kdl = "6"
  protocol/mizan-codegen/src/ir.rs: NamedType { Struct/List/Enum/Alias }
    + TypeShape { Primitive/Ref/List/Optional/Enum/Union } sum types,
    replacing the JsonSchema sprawl. KDL parser walks the
    `kdl::KdlDocument` tree into typed Rust structs.
  protocol/mizan-codegen/src/fetch.rs: subprocess command switches
    to the new IR-export entry points.
  All emit modules (stage1 / react / python / rust / vue / svelte /
    channels) port their type-walkers from JsonSchema to the new
    sum types — case analysis collapses substantially.

Substrate-honesty wins beyond the moat closure:
  - `int | bool` multi-arm unions land as `TypeShape::Union` (was
    silently coerced to "string" before).
  - `<CamelName>Output = list[T]` returns emit as named alias
    types instead of struct-shaped wrappers, so consumer code
    `.map()` works directly on the type.
  - Pydantic field defaults flow through to `default` properties
    in KDL, then back to non-optional shape in every target.

Deleted:
  - backends/mizan-fastapi/src/mizan_fastapi/{cli,schema}.py
  - backends/mizan-django/.../export_mizan_schema.py
  - openapi-bearing half of mizan/export/__init__.py (edge
    manifest generator preserved — separate concern).
  - tests/afi/schema_normalizer.py
  - tests/fixtures/{afi_schema.json, channels_schema.json}
  - tests/fixtures/js_* baseline directories.

Verification:
  - 20 mizan-codegen unit tests green (IR deserialization,
    byte-equivalence parity across stage1/rust/python/react/vue/svelte
    against fresh KDL-driven baselines, channels structural).
  - tests/rust/run_wire_parity.py: 12/12 probes green driving
    the binary end-to-end through KDL.
  - Blazr studio-ui typechecks against the regenerated React client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:14:47 -04:00
..

mizan-django

Django backend adapter for the Mizan protocol. One decorator on a server function. Typed React client generated. Invalidation automatic.

Install

uv add "mizan[channels]"
# or with allauth integration:
uv add "mizan[channels,allauth]"

Setup

# 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)
# urls.py
from django.urls import include, path

urlpatterns = [
    path("api/mizan/", include("mizan.urls")),
]
# 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

# 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:

# 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

@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:

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:

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:

// 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",
}
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/.

// app.tsx
import { MizanContext } from "./api"

export default function App({ children }) {
    return <MizanContext baseUrl="/api/mizan">{children}</MizanContext>
}
// 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

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.