Skip to content

Events

Full attribute and method reference for the event system, generated from docstrings. For a task-oriented walkthrough, see Events.

Event bus

starlette_admin.events.AdminEventBus

Admin-level event bus. Delegates view lifecycle events to view buses; handles admin-level events (e.g. AFTER_LOGIN) itself, since they are not tied to any view.

Source code in starlette_admin/events.py
class AdminEventBus:
    """Admin-level event bus. Delegates view lifecycle events to view buses;
    handles admin-level events (e.g. `AFTER_LOGIN`) itself, since they are not
    tied to any view."""

    def __init__(self) -> None:
        self._pending_on: list[_PendingOn] = []
        # Maps view key to its EventBus, populated as views are registered.
        self._view_buses: dict[str, EventBus] = {}
        # Handles events that are not tied to any view.
        self._bus = EventBus()

    def _register_view(self, key: str, bus: EventBus) -> None:
        """Called when a view is added to the admin; propagates pending subscriptions."""
        logger.debug("AdminEventBus: registering view %r", key)
        self._view_buses[key] = bus
        for p in self._pending_on:
            if _is_view_lifecycle(p.event) and (p.keys is None or key in p.keys):
                bus.on(p.event, p.handler, priority=p.priority)

    def on(
        self,
        event: AdminEvent | str,
        handler: Callable[[EventContext], Awaitable[None]] | None = None,
        *,
        keys: list[str] | None = None,
        priority: int = 0,
    ) -> Callable:
        """Register a handler. For view lifecycle events, delegates to matching view buses.
        Admin-level events (e.g. `AFTER_LOGIN`) are handled directly and ignore `keys`.

        Args:
            event: The event to handle.
            handler: The callback to invoke. If omitted, returns a decorator.
            keys: Restricts the handler to views whose key is in this
                list. `None` (the default) registers it for all views.
                Has no effect for admin-level events.
            priority: Handlers with a higher priority run first.
        """

        def _register(fn: Callable) -> Callable:
            if _is_view_lifecycle(event):
                pending = _PendingOn(event, fn, keys, priority)
                self._pending_on.append(pending)
                for key, bus in self._view_buses.items():
                    if keys is None or key in keys:
                        bus.on(event, fn, priority=priority)
            else:
                self._bus.on(event, fn, priority=priority)
            return fn

        if handler is not None:
            return _register(handler)
        return _register

    async def emit(self, ctx: EventContext) -> None:
        """Fire handlers registered for an admin-level event (e.g. `AFTER_LOGIN`).

        View lifecycle events are emitted by the matching view's `EventBus`
        instead; this is only for events not tied to any view.
        """
        await self._bus.emit(ctx)

    def subscribe(
        self,
        subscriber: AdminEventSubscriber,
        *,
        keys: list[str] | None = None,
    ) -> None:
        """Delegate all subscriptions declared by subscriber to matching view buses."""
        for sub_event, spec in subscriber.subscriptions().items():
            items = spec if isinstance(spec, list) else [spec]
            for item in items:
                if isinstance(item, tuple):
                    fn, prio = cast("tuple[EventHandler, int]", item)
                    self.on(sub_event, fn, keys=keys, priority=prio)
                else:
                    self.on(sub_event, cast(EventHandler, item), keys=keys)

    def off(self, event: AdminEvent | str, handler: Callable) -> None:
        """Remove a handler from all view buses, or from the admin-level bus."""
        if _is_view_lifecycle(event):
            self._pending_on = [
                p
                for p in self._pending_on
                if not (p.event == event and p.handler is handler)
            ]
            for bus in self._view_buses.values():
                bus.off(event, handler)
        else:
            self._bus.off(event, handler)

    def unsubscribe(self, subscriber: AdminEventSubscriber) -> None:
        """Remove all handlers from a subscriber across all buses."""
        for sub_event, spec in subscriber.subscriptions().items():
            items = spec if isinstance(spec, list) else [spec]
            for item in items:
                if isinstance(item, tuple):
                    fn, _ = cast("tuple[EventHandler, int]", item)
                    self.off(sub_event, fn)
                else:
                    self.off(sub_event, cast(EventHandler, item))

emit(ctx) async

Fire handlers registered for an admin-level event (e.g. AFTER_LOGIN).

View lifecycle events are emitted by the matching view's EventBus instead; this is only for events not tied to any view.

Source code in starlette_admin/events.py
async def emit(self, ctx: EventContext) -> None:
    """Fire handlers registered for an admin-level event (e.g. `AFTER_LOGIN`).

    View lifecycle events are emitted by the matching view's `EventBus`
    instead; this is only for events not tied to any view.
    """
    await self._bus.emit(ctx)

off(event, handler)

Remove a handler from all view buses, or from the admin-level bus.

Source code in starlette_admin/events.py
def off(self, event: AdminEvent | str, handler: Callable) -> None:
    """Remove a handler from all view buses, or from the admin-level bus."""
    if _is_view_lifecycle(event):
        self._pending_on = [
            p
            for p in self._pending_on
            if not (p.event == event and p.handler is handler)
        ]
        for bus in self._view_buses.values():
            bus.off(event, handler)
    else:
        self._bus.off(event, handler)

on(event, handler=None, *, keys=None, priority=0)

Register a handler. For view lifecycle events, delegates to matching view buses. Admin-level events (e.g. AFTER_LOGIN) are handled directly and ignore keys.

Parameters:

Name Type Description Default
event AdminEvent | str

The event to handle.

required
handler Callable[[EventContext], Awaitable[None]] | None

The callback to invoke. If omitted, returns a decorator.

None
keys list[str] | None

Restricts the handler to views whose key is in this list. None (the default) registers it for all views. Has no effect for admin-level events.

None
priority int

Handlers with a higher priority run first.

0
Source code in starlette_admin/events.py
def on(
    self,
    event: AdminEvent | str,
    handler: Callable[[EventContext], Awaitable[None]] | None = None,
    *,
    keys: list[str] | None = None,
    priority: int = 0,
) -> Callable:
    """Register a handler. For view lifecycle events, delegates to matching view buses.
    Admin-level events (e.g. `AFTER_LOGIN`) are handled directly and ignore `keys`.

    Args:
        event: The event to handle.
        handler: The callback to invoke. If omitted, returns a decorator.
        keys: Restricts the handler to views whose key is in this
            list. `None` (the default) registers it for all views.
            Has no effect for admin-level events.
        priority: Handlers with a higher priority run first.
    """

    def _register(fn: Callable) -> Callable:
        if _is_view_lifecycle(event):
            pending = _PendingOn(event, fn, keys, priority)
            self._pending_on.append(pending)
            for key, bus in self._view_buses.items():
                if keys is None or key in keys:
                    bus.on(event, fn, priority=priority)
        else:
            self._bus.on(event, fn, priority=priority)
        return fn

    if handler is not None:
        return _register(handler)
    return _register

subscribe(subscriber, *, keys=None)

Delegate all subscriptions declared by subscriber to matching view buses.

Source code in starlette_admin/events.py
def subscribe(
    self,
    subscriber: AdminEventSubscriber,
    *,
    keys: list[str] | None = None,
) -> None:
    """Delegate all subscriptions declared by subscriber to matching view buses."""
    for sub_event, spec in subscriber.subscriptions().items():
        items = spec if isinstance(spec, list) else [spec]
        for item in items:
            if isinstance(item, tuple):
                fn, prio = cast("tuple[EventHandler, int]", item)
                self.on(sub_event, fn, keys=keys, priority=prio)
            else:
                self.on(sub_event, cast(EventHandler, item), keys=keys)

unsubscribe(subscriber)

Remove all handlers from a subscriber across all buses.

Source code in starlette_admin/events.py
def unsubscribe(self, subscriber: AdminEventSubscriber) -> None:
    """Remove all handlers from a subscriber across all buses."""
    for sub_event, spec in subscriber.subscriptions().items():
        items = spec if isinstance(spec, list) else [spec]
        for item in items:
            if isinstance(item, tuple):
                fn, _ = cast("tuple[EventHandler, int]", item)
                self.off(sub_event, fn)
            else:
                self.off(sub_event, cast(EventHandler, item))

starlette_admin.events.EventBus

Per-view event bus. This is the only bus type that emits events directly; AdminEventBus delegates lifecycle events to the matching view's EventBus instead of emitting them itself.

Source code in starlette_admin/events.py
class EventBus:
    """Per-view event bus. This is the only bus type that emits events directly;
    [AdminEventBus][starlette_admin.events.AdminEventBus] delegates lifecycle
    events to the matching view's `EventBus` instead of emitting them itself.
    """

    def __init__(self) -> None:
        # Maps event name to a list of (priority, handler) pairs.
        self._handlers: dict[str, list[tuple[int, Callable]]] = defaultdict(list)
        self._subscribers: list[AdminEventSubscriber] = []

    def on(
        self,
        event: AdminEvent | str,
        handler: Callable[[EventContext], Awaitable[None]] | None = None,
        *,
        priority: int = 0,
    ) -> Callable:
        """Register a handler. Usable as a decorator or a direct call."""
        key = event.value if isinstance(event, AdminEvent) else event

        def _register(fn: Callable) -> Callable:
            self._handlers[key].append((priority, fn))
            self._handlers[key].sort(key=lambda x: x[0], reverse=True)
            logger.debug(
                "EventBus: registered handler %s for event %r (priority=%d)",
                fn.__qualname__,  # ty: ignore[unresolved-attribute]
                key,
                priority,
            )
            return fn

        if handler is not None:
            _register(handler)
            return handler
        return _register

    def subscribe(self, subscriber: AdminEventSubscriber) -> None:
        """Register a subscriber object for multiple events at once."""
        logger.debug("EventBus: subscribing %s", type(subscriber).__qualname__)
        self._subscribers.append(subscriber)
        for event, spec in subscriber.subscriptions().items():
            items = spec if isinstance(spec, list) else [spec]
            for item in items:
                if isinstance(item, tuple):
                    fn, prio = cast("tuple[EventHandler, int]", item)
                    self.on(event, fn, priority=prio)
                else:
                    self.on(event, cast(EventHandler, item))

    def off(self, event: AdminEvent | str, handler: Callable) -> None:
        """Remove a handler for an event."""
        key = event.value if isinstance(event, AdminEvent) else event
        self._handlers[key] = [(p, h) for (p, h) in self._handlers[key] if h != handler]

    def unsubscribe(self, subscriber: AdminEventSubscriber) -> None:
        """Remove all handlers registered by a subscriber."""
        if subscriber not in self._subscribers:
            return
        self._subscribers.remove(subscriber)
        for event, spec in subscriber.subscriptions().items():
            items = spec if isinstance(spec, list) else [spec]
            for item in items:
                if isinstance(item, tuple):
                    fn, _ = cast("tuple[EventHandler, int]", item)
                    self.off(event, fn)
                else:
                    self.off(event, cast(EventHandler, item))

    async def emit(self, ctx: EventContext) -> None:
        """Fire all registered handlers in priority order."""
        key = ctx.event.value if isinstance(ctx.event, AdminEvent) else ctx.event
        handlers = self._handlers.get(key, [])
        logger.debug("EventBus: emitting %r to %d handler(s)", key, len(handlers))

        for _priority, handler in handlers:
            logger.debug(
                "EventBus: calling %s (priority=%d)",
                handler.__qualname__,  # ty: ignore[unresolved-attribute]
                _priority,
            )
            result = handler(ctx)
            if inspect.isawaitable(result):
                await result

emit(ctx) async

Fire all registered handlers in priority order.

Source code in starlette_admin/events.py
async def emit(self, ctx: EventContext) -> None:
    """Fire all registered handlers in priority order."""
    key = ctx.event.value if isinstance(ctx.event, AdminEvent) else ctx.event
    handlers = self._handlers.get(key, [])
    logger.debug("EventBus: emitting %r to %d handler(s)", key, len(handlers))

    for _priority, handler in handlers:
        logger.debug(
            "EventBus: calling %s (priority=%d)",
            handler.__qualname__,  # ty: ignore[unresolved-attribute]
            _priority,
        )
        result = handler(ctx)
        if inspect.isawaitable(result):
            await result

off(event, handler)

Remove a handler for an event.

Source code in starlette_admin/events.py
def off(self, event: AdminEvent | str, handler: Callable) -> None:
    """Remove a handler for an event."""
    key = event.value if isinstance(event, AdminEvent) else event
    self._handlers[key] = [(p, h) for (p, h) in self._handlers[key] if h != handler]

on(event, handler=None, *, priority=0)

Register a handler. Usable as a decorator or a direct call.

Source code in starlette_admin/events.py
def on(
    self,
    event: AdminEvent | str,
    handler: Callable[[EventContext], Awaitable[None]] | None = None,
    *,
    priority: int = 0,
) -> Callable:
    """Register a handler. Usable as a decorator or a direct call."""
    key = event.value if isinstance(event, AdminEvent) else event

    def _register(fn: Callable) -> Callable:
        self._handlers[key].append((priority, fn))
        self._handlers[key].sort(key=lambda x: x[0], reverse=True)
        logger.debug(
            "EventBus: registered handler %s for event %r (priority=%d)",
            fn.__qualname__,  # ty: ignore[unresolved-attribute]
            key,
            priority,
        )
        return fn

    if handler is not None:
        _register(handler)
        return handler
    return _register

subscribe(subscriber)

Register a subscriber object for multiple events at once.

Source code in starlette_admin/events.py
def subscribe(self, subscriber: AdminEventSubscriber) -> None:
    """Register a subscriber object for multiple events at once."""
    logger.debug("EventBus: subscribing %s", type(subscriber).__qualname__)
    self._subscribers.append(subscriber)
    for event, spec in subscriber.subscriptions().items():
        items = spec if isinstance(spec, list) else [spec]
        for item in items:
            if isinstance(item, tuple):
                fn, prio = cast("tuple[EventHandler, int]", item)
                self.on(event, fn, priority=prio)
            else:
                self.on(event, cast(EventHandler, item))

unsubscribe(subscriber)

Remove all handlers registered by a subscriber.

Source code in starlette_admin/events.py
def unsubscribe(self, subscriber: AdminEventSubscriber) -> None:
    """Remove all handlers registered by a subscriber."""
    if subscriber not in self._subscribers:
        return
    self._subscribers.remove(subscriber)
    for event, spec in subscriber.subscriptions().items():
        items = spec if isinstance(spec, list) else [spec]
        for item in items:
            if isinstance(item, tuple):
                fn, _ = cast("tuple[EventHandler, int]", item)
                self.off(event, fn)
            else:
                self.off(event, cast(EventHandler, item))

starlette_admin.events.AdminEventSubscriber

Source code in starlette_admin/events.py
class AdminEventSubscriber:
    def subscriptions(self) -> dict[AdminEvent | str, HandlerSpec | list[HandlerSpec]]:
        """Return event → handler(s) mapping.

        The default implementation discovers methods decorated with ``@on``.
        Override to use the explicit dict API instead.
        """
        result: dict[AdminEvent | str, list[HandlerSpec]] = defaultdict(list)
        seen: set[str] = set()
        for cls in type(self).__mro__:
            for name, fn in cls.__dict__.items():
                if name in seen or not callable(fn):
                    continue
                seen.add(name)
                specs = getattr(fn, _ADMIN_EVENTS_ATTR, None)
                if specs:
                    method = getattr(self, name)
                    for event, priority in specs:
                        result[event].append(
                            (method, priority) if priority != 0 else method
                        )
        return dict(result)

subscriptions()

Return event → handler(s) mapping.

The default implementation discovers methods decorated with @on. Override to use the explicit dict API instead.

Source code in starlette_admin/events.py
def subscriptions(self) -> dict[AdminEvent | str, HandlerSpec | list[HandlerSpec]]:
    """Return event → handler(s) mapping.

    The default implementation discovers methods decorated with ``@on``.
    Override to use the explicit dict API instead.
    """
    result: dict[AdminEvent | str, list[HandlerSpec]] = defaultdict(list)
    seen: set[str] = set()
    for cls in type(self).__mro__:
        for name, fn in cls.__dict__.items():
            if name in seen or not callable(fn):
                continue
            seen.add(name)
            specs = getattr(fn, _ADMIN_EVENTS_ATTR, None)
            if specs:
                method = getattr(self, name)
                for event, priority in specs:
                    result[event].append(
                        (method, priority) if priority != 0 else method
                    )
    return dict(result)

starlette_admin.events.on(*events, priority=0)

Decorator that marks a subscriber method as a handler for one or more events.

Usage::

class MyHandler(AdminEventSubscriber):
    @on(AdminEvent.BEFORE_CREATE, priority=10)
    async def handle(self, ctx):
        ...
Source code in starlette_admin/events.py
def on(*events: AdminEvent | str, priority: int = 0) -> Callable:
    """Decorator that marks a subscriber method as a handler for one or more events.

    Usage::

        class MyHandler(AdminEventSubscriber):
            @on(AdminEvent.BEFORE_CREATE, priority=10)
            async def handle(self, ctx):
                ...
    """

    def decorator(fn: Callable) -> Callable:
        if not hasattr(fn, _ADMIN_EVENTS_ATTR):
            fn._admin_events = []  # ty: ignore[invalid-assignment]
        for event in events:
            fn._admin_events.append((event, priority))  # ty: ignore[unresolved-attribute]
        return fn

    return decorator

starlette_admin.events.AdminEvent

Bases: StrEnum

Source code in starlette_admin/events.py
class AdminEvent(StrEnum):
    # Record lifecycle
    BEFORE_CREATE = "before_create"
    AFTER_CREATE = "after_create"
    AFTER_CREATE_COMMITTED = "after_create_committed"
    BEFORE_EDIT = "before_edit"
    AFTER_EDIT = "after_edit"
    AFTER_EDIT_COMMITTED = "after_edit_committed"
    BEFORE_DELETE = "before_delete"
    AFTER_DELETE = "after_delete"
    AFTER_DELETE_COMMITTED = "after_delete_committed"

    # Actions
    BEFORE_ACTION = "before_action"
    AFTER_ACTION = "after_action"

    # Export / Import
    BEFORE_EXPORT = "before_export"
    AFTER_EXPORT = "after_export"
    BEFORE_IMPORT = "before_import"
    AFTER_IMPORT = "after_import"

    # Auth
    AFTER_LOGIN = "after_login"

Event contexts

starlette_admin.events.EventContext dataclass

Base context present on every event.

Source code in starlette_admin/events.py
@dataclass
class EventContext:
    """Base context present on every event."""

    event: AdminEvent | str
    request: Request
    view_key: str
    extra: dict = field(default_factory=dict)

starlette_admin.events.BeforeCreateContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class BeforeCreateContext(EventContext):
    data: dict = field(default_factory=dict)
    obj: Any = None

starlette_admin.events.AfterCreateContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class AfterCreateContext(EventContext):
    pk: Any = None
    obj: Any = None

starlette_admin.events.BeforeEditContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class BeforeEditContext(EventContext):
    pk: Any = None
    data: dict = field(default_factory=dict)
    obj: Any = None
    old_data: dict = field(default_factory=dict)

starlette_admin.events.AfterEditContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class AfterEditContext(EventContext):
    pk: Any = None
    obj: Any = None
    old_data: dict = field(default_factory=dict)

starlette_admin.events.BeforeDeleteContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class BeforeDeleteContext(EventContext):
    pk: Any = None
    obj: Any = None

starlette_admin.events.AfterDeleteContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class AfterDeleteContext(EventContext):
    pk: Any = None
    obj: Any = None

starlette_admin.events.BeforeBulkDeleteContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class BeforeBulkDeleteContext(EventContext):
    pks: list[Any] = field(default_factory=list)

starlette_admin.events.AfterBulkDeleteContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class AfterBulkDeleteContext(EventContext):
    pks: list[Any] = field(default_factory=list)
    deleted_count: int = 0

starlette_admin.events.BeforeActionContext dataclass

Bases: EventContext

selection.pks() (and .rows(), .count()) are async and resolved lazily: call them from a handler only if you need the target rows, since a select-all action can otherwise skip materializing them entirely.

Source code in starlette_admin/events.py
@dataclass
class BeforeActionContext(EventContext):
    """`selection.pks()` (and `.rows()`, `.count()`) are async and resolved lazily:
    call them from a handler only if you need the target rows, since a select-all
    action can otherwise skip materializing them entirely."""

    action_name: str = ""
    selection: ActionSelection | None = None

starlette_admin.events.AfterActionContext dataclass

Bases: EventContext

See BeforeActionContext for selection's lazy-resolution contract.

Source code in starlette_admin/events.py
@dataclass
class AfterActionContext(EventContext):
    """See `BeforeActionContext` for `selection`'s lazy-resolution contract."""

    action_name: str = ""
    selection: ActionSelection | None = None
    success: bool = True
    error: str | None = None

starlette_admin.events.BeforeExportContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class BeforeExportContext(EventContext):
    export_type: BaseExporter | None = None
    items: Sequence[Any] = field(default_factory=list)
    export_ctx: ExportContext | None = None

starlette_admin.events.AfterExportContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class AfterExportContext(EventContext):
    export_type: BaseExporter | None = None
    items: Sequence[Any] = field(default_factory=list)
    row_count: int = 0
    export_ctx: ExportContext | None = None

starlette_admin.events.BeforeImportContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class BeforeImportContext(EventContext):
    import_type: BaseImporter | None = None
    import_ctx: ImportContext | None = None

starlette_admin.events.AfterImportContext dataclass

Bases: EventContext

Source code in starlette_admin/events.py
@dataclass
class AfterImportContext(EventContext):
    import_type: BaseImporter | None = None
    row_count: int = 0
    error_count: int = 0
    import_ctx: ImportContext | None = None

starlette_admin.events.AfterLoginContext dataclass

Bases: EventContext

view_key is always empty: login is not tied to any view.

Source code in starlette_admin/events.py
@dataclass
class AfterLoginContext(EventContext):
    """`view_key` is always empty: login is not tied to any view."""

    user: AdminUser | None = None