Theme
Version
GitHub PyPI Discord
On this page

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.

View source

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 str

On a component with no State class, subclass the bare base: self.state is then typed (and is) None.

Attributes

  • state StateT - The component's State instance for the call being handled; ``None`` when the component declares no State class.
  • context Any - Whatever the ``_context`` hook returned for the call being handled; ``None`` when no hook is configured.
  • request RouteRequest - A framework-neutral view of the request that carried the call ([`RouteRequest`](/reference/web/#citry-routerequest)).
  • event Any - Metadata about the call being handled.
View source

X_CITRY_EVENTS_HEADER attribute

Header required on JSON Events calls as part of the same-origin CSRF check.

View source

CallEvent class

The event value injected into event handlers: metadata about one call.

Attributes

  • name str - The handler's wire name the call addressed.
  • instance_id str | None - The calling instance's render id, or ``None`` for an instance-less call (an API client, a hand-written form).
  • transport str - The transport that carried the call (``"http"``, ...).
  • args Mapping[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).
View source

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.
  • fields dict[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.
View source

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

  • method str - The HTTP method, uppercase.
  • path str - The full URL path of the request.
  • query Mapping[str, tuple[str, ...]] - The query-string parameters; each key maps to every value it was sent with, in order.
  • headers Mapping[str, str] - The request headers; lookups ignore case.
  • body bytes - The raw request body.
  • content_type str - The ``Content-Type`` header value, or ``""``.
  • form Mapping[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.
  • files Mapping[str, Any] - Uploaded files by field name when the selected payload codec or transport supplies them; otherwise empty.
  • native Any - The untouched host request object.
View source

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.

View source

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] | RouteResponse

Dispatch 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

  • envelope Mapping[str, Any] - The decoded call envelope (design 4.2).
  • ctx TransportContext - What the transport knows about the request.
  • request EventRequest | None - The neutral request injected into handlers as ``request``; ``None`` builds an empty one carrying ``ctx.host_request`` as ``native``.
  • url_component str | 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_event str | None - The event wire name the per-event URL names. A raw response additionally requires HTTP and ``@event(bundle=False)``.
  • csrf_check Callable[[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

View source

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] | RouteResponse

Dispatch 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

  • envelope Mapping[str, Any] - The decoded call envelope.
  • ctx TransportContext - What the transport knows about the request.
  • request EventRequest | None - The neutral request injected into handlers, or ``None`` to build one from ``ctx.host_request``.
  • url_component str | None - The class id named by a per-event URL, or ``None`` for a batch or custom transport.
  • url_event str | None - The event name named by a per-event URL. A raw response additionally requires HTTP and ``@event(bundle=False)``.
  • csrf_check Callable[[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

View source

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.

View source

urls attribute

urls: list[URLRoute]

Return the Events routes bound to this extension's Citry instance.

Returns

list[URLRoute]: Routes for the client runtime, batched calls, named handlers, and

View source

validate_config_fields function

validate_config_fields(fields: Mapping[str, Any], component: type[Component] | None = None) -> None

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

  • fields Mapping[str, Any] - The declared fields, mapping field name to declared value.
  • component type[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.
View source

on_component_class_created function

on_component_class_created(ctx: OnComponentClassCreatedContext) -> None

Validate and record one component's Events and State declarations.

View source

inspect_component function

inspect_component(ctx: ComponentIntrospectionContext) -> dict[str, object] | None

Return public, JSON-safe Events metadata for component introspection.

Parameters

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.
View source

on_template_loaded function

on_template_loaded(ctx: OnTemplateLoadedContext) -> str

Validate and compile literal @c-* and :c-* bindings.

Parameters

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.
View source

on_attrs_resolved function

on_attrs_resolved(ctx: OnAttrsResolvedContext) -> dict[str, Any] | None

Validate and compile @c-* and :c-* bindings from dynamic attrs.

Parameters

Returns

dict[str, Any] | None: Updated attributes, or ``None`` when the element has no Events

View source

on_component_data function

on_component_data(ctx: OnComponentDataContext) -> None

Prepare one rendered Events instance for browser activation.

Parameters

View source

on_render_context_merge function

on_render_context_merge(ctx: OnRenderContextMergeContext) -> None

Merge child Events records into their parent render context.

View source

export_render_cache function

export_render_cache(ctx: OnRenderCacheExportContext) -> dict[str, object]
View source

stage_render_cache function

stage_render_cache(ctx: OnRenderCacheStageContext) -> StagedRenderCacheContribution
View source

on_dependencies function

on_dependencies(ctx: OnDependenciesContext) -> None

Add the Events runtime and instance data to serialized dependencies.

Parameters

View source

two_way_binding_targets function

two_way_binding_targets(comp_cls: type[Component]) -> frozenset[str]

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).

View source

resolve function

resolve(comp_cls: type[Component]) -> EventsInfo

The events info of a component class: handlers, State, resolved config.

Parameters

Returns

EventsInfo: The resolved handler, State, and configuration record computed

View source

build_state function

build_state(component: Component) -> Any

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

  • component Component - 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.
View source

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

  • transport str - The transport's name (``"http"``, later ``"ws"``); handlers see it as ``event.transport``.
  • citry Citry - The engine the call dispatches against.
  • host_request Any - The untouched host request object (Django's ``HttpRequest``, the ASGI scope, a WS connection); ``None`` when the transport has none.
  • headers Mapping[str, str] - A case-insensitive view of the request headers; empty when the transport carries none.
  • response_mode Literal['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.
View source

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.
View source

read function

read(size: int = -1) -> bytes

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

  • size int - How many bytes to read; the whole content when negative.

Returns

bytes: The bytes read.

View source

save function

save(path: str | Path) -> Path

Write the file's whole content to path.

A seekable file is rewound first, so save() writes the full content regardless of earlier reads. The parent directory must already exist.

Parameters

  • path str | Path - Where to write the file.

Returns

Path: The destination as a ``Path``.

View source

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.
View source

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.

View source

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

  • delay float - Seconds the client waits before applying the action. ``0`` (the default) applies it immediately.
  • wait bool - 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.
View source

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

  • element CitryElement | CitryRender - What to render: a component element (``MyComponent(...)``) or an already-rendered [`CitryRender`](/reference/rendering/#citry-citryrender).
  • target str | 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.
  • swap str - How the HTML is applied: ``"morph"`` (the default, a minimal in-place diff), ``"replace"``, ``"inner"``, ``"append"``, ``"prepend"``, ``"remove"``, or ``"none"``.
  • delay float - Seconds the client waits before applying the action.
  • wait bool - Whether later actions hold until this one has applied.
View source

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

  • value Any - The JSON-serializable value the caller receives.
  • delay float - Seconds the client waits before applying the action.
  • wait bool - Whether later actions hold until this one has applied.
View source

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

  • name str - The event name, dispatched verbatim.
  • detail Any - The ``CustomEvent`` ``detail`` payload, a JSON-serializable value; ``None`` (the default) sends no detail.
  • delay float - Seconds the client waits before applying the action.
  • wait bool - Whether later actions hold until this one has applied.
View source

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

  • url str - The URL to navigate to.
  • delay float - Seconds the client waits before applying the action.
  • wait bool - Whether later actions hold until this one has applied.
View source

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

  • url str - 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.
  • delay float - Seconds the client waits before applying the action.
  • wait bool - Whether later actions hold until this one has applied.
View source

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

  • url str - 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.
  • delay float - Seconds the client waits before applying the action.
  • wait bool - Whether later actions hold until this one has applied.
View source

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

  • content str | bytes - The response body, as text or raw bytes.
  • filename str - The filename offered to the browser. It may contain Unicode, but cannot be a path or contain control characters.
  • content_type str - The response's media type. The default is ``"application/octet-stream"``.
View source

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).
  • name str | None - Wire name override: rename the Python method without touching templates.
  • methods tuple[str, ...] | list[str] | None - The allowed HTTP methods for this handler, e.g. ``("GET",)``.
  • guard Callable[..., Any] | None - Per-handler authorization callable; replaces (does not stack on) the component's ``_guard``.
  • csrf str | bool | Callable[..., Any] | None - Per-handler CSRF policy: ``"auto"``, ``False``, or a callable.
  • debounce int | None - Client-side debounce, in milliseconds.
  • throttle int | None - Client-side throttle, in milliseconds.
  • latest_wins bool - 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.
  • bundle bool - 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.
View source

get_event_url function

get_event_url(comp_cls: type[Component], name: str, query: dict[str, Any] | None = None, fragment: str | None = None) -> str

The 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_cls type[Component] - The component class declaring the handler.
  • name str - The handler's wire name.
  • query dict[str, Any] | None - Optional query parameters to append.
  • fragment str | 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.
Citry version: 0.3.0