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;Nonewhen the component declares no State class.contextAny- Whatever the_contexthook returned for the call being handled;Nonewhen no hook is configured.requestRouteRequest- A framework-neutral view of the request that carried the call (RouteRequest).eventAny- Metadata about the call being handled.
url function
Build the URL that calls one of this component's event handlers.
Call it on the component while it renders, usually in template_data(), to point a plain HTML form or a link at a handler. Outside a render, use get_event_url() with the component class instead.
Example
class Signup(Component): class Events: def submit(self, data: SignupIn) -> None: create_account(data.email) def template_data(self, kwargs, slots): return {"submit_url": self.events.url("submit")}
Parameters
namestr- The handler's name: the method name, or the name given with@event(name=...).querydict[str, Any] | None- Query parameters to add to the URL. Default: none.fragmentstr | None- Text for the#fragmentpart of the URL. Default: none.
Returns
str: The handler's URL path with `query` and `fragment` added, for
Raises
ValueError- When the component has no handler namedname.RuntimeError- When no web framework integration is mounted, so the URL would point nowhere, or when you callurl()on anEventsobject you created yourself instead of onself.eventsof a component.
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, orNonefor an instance-less call (an API client, a hand-written form).transportstr- The transport that carried the call ("http", ...).argsMapping[str, Any]- The call's args before validation, with the host CSRF field names removed (the_csrf_field_nameskey ofextensions_defaults["events"]). Guards read this when they need payload values, because one guard covers handlers with different schemas.
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 (for example, $error('submit')?.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": "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, and409cover 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, 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- TheContent-Typeheader 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 same JSON call that Citry's browser code sends (the citry-events/1 call envelope) 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 decodedcitry-events/1call envelope.ctxTransportContext- What the transport knows about the request.requestEventRequest | None- The neutral request injected into handlers asrequest;Nonebuilds an empty one carryingctx.host_requestasnative.url_componentstr | None- The class id the per-event URL names; when given, the URL is authoritative and a differing body field is rejected.Noneon 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 ascsrf_failed.Noneskips the check (a transport with its own protection, or a direct caller).
Returns
dict[str, Any] | RouteResponse: The ``citry-events/1`` result envelope, 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 run in a worker thread 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, orNoneto build one fromctx.host_request.url_componentstr | None- The class id named by a per-event URL, orNonefor 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, orNonewhen 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.
Experimental
The hooks this extension fires for other extensions,
on_event,on_event_resultandon_event_error, are experimental. Their names, context objects and return values may change in a minor release without a deprecation period.
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, plus the engine-wide _max_body_bytes, _max_form_fields, and _csrf_field_names, 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. 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, orNonewhen they come from theextensions_defaultssetting.
Raises
ValueError- For an unrecognized underscore name (with a "did you mean" hint), an event handler inextensions_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_compiled function
on_template_compiled(ctx: OnTemplateCompiledContext) -> list[Any]Validate and compile literal @c-* and :c-* bindings.
Parameters
ctxOnTemplateCompiledContext- The compiled template body and its owning component.
Returns
list[Any]: The body with parser-proven element bindings compiled.
Raises
ValueError- When a binding names an unknown handler or State field, or uses an invalid modifier combination.
on_template_reset function
on_template_reset(ctx: OnTemplateResetContext) -> NoneDiscard binding diagnostics derived from the previous compiled body.
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 two_way_binding_targets function
The State fields bound two-way in a component's template.
Populated when the template first compiles, so it is empty for a component whose template has not been compiled yet or that has no two-way bindings. Each individual target was already validated against _model during compilation; 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 whenstate_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, such as"http"; handlers see it asevent.transport.citryCitry- The engine the call dispatches against.host_requestAny- The untouched host request object (Django'sHttpRequest, the ASGI scope, or a custom transport's own object);Nonewhen 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, orNonewhen the request did not carry one.native- The host framework's own file object (e.g. Django'sUploadedFileor Starlette'sUploadFile), for the rare case a handler needs a host-specific capability;Nonewhen 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: instructions the browser applies in the order the handler returned them. 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 Checkout(Component):
class Events:
def save(self, state):
order = create_order(state.draft_id)
return [
actions.Dispatch(
"Checkout:order-saved",
{"id": order.id},
),
actions.Redirect(f"/orders/{order.id}"),
]
Every envelope action accepts delay (seconds before the client applies the action). Most also accept wait (whether later actions hold for it). A Data action always waits because applying it resolves the caller's promise. Download is not an envelope action; it constructs a raw HTTP response result.
A handler may also return a bare dict (a Data action), a component element or CitryRender (a Render action), or a value that one of the event_result_resolvers converts into actions.
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 itsdelay) has applied.True(the default) keeps the list strictly sequential;Falseschedules this action and lets the rest proceed immediately. Concrete actions may requireTruewhen their effect cannot run independently.
Render class
Bases: Action
Render a component element on the server and update the page with it.
Citry renders the element, and the browser updates target with the result through Vue. A handler builds a fresh tree to render; nothing of the instance's original render is replayed.
Several independent targets may be updated by one contiguous run of immediate, blocking Render actions. Every Render in that run must omit delay or use 0 and must keep wait=True. Deferred or interleaved multi-target runs, duplicate targets, and overlapping ancestor and descendant targets are rejected before the response changes State.
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="mark:cart-badge", )
Attributes
elementCitryElement | CitryRender- What to render: a component element (MyComponent(...)) or an already-renderedCitryRender.targetstr | None- The component addressrender:<id>, the caller-relative marker addressmark:<name>, orNonefor the calling component instance.swapstr- How the browser applies the rendered component. The Vue browser runtime supports only"morph"(the default), which updates the existing content in place. Another value raisesValueErrorwhen you build the action if Citry does not know the value or you also pass atarget. Otherwise the Vue renderer raises it when Citry builds a response for the browser runtime.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, $component sendEvent, or Citry.events.send promise on the client. Declarative @c-* bindings discard that promise, so they do not expose the Data value; return Dispatch when browser code must observe their result. 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.
Raises
ValueError- IfwaitisFalse, or another timing value is invalid.
Attributes
valueAny- The JSON-serializable value the caller receives.delayfloat- Seconds the client waits before applying the action.waitbool- AlwaysTruebecause applying this action resolves the caller's promise.
Dispatch class
Bases: Action
Send a named event to the browser.
The browser delivers the event under the exact given name in two ways. Listeners that the calling component registered with this.$onEvent in its Vue code, or with onEvent in its $component callback, receive detail. Citry also dispatches a bubbling DOM CustomEvent on the first element the calling component currently renders, so listeners on that element, its ancestors, or document receive it. If the component renders no element (only text, for example), the event goes to the component's root DOM node instead. A component that renders several top-level elements still dispatches one event, on the first of them, so a document listener runs once. When page code makes the call without a component (a hand-written fetch, for example) and applies the returned actions itself with Citry.events.applyActions, the event is dispatched only on document, and no $onEvent listener is called.
Prefix the name with the component name, such as "MyCard:submit", so events from different components do not collide. Names starting with citry: are reserved for Citry's own events and raise ValueError.
Raises
ValueError- Ifnameis empty or starts withcitry:, or a timing value is invalid.
Attributes
namestr- The event name, dispatched verbatim.detailAny- TheCustomEventdetailpayload, 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.
With JavaScript, 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).
The browser resolves a relative URL such as "?page=2" or "next" against the visitor's page. When a plain HTML form posts without JavaScript, the server reads that page from the Referer header. If no Referer from the same host arrives, the visitor lands relative to the handler URL, usually on a "not found" page, and the server logs a warning. Use a path that starts with / on a page that sends no Referer or only the site's origin. A <base href> element does not change where a relative URL leads.
The action checks the scheme only, not where the URL leads. A URL such as //other.example or /\other.example has no scheme, yet the browser sends the visitor to another site. When the URL comes from the request, such as a next parameter, check it before you pass it in. Checking that it starts with / is not enough.
Raises
ValueError- Ifurlis not a string, is empty, contains a control character, or has a scheme other thanhttp:orhttps:.
Attributes
urlstr- The URL to navigate to: anhttp:orhttps:URL, or a URL with no scheme such as/orders/7or?page=2.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; Citry resolves them against the page's own address, and a<base>element does not change them.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; Citry resolves them against the page's own address, and a<base>element does not change them.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, detached: bool = False) -> _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, bundle and detached are the exception: they exist per handler only, so a call site that needs different queue semantics names a different handler. A detached handler also needs the call site to pass wait: false; a call without it waits its turn.
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)} @event(detached=True) def spell_check(self, data: SpellCheckIn) -> dict: return {"typos": find_typos(data.text)}
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",). Each must be one of GET, HEAD, POST, PUT, PATCH, DELETE, or OPTIONS, the methods the per-event route accepts.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, an in-flight one is abandoned (its response is ignored when it arrives), and one whose result already arrived stops, so none of its actions that have not started yet will run. 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. PassFalsefor a handler that should always travel alone, e.g. a slow export that fast toggles should not wait on.detachedbool- Let browser code call this handler without waiting for the app's other calls, with$sendEvent(name, args, {wait: false}). In exchange, the handler must not render or change State: it may return data, dispatch an event, redirect, update the URL, or return aDownload, and it may read State. A detached handler that renders or changes State fails with ahandler_error. It reads State from the token the browser sends with the call: browser State changes not yet sent stay in the browser for the next call to a handler that is not detached, so pass values the user is still editing as arguments. Its calls can run on the server at the same time as the app's other calls, so do not rely on what those calls write. Off by default.
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. amethodsstring instead of a tuple), or whenmethodsnames a method the per-event route does not accept (anything other than GET, HEAD, POST, PUT, PATCH, DELETE, and OPTIONS), 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#fragmentto 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.