izana-django
Django backend adapter for the Izana protocol. One decorator on a server function. Typed React client generated. Invalidation automatic.
Install
uv add "django-izana[channels]"
The distribution is django-izana; the importable package is izana.
Setup
# settings.py
INSTALLED_APPS = ["izana", "myapp", ...]
IZANA_CACHE_SECRET = "..." # 32-byte HMAC signing key
IZANA_CACHE_REDIS_URL = "redis://localhost:6379/0"
IZANA_MWT_SECRET = "..." # MWT signing key (separate from cache + JWT)
# urls.py
from django.urls import include, path
urlpatterns = [
path("api/izana/", include("izana.urls")),
]
# asgi.py — for WebSocket / Channels support
from django.core.asgi import get_asgi_application
from izana import wrap_asgi
application = wrap_asgi(get_asgi_application())
Define server functions
# myapp/clients.py
from izana.client import client
from izana.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 izana.setup import izana_clients
izana_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", "cart"]) # mutation — invalidates several contexts
@client(merge="cart") # mutation — its result splices into the cart context's matching slot
@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 izana.forms import izanaFormMixin, izanaFormMeta
class ContactForm(izanaFormMixin, forms.Form):
izana = izanaFormMeta(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. The
codegen doesn't emit a hook for them; the frontend drives the three through
useIzanaFormCore({ name: "contact" }) from @eralyr/izana-react, which
binds to that package's IzanaProvider rather than the generated
IzanaContext kernel.
Auth-provider forms (django-allauth login, signup, MFA, WebAuthn) live in the
dedicated izana-allauth repository, built on this mixin.
Channels
WebSocket-native RPC via a flag flip. The message slots are named from the
client's point of view: ClientMessage travels client → server,
ServerMessage travels server → client. Declare only the directions the
channel uses.
from pydantic import BaseModel
from izana.channels import Channel
class ChatChannel(Channel):
class Params(BaseModel):
room: str
class ClientMessage(BaseModel):
text: str
class ServerMessage(BaseModel):
text: str
user: str
def authorize(self, params: Params) -> bool:
return self.user.is_authenticated
def group(self, params: Params) -> str:
return f"chat_{params.room}"
def receive(self, params: Params, msg: ClientMessage) -> ServerMessage:
return self.ServerMessage(text=msg.text, user=self.user.email)
Frontend gets useChatChannel({ room }). authorize, group, and
receive may be plain def and use the ORM freely — the consumer runs them
off the event loop, the same way it dispatches RPC — or async def, which it
awaits. Annotate the hooks with your own nested Params/ClientMessage as
above: the base declares them Any (it can't name classes you nest later),
so the annotation is what gives your editor the field names. A channel with
no Params writes params: None = None.
Server code outside a subscription broadcasts with push(). It builds a bare
channel instance (no user), so group() must be derivable from params
alone:
await ChatChannel.push(room="general", message=ChatChannel.ServerMessage(...))
Generate the frontend
The codegen is the izana-generate Rust binary (source at
protocol/izana-codegen/; protocol/izana-generate/ is a thin npm
launcher that dispatches to the platform binary). From your frontend
project, point a izana.toml at the Django backend and run the CLI:
# frontend/izana.toml
output = "src/api"
targets = ["react"]
[source.django]
manage_path = "../backend/manage.py"
command = ["uv", "run", "python"] # optional — defaults to ["python"]
[source.django.env]
PYTHONPATH = "../backend"
DJANGO_SETTINGS_MODULE = "myproject.settings"
izana-generate --config izana.toml
The codegen drives Django's management command (export_izana_ir) under
the hood, parses the emitted KDL IR, then emits Stage 1 (typed
callXxx/fetchXxx over the runtime kernel) + Stage 2 (<IzanaContext>
provider, per-context providers, use{Hook}() hooks) into src/api/.
// app.tsx
import { IzanaContext } from "./api"
export default function App({ children }) {
return <IzanaContext baseUrl="/api/izana">{children}</IzanaContext>
}
// 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
izana-django is one of two reference backend adapters (the other is
backends/izana-fastapi). Both implement the same Izana protocol on top of
the shared cores/izana-python core (@client, registry, MWT, HMAC cache
keys). See docs/AFI_ARCHITECTURE.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_izana-1.1.0.tar.gz.
File metadata
- Download URL: django_izana-1.1.0.tar.gz
- Upload date:
- Size: 156.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"CachyOS Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e816a3f28ad72671a47799411668c0eb46cb464a9dd122b55e993d0e4f277f79
|
|
| MD5 |
531f7eafac1513d832dbeac5c663c49d
|
|
| BLAKE2b-256 |
dab402daba430377d6e77b3ed02f96b3edfb6222f5319bc79b33875d7d95dc79
|
File details
Details for the file django_izana-1.1.0-py3-none-any.whl.
File metadata
- Download URL: django_izana-1.1.0-py3-none-any.whl
- Upload date:
- Size: 46.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.7 {"installer":{"name":"uv","version":"0.12.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"CachyOS Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d9f77c3d2464a52699e9bf0ada51e86454caf9ed1464f5e80c102a7f142a705
|
|
| MD5 |
fb8b300c93624e05d7588669d8fedbab
|
|
| BLAKE2b-256 |
23f3e92049cae81eccbb1536ed85d6e7efdb51a967568850510fa523f9c9fd7d
|