Source code for roost.hooks
"""User-supplied event hooks around handler execution.
Hooks let host apps plug in extra observability, audit logging, custom
metrics, or shadow-mode dispatch without modifying Roost core.
Example::
from roost import Hooks, Worker
async def before(job, *, ctx):
ctx["t0"] = time.monotonic()
async def after(job, *, result, error, ctx):
elapsed = time.monotonic() - ctx["t0"]
my_metrics.observe(job.task, elapsed, error=error is not None)
worker = Worker(dsn, hooks=Hooks(before_job=before, after_job=after))
The ``ctx`` dict is shared between ``before_job`` and ``after_job`` for the
same execution. Hooks are awaited; sync hooks raise ``TypeError``.
"""
from __future__ import annotations
import inspect
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
BeforeHook = Callable[..., Awaitable[None]]
AfterHook = Callable[..., Awaitable[None]]
[docs]
@dataclass(frozen=True)
class Hooks:
"""Bundle of optional hooks invoked around every handler call.
Both hooks are async. The signature is ``(job, *, ctx)`` for
``before_job`` and ``(job, *, result, error, ctx)`` for
``after_job``. Either can be omitted; only set the ones you need.
"""
before_job: BeforeHook | None = None
after_job: AfterHook | None = None
async def call_before(hooks: Hooks | None, job: Any, ctx: dict[str, Any]) -> None:
if hooks is None or hooks.before_job is None:
return
result = hooks.before_job(job, ctx=ctx)
if not inspect.isawaitable(result):
raise TypeError("Hooks.before_job must be async (return an awaitable)")
await result
async def call_after(
hooks: Hooks | None,
job: Any,
*,
result: Any | None,
error: BaseException | None,
ctx: dict[str, Any],
) -> None:
if hooks is None or hooks.after_job is None:
return
awaitable = hooks.after_job(job, result=result, error=error, ctx=ctx)
if not inspect.isawaitable(awaitable):
raise TypeError("Hooks.after_job must be async (return an awaitable)")
await awaitable
__all__ = ["AfterHook", "BeforeHook", "Hooks", "call_after", "call_before"]