Events
Event handlers declared on components: the class Events: contract, the typed base for it, and the built-in citry.ext.events extension that owns it.
Events class
Bases: ExtensionConfig, Generic
Optional typed base for a component's Events class.
A component's class Events: needs no base class: the built-in events extension rebuilds it on this class either way, so subclassing is purely a typing aid and changes nothing at runtime. Subscript the base with the component's State class to type self.state for editors and type checkers (mypy and pyright):
Example
import citry from citry import Component class TodoState: project_id: int query: str = "" class TodoList(Component): State = TodoState class Events(citry.Events[TodoState]): def refresh(self) -> None: self.state.query # typed as strOn a component with no State class, subclass the bare base:
self.stateis then typed (and is)None.
Attributes
stateStateT- The component's State instance for the call being handled; ``None`` when the component declares no State class.contextAny- Whatever the ``_context`` hook returned for the call being handled; ``None`` when no hook is configured.requestRouteRequest- A framework-neutral view of the request that carried the call ([`RouteRequest`](/reference/web/#citry-routerequest)).eventAny- Metadata about the call being handled.
X_CITRY_EVENTS_HEADER attribute
Header required on JSON Events calls as part of the same-origin CSRF check.
CallEvent class
The event value injected into event handlers: metadata about one call.
Attributes
namestr- The handler's wire name the call addressed.instance_idstr | None- The calling instance's render id, or ``None`` for an instance-less call (an API client, a hand-written form).transportstr- The transport that carried the call (``"http"``, ...).argsMapping[str, Any]- The raw, unvalidated wire args payload. Guards read this when they need payload values, because one guard covers handlers with different schemas (design 3.5).
EventError class
Bases: Exception
A structured error an event handler or guard raises to answer a call.
The client receives the status, a stable string code derived from it, the message as the human summary (a toast or banner), and fieldErrors as the per-field error map surfaced next to inputs ($error.fieldErrors). Raising it anywhere in a handler, a guard, or the _context hook turns the call into that error response; it never becomes a host 500.
Example
from citry.ext.events import EventError class DocumentEditor(Component): class State: document_id: int class Events: def _guard(self): user = user_from_request(self.request) if can_edit(user, self.state.document_id): return raise EventError( "You cannot edit this document.", status=403, ) def subscribe(self, data: SubscribeIn): if is_taken(data.email): raise EventError( "Please fix the errors below.", fields={"email": "This address is already subscribed."}, )
Attributes
message- The human summary of the error.fieldsdict[str, str]- Per-field error messages, keyed by data-schema field name (e.g. ``{"email": "Enter a valid email address."}``). Empty when the error is not about specific fields.status- The HTTP status of the error (400 to 599). ``422`` (the default) is a validation failure; ``403``, ``404``, and ``409`` cover forbidden, not found, and conflict.
code attribute
code: strThe stable wire code for the error's status (e.g. "forbidden" for 403).
EventRequest class
The request value injected into event handlers.
The framework-neutral request fields (design 3.3), always populated: the HTTP routes fill everything; other transports fill what they carry. The untouched host object stays reachable as native, and event.transport says which transport built it.
Attributes
methodstr- The HTTP method, uppercase.pathstr- The full URL path of the request.queryMapping[str, tuple[str, ...]]- The query-string parameters; each key maps to every value it was sent with, in order.headersMapping[str, str]- The request headers; lookups ignore case.bodybytes- The raw request body.content_typestr- The ``Content-Type`` header value, or ``""``.formMapping[str, Any]- The parsed form fields of a form post (urlencoded today), mapping field name to its string value (or list of values for a repeated field); empty for non-form requests.filesMapping[str, Any]- Uploaded files by field name when the selected payload codec or transport supplies them; otherwise empty.nativeAny- The untouched host request object.
EventsDispatcher class
Runs call envelopes through the events pipeline and answers result envelopes.
Stateless: everything a dispatch needs rides in the envelope and the TransportContext, so one instance (or a fresh one per call) serves every transport. The HTTP routes own the built-in usage; a custom transport (a GraphQL mutation resolver, say) decodes its request into the call envelope of design 4.2 and calls dispatch (or its async twin) directly.
dispatch function
dispatch(envelope: Mapping[str, Any], ctx: TransportContext, request: EventRequest | None = None, url_component: str | None = None, url_event: str | None = None, csrf_check: Callable[[EventHandler], None] | None = None) -> dict[str, Any] | RouteResponseDispatch a call envelope synchronously and return the result envelope.
This is the plain-def pipeline WSGI and sync Django hosts run. An async def event handler is answered with the 500 error naming the fix (dispatch through dispatch_async, which an async transport calls); it is never silently run on a private event loop.
Parameters
envelopeMapping[str, Any]- The decoded call envelope (design 4.2).ctxTransportContext- What the transport knows about the request.requestEventRequest | None- The neutral request injected into handlers as ``request``; ``None`` builds an empty one carrying ``ctx.host_request`` as ``native``.url_componentstr | None- The class id the per-event URL names; when given, the URL is authoritative and a differing body field is rejected. ``None`` on the batch endpoint.url_eventstr | None- The event wire name the per-event URL names. A raw response additionally requires HTTP and ``@event(bundle=False)``.csrf_checkCallable[[EventHandler], None] | None- The transport's per-call CSRF check, called with the resolved handler; raise to reject the call as ``csrf_failed``. ``None`` skips the check (a transport with its own protection, or a direct caller).
Returns
dict[str, Any] | RouteResponse: The result envelope (design 4.3), or the handler's
dispatch_async function
dispatch_async(envelope: Mapping[str, Any], ctx: TransportContext, request: EventRequest | None = None, url_component: str | None = None, url_event: str | None = None, csrf_check: Callable[[EventHandler], None] | None = None) -> dict[str, Any] | RouteResponseDispatch a call envelope from async code.
The one behavioral difference from dispatch: async def event handlers are awaited on the running loop, and sync handlers are offloaded to a worker thread (citry.util.routing.call_maybe_sync) so they cannot block it.
Parameters
envelopeMapping[str, Any]- The decoded call envelope.ctxTransportContext- What the transport knows about the request.requestEventRequest | None- The neutral request injected into handlers, or ``None`` to build one from ``ctx.host_request``.url_componentstr | None- The class id named by a per-event URL, or ``None`` for a batch or custom transport.url_eventstr | None- The event name named by a per-event URL. A raw response additionally requires HTTP and ``@event(bundle=False)``.csrf_checkCallable[[EventHandler], None] | None- A callable that raises to reject a resolved handler, or ``None`` when the transport provides its own protection.
Returns
dict[str, Any] | RouteResponse: The result envelope, or the handler's ``RouteResponse`` when the
EventsExtension class
Bases: Extension
Built-in extension that validates, serves, and renders component Events.
Every Citry instance installs this extension. It validates a component's Events and State declarations when the component class is created, contributes the Events HTTP routes, prepares declarative bindings, and emits the browser runtime data needed by rendered instances.
Most applications use the nested class Events API described in Server events and never instantiate this class directly. Extension and tooling authors can retrieve it from the instance's ExtensionManager to inspect resolved handler or State metadata.
validate_config_fields function
Check declared Events config fields against the two-tier rule.
Underscore names are configuration and must be one of the recognized config attributes (_guard, _context, _csrf, _methods, _debounce, _throttle, _topics, plus the engine-wide _max_envelope_bytes, which only extensions_defaults may set); on a component's Events class an underscore def is a private helper and is exempt. Unprefixed names are event handlers: they belong on a component's nested Events class, so they are rejected in extensions_defaults (an event handler cannot be defaulted globally), and on the component they must be plain methods defined with def, which is exactly what handler enumeration collects (design events.md 3.1). A staticmethod or classmethod passes here so enumeration can reject it with its own pointed error; anything else (a property, a functools.partial, a plain value) fails here rather than sit on Events as silently neither handler nor config. Citry calls this at engine construction (for the setting) and at component class definition; see Extension.validate_config_fields.
Parameters
fieldsMapping[str, Any]- The declared fields, mapping field name to declared value.componenttype[Component] | None- The component class the fields were declared on, or ``None`` when they come from the ``extensions_defaults`` setting.
Raises
ValueError- For an unrecognized underscore name (with a "did you mean" hint), an event handler in ``extensions_defaults``, or a handler value that is not a plain function.
on_component_class_created function
on_component_class_created(ctx: OnComponentClassCreatedContext) -> NoneValidate and record one component's Events and State declarations.
inspect_component function
inspect_component(ctx: ComponentIntrospectionContext) -> dict[str, object] | NoneReturn public, JSON-safe Events metadata for component introspection.
Parameters
ctxComponentIntrospectionContext- The component-introspection request.
Returns
dict[str, object] | None: Versioned handler metadata, or ``None`` when the component has no
Raises
RuntimeError- When the component was not recorded at class creation.
on_template_loaded function
on_template_loaded(ctx: OnTemplateLoadedContext) -> strValidate and compile literal @c-* and :c-* bindings.
Parameters
ctxOnTemplateLoadedContext- The template-load context carrying the component and source.
Returns
str: The template source with its Events bindings compiled.
Raises
ValueError- When a binding names an unknown handler or State field, or uses an invalid modifier combination.
on_attrs_resolved function
on_attrs_resolved(ctx: OnAttrsResolvedContext) -> dict[str, Any] | NoneValidate and compile @c-* and :c-* bindings from dynamic attrs.
Parameters
ctxOnAttrsResolvedContext- The resolved-attribute context for one HTML element.
Returns
dict[str, Any] | None: Updated attributes, or ``None`` when the element has no Events
on_component_data function
on_component_data(ctx: OnComponentDataContext) -> NonePrepare one rendered Events instance for browser activation.
Parameters
ctxOnComponentDataContext- The component-data context for the instance being rendered.
on_render_context_merge function
on_render_context_merge(ctx: OnRenderContextMergeContext) -> NoneMerge child Events records into their parent render context.
stage_render_cache function
stage_render_cache(ctx: OnRenderCacheStageContext) -> StagedRenderCacheContribution on_dependencies function
on_dependencies(ctx: OnDependenciesContext) -> NoneAdd the Events runtime and instance data to serialized dependencies.
Parameters
ctxOnDependenciesContext- The dependencies context for the render being serialized.
two_way_binding_targets function
The State fields bound two-way in a component's template.
Populated when the template first loads (the stage-one binding rewrite), so it is empty for a component whose template has not been loaded yet or that has no two-way bindings. Each individual target was already validated against _model during the rewrite; this aggregate is exposed for diagnostics and introspection.
Parameters
Returns
frozenset[str]: The two-way bound State field names (empty when none).
build_state function
Build the State instance for the component instance being rendered.
Uses the component's own state_data(kwargs, slots) when it defines one (returning the State instance or a dict for it); otherwise derives the State from same-named kwargs, with State-field defaults filling the gaps.
Parameters
componentComponent- The component instance being rendered.
Returns
Any: The State instance, or ``None`` when the component declares no
Raises
ValueError- When a State field has neither a matching kwarg nor a default, or when ``state_data()`` returns something other than the State or a dict.
TransportContext class
What a transport tells the dispatcher about the request it decoded.
One is built per request by the transport (the HTTP routes build it in citry.ext.events.routes; a custom transport builds its own) and passed to EventsDispatcher.dispatch.
Attributes
transportstr- The transport's name (``"http"``, later ``"ws"``); handlers see it as ``event.transport``.citryCitry- The engine the call dispatches against.host_requestAny- The untouched host request object (Django's ``HttpRequest``, the ASGI scope, a WS connection); ``None`` when the transport has none.headersMapping[str, str]- A case-insensitive view of the request headers; empty when the transport carries none.response_modeLiteral['wire', 'compat']- ``"wire"`` for an ordinary result envelope, or ``"compat"`` when the HTTP transport will turn one call into a browser-native HTML, redirect, JSON, or empty response.
UploadedFile class
One file received by an event handler, independent of the web framework.
Payload codecs and custom transports can construct this neutral wrapper so handler code does not depend on a host framework's file type. Reading is synchronous: plain handlers run in a worker thread under ASGI, so a plain read() cannot stall the event loop.
Citry's built-in payload codecs do not parse multipart request bodies. Use this type with a custom codec or transport that supplies the file values.
Example
class AvatarIn: avatar: UploadedFile class Profile(Component): class Events: def upload_avatar(self, data: AvatarIn): data.avatar.save(MEDIA_DIR / f"{uuid4()}.png")
Attributes
filename- The file name the client sent (never a trusted path).size- The file size in bytes.content_type- The content type the client sent, or ``None`` when the request did not carry one.native- The host framework's own file object (e.g. Django's ``UploadedFile`` or Starlette's ``UploadFile``), for the rare case a handler needs a host-specific capability; ``None`` when there is no host object.
read function
Read the file's content, synchronously.
A full read (no size) always returns the whole content: a seekable file is rewound first, so calling read() twice returns the content twice. A partial read(n) reads the next n bytes from the current position, like any Python file.
Parameters
sizeint- How many bytes to read; the whole content when negative.
Returns
bytes: The bytes read.
ViewEvents class
Bases: Events
Base class for verb-shaped Events classes, for ports from view code.
Subclass it as a component's class Events(ViewEvents): and the seven HTTP verb names (get, post, put, patch, delete, head, options) become reserved handler names: each one accepts exactly its own HTTP method, and the extra route ext/events/e/{class_id} dispatches to it from the request method alone, so a form can post to the component URL with no event name in it. Existing handler bodies still need to replace host-specific request parsing and responses with typed data, the neutral request, and Events return values.
The verb compatibility route does not require a State token, and verb handlers cannot inject state. A named runtime call to a verb on a State-declaring component may still carry the component token. Stateful work belongs in named event handlers, which can live on the same class.
Prefer naming events after actions: once a component has more than one mutation, one post that inspects the payload is the multiplexing this extension exists to remove. Keep the verbs for the initial port, then split them into named handlers (save, archive, ...) as the component grows.
Example
from citry import Component from citry.ext.events import ViewEvents class ContactIn: email: str = "" message: str = "" class ContactForm(Component): class Events(ViewEvents): def post(self, data: ContactIn, request): send_message(data.email, data.message) return ContactForm() # <form method="post"> posting to ext/events/e/{class_id} # reaches ContactForm.Events.post.
actions module
The action constructors of the events extension: what a handler returns.
An event handler's return value is its whole response, and what flows back to the browser is actions: self-addressed instructions the client runtime applies in order (design docs/design/events.md 3.4). The capitalized constructors here build those action values; calling one performs nothing. Import the namespace once and return what you build::
from citry.ext.events import actions
class Events:
def save(self, state):
order = create_order(state.draft_id)
return [
actions.Dispatch("order-saved", {"id": order.id}),
actions.Redirect(f"/orders/{order.id}"),
]
Every envelope action accepts the timing keywords delay (seconds before the client applies the action) and wait (whether later actions hold for it). Download is the exception: it constructs a raw HTTP response result, not an envelope action.
Turning return values into these actions (dicts, elements, resolver-claimed values) and encoding them for the wire lives in the sibling results module; this module is only the vocabulary.
Action class
Base class of the action values an event handler returns.
The concrete constructors are Render, Data, Dispatch, and Redirect, plus the history actions PushUrl and ReplaceUrl. An action is a plain value: constructing one performs nothing, and it only takes effect when the handler returns it (alone or in a list).
Attributes
delayfloat- Seconds the client waits before applying the action. ``0`` (the default) applies it immediately.waitbool- Whether later actions in the same result hold until this one (and its ``delay``) has applied. ``True`` (the default) keeps the list strictly sequential; ``False`` schedules this action and lets the rest proceed immediately.
Render class
Bases: Action
Render a component element server-side and morph it into the page.
The element renders as a citry fragment (markup plus its dependency and events manifests), and the client swaps it into target. A handler builds a fresh tree to render; nothing of the instance's original render is replayed (design events.md 7.5).
Example
def add_to_cart(self, data: CartIn, context): cart = add_item(context.user, data.product_id) return actions.Render( CartBadge(count=cart.count), target="#cart-badge", )
Attributes
elementCitryElement | CitryRender- What to render: a component element (``MyComponent(...)``) or an already-rendered [`CitryRender`](/reference/rendering/#citry-citryrender).targetstr | None- Where the rendered HTML goes: a CSS selector string (applied to every match), or ``None`` (the default) for the component instance whose event was called.swapstr- How the HTML is applied: ``"morph"`` (the default, a minimal in-place diff), ``"replace"``, ``"inner"``, ``"append"``, ``"prepend"``, ``"remove"``, or ``"none"``.delayfloat- Seconds the client waits before applying the action.waitbool- Whether later actions hold until this one has applied.
Data class
Bases: Action
Resolve the client caller's promise with a JSON value.
The value becomes the resolution of the $sendEvent / Citry.events.send promise on the client. At most one Data may appear in one handler result (two would contradict: which value resolves the promise?); returning a bare dict from a handler builds this action implicitly.
Attributes
valueAny- The JSON-serializable value the caller receives.delayfloat- Seconds the client waits before applying the action.waitbool- Whether later actions hold until this one has applied.
Dispatch class
Bases: Action
Dispatch a named browser event (a DOM CustomEvent).
The event fires under the exact given name on the calling instance's first live root (or on document when the call carries no instance), bubbles, and reaches onEvent listeners and plain addEventListener alike. A multi-root or mirrored instance deliberately uses one canonical root so a logical dispatch reaches document-level listeners only once. Names starting with citry: are reserved for the runtime's own events; the documented convention is prefixing with the component name ("MyCard:submit").
Attributes
namestr- The event name, dispatched verbatim.detailAny- The ``CustomEvent`` ``detail`` payload, a JSON-serializable value; ``None`` (the default) sends no detail.delayfloat- Seconds the client waits before applying the action.waitbool- Whether later actions hold until this one has applied.
Redirect class
Bases: Action
Navigate the page to a URL.
A redirect is an ordinary action, not an HTTP 30x: it applies in list order like everything else. Actions listed after it race the navigation, so put it last, or give it delay / wait timing when something (say a farewell toast) must be seen first: actions.Redirect(url, delay=5, wait=False).
Attributes
urlstr- The URL to navigate to.delayfloat- Seconds the client waits before applying the action.waitbool- Whether later actions hold until this one has applied.
PushUrl class
Bases: Action
Push a URL onto the browser's history stack without navigating.
The browser changes the address and adds one history entry, but Citry does not fetch the URL or replace the page. Back and Forward therefore change the address without restoring component HTML or State; use a client router when that restoration is required.
Attributes
urlstr- The same-origin URL to place in browser history. Relative URLs, query strings, and fragments are accepted; the browser resolves them against the current document.delayfloat- Seconds the client waits before applying the action.waitbool- Whether later actions hold until this one has applied.
ReplaceUrl class
Bases: Action
Replace the browser's current history URL without navigating.
The browser changes the address in place, but Citry does not fetch the URL or replace the page. Back and Forward therefore change the address without restoring component HTML or State; use a client router when that restoration is required.
Attributes
urlstr- The same-origin URL to place in browser history. Relative URLs, query strings, and fragments are accepted; the browser resolves them against the current document.delayfloat- Seconds the client waits before applying the action.waitbool- Whether later actions hold until this one has applied.
Download class
Return a file download from a per-event HTTP handler.
A download is an HTTP response result, not an envelope action. Its handler must use @event(bundle=False) and be called through its per-event HTTP route. It may be returned bare or as the only item in a list or tuple.
Example
from citry.ext.events import actions, event @event(bundle=False) def export(self): return actions.Download( make_csv(), "orders.csv", content_type="text/csv; charset=utf-8", )
Attributes
contentstr | bytes- The response body, as text or raw bytes.filenamestr- The filename offered to the browser. It may contain Unicode, but cannot be a path or contain control characters.content_typestr- The response's media type. The default is ``"application/octet-stream"``.
event function
event(func: _F | None = None, name: str | None = None, methods: tuple[str, ...] | list[str] | None = None, guard: Callable[..., Any] | None = None, csrf: str | bool | Callable[..., Any] | None = None, debounce: int | None = None, throttle: int | None = None, latest_wins: bool = False, bundle: bool = True) -> _F | Callable[[_F], _F]Per-handler configuration for one event handler.
Bare handlers need no decorator; use @event(...) only to override the component-level defaults for one handler. Every key not given falls back to the component's underscore config (_methods, _guard, _csrf, _debounce, _throttle), which itself falls back to extensions_defaults["events"] and then the built-in defaults. The queue knobs latest_wins and bundle are the exception: they exist per handler only, so a call site that needs different queue semantics names a different handler.
Example
from citry.ext.events import event class Document(Component): class Events: @event(debounce=400) def autosave(self, state: DocState): save_draft(state.doc_id, state.title) @event(methods=("GET",)) def word_count(self, data: WordCountIn) -> dict: return {"words": count_words(data.doc_id)}
Parameters
func_F | None- The handler, when used as a bare ``@event`` (no arguments).namestr | None- Wire name override: rename the Python method without touching templates.methodstuple[str, ...] | list[str] | None- The allowed HTTP methods for this handler, e.g. ``("GET",)``.guardCallable[..., Any] | None- Per-handler authorization callable; replaces (does not stack on) the component's ``_guard``.csrfstr | bool | Callable[..., Any] | None- Per-handler CSRF policy: ``"auto"``, ``False``, or a callable.debounceint | None- Client-side debounce, in milliseconds.throttleint | None- Client-side throttle, in milliseconds.latest_winsbool- Opt this handler into newest-wins queueing on the client. When a newer call to the handler is queued for the same component instance, calls still waiting to be sent are dropped, and an in-flight one is abandoned (its response is ignored when it arrives). The server still executes every call it received, so opt in only when overlapping runs are safe, e.g. an idempotent autosave. Off by default, because dropping a call is data loss unless the handler is written for it.bundlebool- Whether the client may send this handler's calls in a shared batch request alongside other calls that become ready at the same moment. Pass ``False`` for a handler that should always travel alone, e.g. a slow export that fast toggles should not wait on.
Returns
_F | Callable[[_F], _F]: The handler itself (the decorator only attaches the configuration).
Raises
ValueError- When a value has the wrong shape (e.g. a ``methods`` string instead of a tuple), at decoration time.
get_event_url function
get_event_url(comp_cls: type[Component], name: str, query: dict[str, Any] | None = None, fragment: str | None = None) -> strThe URL one event handler is dispatchable at (the per-event route).
Checks the event exists on the component, so a typo fails at build time instead of 404-ing at call time. Requires a mounted web integration (the URL must point somewhere); Citry.build_url raises the standard pointed error otherwise.
Example
from citry.ext.events import get_event_url get_event_url(TodoList, "search", query={"q": "milk"})
Parameters
comp_clstype[Component]- The component class declaring the handler.namestr- The handler's wire name.querydict[str, Any] | None- Optional query parameters to append.fragmentstr | None- Optional ``#fragment`` to append.
Returns
str: The absolute URL path, e.g. ``"/citry/ext/events/e/Doc_a1b2c3/save"``.
Raises
ValueError- When the component declares no event of that name.RuntimeError- When no web integration is mounted.