Skip to content

API reference: servicewright

Everything importable from the top-level package. This is the public vocabulary — the names here are covered by semantic versioning.

from servicewright import AppSpec, Service, ServiceError, run

Other pages: adapters, testing doubles.

Batteries-optional microservice runtime.

One Host, many Entrypoints. Describe a service once as an AppSpec and run it through any number of pluggable entrypoints (HTTP, gRPC, scheduler, consumer, daemon, one-shot) under one unified lifecycle.

AppScopeProtocol

Bases: Protocol

Protocol for application-level dependency scope.

The application scope is opened once for the whole process lifetime and hosts long-lived singletons (connection pools, clients, ...).

Source code in servicewright/core/contracts/container.py
class AppScopeProtocol(Protocol):
    """Protocol for application-level dependency scope.

    The application scope is opened once for the whole process lifetime and
    hosts long-lived singletons (connection pools, clients, ...).
    """

    @overload
    async def get(self, dependency_key: type[T]) -> T: ...

    @overload
    async def get(self, dependency_key: str) -> Any: ...

    async def get(self, dependency_key: type[T] | str) -> T | Any:
        """Retrieve dependency instance by key or type."""
        ...

get(dependency_key) async

get(dependency_key: type[T]) -> T
get(dependency_key: str) -> Any

Retrieve dependency instance by key or type.

Source code in servicewright/core/contracts/container.py
async def get(self, dependency_key: type[T] | str) -> T | Any:
    """Retrieve dependency instance by key or type."""
    ...

AppSpec dataclass

Complete transport-neutral declarative description of a microservice.

Source code in servicewright/core/spec.py
@dataclass(slots=True)
class AppSpec[TSettings: "BaseServiceSettingsProtocol", TContainer: "DependencyContainerProtocol"]:
    """Complete transport-neutral declarative description of a microservice."""

    service_name: str
    create_container: Callable[[TSettings], TContainer]
    lifecycle: Lifecycle = field(default_factory=Lifecycle)
    observability: ObservabilityManager = field(default_factory=ObservabilityManager)
    health: HealthRegistry = field(default_factory=HealthRegistry)
    warmers: list[AsyncWarmer] = field(default_factory=list)
    warmers_factory: WarmerFactory | None = None
    drain_grace_seconds: float = DEFAULT_DRAIN_GRACE_SECONDS
    """How long each entrypoint gets to finish in-flight work during drain."""
    cleanup_timeout_seconds: float = DEFAULT_CLEANUP_TIMEOUT_SECONDS
    """Budget for each post-drain step (``stop()``, hooks, observability flush)."""

cleanup_timeout_seconds = DEFAULT_CLEANUP_TIMEOUT_SECONDS class-attribute instance-attribute

Budget for each post-drain step (stop(), hooks, observability flush).

drain_grace_seconds = DEFAULT_DRAIN_GRACE_SECONDS class-attribute instance-attribute

How long each entrypoint gets to finish in-flight work during drain.

AsyncWarmer

Bases: ABC

Base class for async infrastructure warmers.

Source code in servicewright/core/contracts/warmer.py
class AsyncWarmer(ABC):
    """Base class for async infrastructure warmers."""

    def __init__(self, *, raise_on_failure: bool = True) -> None:
        """Initialize base warmer behavior."""
        self._raise_on_failure = raise_on_failure

    @property
    def raise_on_failure(self) -> bool:
        """Whether orchestrator should raise when this warmer fails."""
        return self._raise_on_failure

    @property
    def priority(self) -> int:
        """Priority of the warmer (lower value means higher priority).

        Warmers with higher priority (lower values) are executed first.
        Warmers with the same priority are executed in parallel.
        """
        return 0

    @abstractmethod
    async def warmup(self) -> None:
        """Perform infrastructure warmup.

        This method should initialize connection pools, fetch metadata,
        or perform any other operations to prepare the infrastructure
        for high-load production traffic.

        Raises:
            WarmupError: If warmup operation fails and cannot be recovered.
        """
        raise NotImplementedError

priority property

Priority of the warmer (lower value means higher priority).

Warmers with higher priority (lower values) are executed first. Warmers with the same priority are executed in parallel.

raise_on_failure property

Whether orchestrator should raise when this warmer fails.

__init__(*, raise_on_failure=True)

Initialize base warmer behavior.

Source code in servicewright/core/contracts/warmer.py
def __init__(self, *, raise_on_failure: bool = True) -> None:
    """Initialize base warmer behavior."""
    self._raise_on_failure = raise_on_failure

warmup() abstractmethod async

Perform infrastructure warmup.

This method should initialize connection pools, fetch metadata, or perform any other operations to prepare the infrastructure for high-load production traffic.

Raises:

Type Description
WarmupError

If warmup operation fails and cannot be recovered.

Source code in servicewright/core/contracts/warmer.py
@abstractmethod
async def warmup(self) -> None:
    """Perform infrastructure warmup.

    This method should initialize connection pools, fetch metadata,
    or perform any other operations to prepare the infrastructure
    for high-load production traffic.

    Raises:
        WarmupError: If warmup operation fails and cannot be recovered.
    """
    raise NotImplementedError

BaseServiceSettingsProtocol

Bases: Protocol

Protocol for complete microservice settings.

Observability sections are named by concern; None means the concern is unconfigured (its sink stays a NullObject).

Source code in servicewright/core/contracts/settings.py
class BaseServiceSettingsProtocol(Protocol):
    """Protocol for complete microservice settings.

    Observability sections are named by concern; ``None`` means the concern is
    unconfigured (its sink stays a NullObject).
    """

    @property
    def logging(self) -> LoggingSettingsProtocol | None:
        """Get logging configuration."""
        ...

    @property
    def metrics(self) -> MetricsSettingsProtocol | None:
        """Get metrics configuration."""
        ...

    @property
    def error_tracking(self) -> ErrorTrackingSettingsProtocol | None:
        """Get error-tracking configuration."""
        ...

    @property
    def tracing(self) -> TracingSettingsProtocol | None:
        """Get tracing configuration."""
        ...

    def get_app_version(self) -> str:
        """Return application version string."""
        ...

error_tracking property

Get error-tracking configuration.

logging property

Get logging configuration.

metrics property

Get metrics configuration.

tracing property

Get tracing configuration.

get_app_version()

Return application version string.

Source code in servicewright/core/contracts/settings.py
def get_app_version(self) -> str:
    """Return application version string."""
    ...

BootstrapContext dataclass

Context available before the application scope is opened.

Source code in servicewright/core/spec.py
@dataclass(slots=True)
class BootstrapContext[TSettings: "BaseServiceSettingsProtocol", TContainer: "DependencyContainerProtocol"]:
    """Context available before the application scope is opened."""

    settings: TSettings
    service_name: str
    container: TContainer
    lifecycle: Lifecycle = field(default_factory=Lifecycle)

ChainRedactor

Applies redactors left to right: ChainRedactor(KeyRedactor(), ValueRedactor(m)).

Order matters and the conventional order is key-based first: sensitive fields are already collapsed to the mask before the (potentially more expensive) value masker sees the payload.

Source code in servicewright/core/observability/redaction.py
class ChainRedactor:
    """Applies redactors left to right: ``ChainRedactor(KeyRedactor(), ValueRedactor(m))``.

    Order matters and the conventional order is key-based first: sensitive
    fields are already collapsed to the mask before the (potentially more
    expensive) value masker sees the payload.
    """

    def __init__(self, *redactors: Redactor) -> None:
        self._redactors = redactors

    def __call__(self, data: dict[str, Any]) -> dict[str, Any]:
        """Return ``data`` passed through every redactor in order."""
        for redactor in self._redactors:
            data = redactor(data)
        return data

__call__(data)

Return data passed through every redactor in order.

Source code in servicewright/core/observability/redaction.py
def __call__(self, data: dict[str, Any]) -> dict[str, Any]:
    """Return ``data`` passed through every redactor in order."""
    for redactor in self._redactors:
        data = redactor(data)
    return data

CleanupTimeoutError

Bases: ServiceWrightError, TimeoutError

Raised when graceful cleanup does not finish within the allotted timeout.

Source code in servicewright/core/exceptions.py
class CleanupTimeoutError(ServiceWrightError, TimeoutError):
    """Raised when graceful cleanup does not finish within the allotted timeout."""

ContextSetter

Bases: Protocol

Pushes context values into an external system (logging, tracing, ...).

Implementations receive the full per-unit context dictionary and return a cleanup callable that undoes the binding when the unit of work finishes.

Source code in servicewright/core/context.py
class ContextSetter(Protocol):
    """Pushes context values into an external system (logging, tracing, ...).

    Implementations receive the full per-unit context dictionary and return a
    cleanup callable that undoes the binding when the unit of work finishes.
    """

    def set(self, context_data: dict[str, Any]) -> Callable[[], None]:
        """Set the context values and return a cleanup callable that resets them."""
        ...

set(context_data)

Set the context values and return a cleanup callable that resets them.

Source code in servicewright/core/context.py
def set(self, context_data: dict[str, Any]) -> Callable[[], None]:
    """Set the context values and return a cleanup callable that resets them."""
    ...

DaemonEntrypoint

Bases: ScopedEntrypoint

Runs func(scope, stop) in one long-lived unit scope.

The function is expected to loop until stop is set.

Source code in servicewright/adapters/builtin/daemon.py
class DaemonEntrypoint(ScopedEntrypoint):
    """Runs ``func(scope, stop)`` in one long-lived unit scope.

    The function is expected to loop until ``stop`` is set.
    """

    def __init__(
        self,
        func: Callable[[UnitScopeProtocol, asyncio.Event], Awaitable[None]],
        *,
        kind: str = "daemon",
        essential: bool = True,
    ) -> None:
        super().__init__()
        self._func = func
        self.kind = kind
        self.essential = essential

    async def serve(self, *, stop: asyncio.Event) -> None:
        """Open one unit scope and hand control to the user loop."""
        async with self.unit_scope() as scope:
            await self._func(scope, stop)

bind(ctx) async

Capture the container so per-unit scopes can be opened.

Source code in servicewright/core/contracts/bases.py
async def bind(self, ctx: ServiceContext) -> None:
    """Capture the container so per-unit scopes can be opened."""
    self._container = ctx.container

drain(grace) async

Stop intake; let in-flight units finish.

Source code in servicewright/core/contracts/bases.py
async def drain(self, grace: float) -> None:
    """Stop intake; let in-flight units finish."""
    return None

serve(*, stop) async

Open one unit scope and hand control to the user loop.

Source code in servicewright/adapters/builtin/daemon.py
async def serve(self, *, stop: asyncio.Event) -> None:
    """Open one unit scope and hand control to the user loop."""
    async with self.unit_scope() as scope:
        await self._func(scope, stop)

stop() async

Hard stop / release resources.

Source code in servicewright/core/contracts/bases.py
async def stop(self) -> None:
    """Hard stop / release resources."""
    return None

unit_scope(context=None)

Open a per-unit-of-work DI scope.

Raises:

Type Description
RuntimeError

If called before :meth:bind.

Source code in servicewright/core/contracts/bases.py
def unit_scope(
    self, context: Mapping[Any, Any] | None = None
) -> contextlib.AbstractAsyncContextManager[UnitScopeProtocol]:
    """Open a per-unit-of-work DI scope.

    Raises:
        RuntimeError: If called before :meth:`bind`.
    """
    if self._container is None:
        raise RuntimeError("unit_scope() called before bind(); entrypoint is not bound to a container")
    return self._container.unit_scope(context)

DependencyContainerProtocol

Bases: Protocol

Protocol for a DI container exposing the two scope tiers.

Source code in servicewright/core/contracts/container.py
class DependencyContainerProtocol(Protocol):
    """Protocol for a DI container exposing the two scope tiers."""

    def app_scope(self) -> contextlib.AbstractAsyncContextManager[AppScopeProtocol]:
        """Return async context manager for the process-lifetime application scope."""
        ...

    def unit_scope(
        self, context: Mapping[Any, Any] | None = None
    ) -> contextlib.AbstractAsyncContextManager[UnitScopeProtocol]:
        """Return async context manager for a per-unit-of-work scope.

        ``context`` keys are container-defined: string-keyed payloads and
        type-keyed contexts (e.g. dishka's ``{Request: request}``) both fit.
        """
        ...

app_scope()

Return async context manager for the process-lifetime application scope.

Source code in servicewright/core/contracts/container.py
def app_scope(self) -> contextlib.AbstractAsyncContextManager[AppScopeProtocol]:
    """Return async context manager for the process-lifetime application scope."""
    ...

unit_scope(context=None)

Return async context manager for a per-unit-of-work scope.

context keys are container-defined: string-keyed payloads and type-keyed contexts (e.g. dishka's {Request: request}) both fit.

Source code in servicewright/core/contracts/container.py
def unit_scope(
    self, context: Mapping[Any, Any] | None = None
) -> contextlib.AbstractAsyncContextManager[UnitScopeProtocol]:
    """Return async context manager for a per-unit-of-work scope.

    ``context`` keys are container-defined: string-keyed payloads and
    type-keyed contexts (e.g. dishka's ``{Request: request}``) both fit.
    """
    ...

DrainTimeoutError

Bases: ServiceWrightError, TimeoutError

Raised when an entrypoint does not drain in-flight work within the grace window.

Source code in servicewright/core/exceptions.py
class DrainTimeoutError(ServiceWrightError, TimeoutError):
    """Raised when an entrypoint does not drain in-flight work within the grace window."""

Entrypoint

Bases: Protocol

Host-facing protocol implemented by every driver.

Source code in servicewright/core/contracts/entrypoint.py
@runtime_checkable
class Entrypoint(Protocol):
    """Host-facing protocol implemented by every driver."""

    kind: str
    """Telemetry label only ("http"|"grpc"|"scheduler"|"kafka"|...)."""

    essential: bool
    """If True, this entrypoint's failure/exit stops the whole process.

    A failure also propagates out of ``Host.run`` once cleanup is done, so the
    process exit code distinguishes a crash from a graceful stop.
    """

    async def bind(self, ctx: ServiceContext) -> None:
        """Allocate/subscribe/register. No traffic is accepted yet.

        Raise here if the resource cannot be acquired (a port already in use, a
        missing topic): the Host aborts startup instead of reporting ready.
        """
        ...

    async def serve(self, *, stop: asyncio.Event) -> None:
        """Run until ``stop`` is set, then return **without** shutting down.

        Returning is the signal that the entrypoint is still accepting work and
        is ready to be torn down in order: the Host flips readiness to false
        first (so load balancers stop routing), then calls :meth:`drain`, then
        :meth:`stop`. Closing listeners here instead would make ``drain(grace)``
        inert and would take the readiness endpoint down before the router knows.

        Raise to report a fatal serve-time failure.
        """
        ...

    async def drain(self, grace: float) -> None:
        """Stop intake and let in-flight units finish within ``grace`` seconds."""
        ...

    async def stop(self) -> None:
        """Hard stop / release resources."""
        ...

essential instance-attribute

If True, this entrypoint's failure/exit stops the whole process.

A failure also propagates out of Host.run once cleanup is done, so the process exit code distinguishes a crash from a graceful stop.

kind instance-attribute

Telemetry label only ("http"|"grpc"|"scheduler"|"kafka"|...).

bind(ctx) async

Allocate/subscribe/register. No traffic is accepted yet.

Raise here if the resource cannot be acquired (a port already in use, a missing topic): the Host aborts startup instead of reporting ready.

Source code in servicewright/core/contracts/entrypoint.py
async def bind(self, ctx: ServiceContext) -> None:
    """Allocate/subscribe/register. No traffic is accepted yet.

    Raise here if the resource cannot be acquired (a port already in use, a
    missing topic): the Host aborts startup instead of reporting ready.
    """
    ...

drain(grace) async

Stop intake and let in-flight units finish within grace seconds.

Source code in servicewright/core/contracts/entrypoint.py
async def drain(self, grace: float) -> None:
    """Stop intake and let in-flight units finish within ``grace`` seconds."""
    ...

serve(*, stop) async

Run until stop is set, then return without shutting down.

Returning is the signal that the entrypoint is still accepting work and is ready to be torn down in order: the Host flips readiness to false first (so load balancers stop routing), then calls :meth:drain, then :meth:stop. Closing listeners here instead would make drain(grace) inert and would take the readiness endpoint down before the router knows.

Raise to report a fatal serve-time failure.

Source code in servicewright/core/contracts/entrypoint.py
async def serve(self, *, stop: asyncio.Event) -> None:
    """Run until ``stop`` is set, then return **without** shutting down.

    Returning is the signal that the entrypoint is still accepting work and
    is ready to be torn down in order: the Host flips readiness to false
    first (so load balancers stop routing), then calls :meth:`drain`, then
    :meth:`stop`. Closing listeners here instead would make ``drain(grace)``
    inert and would take the readiness endpoint down before the router knows.

    Raise to report a fatal serve-time failure.
    """
    ...

stop() async

Hard stop / release resources.

Source code in servicewright/core/contracts/entrypoint.py
async def stop(self) -> None:
    """Hard stop / release resources."""
    ...

ErrorInfo dataclass

Normalized view of one failure, ready for rendering.

Attributes:

Name Type Description
kind ErrorKind

The failure category.

code str

Machine-readable error code.

detail str | None

Human-readable message (None -> the renderer supplies a generic phrase).

params Mapping[str, Any]

Structured, JSON-safe details.

public bool

Whether the details may be shown to the client.

status_override int | None

Explicit HTTP status taking precedence over the kind's default (e.g. 422 for request validation).

headers Mapping[str, str] | None

Extra response headers (e.g. from an HTTPException).

Source code in servicewright/core/errors.py
@dataclass(frozen=True, slots=True)
class ErrorInfo:
    """Normalized view of one failure, ready for rendering.

    Attributes:
        kind: The failure category.
        code: Machine-readable error code.
        detail: Human-readable message (``None`` -> the renderer supplies a
            generic phrase).
        params: Structured, JSON-safe details.
        public: Whether the details may be shown to the client.
        status_override: Explicit HTTP status taking precedence over the kind's
            default (e.g. 422 for request validation).
        headers: Extra response headers (e.g. from an ``HTTPException``).
    """

    kind: ErrorKind
    code: str
    detail: str | None = None
    params: Mapping[str, Any] = field(default_factory=dict)
    public: bool = True
    status_override: int | None = None
    headers: Mapping[str, str] | None = None

    @classmethod
    def from_service_error(cls, exc: ServiceError) -> ErrorInfo:
        """Build the normalized view of a :class:`ServiceError`."""
        return cls(
            kind=exc.kind,
            code=exc.code or INTERNAL_ERROR_CODE,
            detail=exc.detail,
            params=exc.params,
            public=exc.public,
        )

    @property
    def http_status(self) -> int:
        """The HTTP status: the override if set, else the kind's default."""
        return self.status_override if self.status_override is not None else HTTP_STATUS_BY_KIND[self.kind]

http_status property

The HTTP status: the override if set, else the kind's default.

from_service_error(exc) classmethod

Build the normalized view of a :class:ServiceError.

Source code in servicewright/core/errors.py
@classmethod
def from_service_error(cls, exc: ServiceError) -> ErrorInfo:
    """Build the normalized view of a :class:`ServiceError`."""
    return cls(
        kind=exc.kind,
        code=exc.code or INTERNAL_ERROR_CODE,
        detail=exc.detail,
        params=exc.params,
        public=exc.public,
    )

ErrorKind

Bases: StrEnum

Transport-neutral failure category (maps to HTTP and gRPC statuses).

Source code in servicewright/core/errors.py
class ErrorKind(enum.StrEnum):
    """Transport-neutral failure category (maps to HTTP and gRPC statuses)."""

    INVALID = "invalid"
    UNAUTHENTICATED = "unauthenticated"
    FORBIDDEN = "forbidden"
    NOT_FOUND = "not_found"
    CONFLICT = "conflict"
    PRECONDITION_FAILED = "precondition_failed"
    TOO_MANY_REQUESTS = "too_many_requests"
    DEADLINE_EXCEEDED = "deadline_exceeded"
    UNAVAILABLE = "unavailable"
    NOT_IMPLEMENTED = "not_implemented"
    INTERNAL = "internal"

ErrorTrackingSettingsProtocol

Bases: Protocol

Error-tracking concern: reporting endpoint and sampling.

Source code in servicewright/core/observability/protocols.py
class ErrorTrackingSettingsProtocol(Protocol):
    """Error-tracking concern: reporting endpoint and sampling."""

    @property
    def dsn(self) -> str | None: ...

    @property
    def environment(self) -> str: ...

    @property
    def traces_sample_rate(self) -> float: ...

    @property
    def profiles_sample_rate(self) -> float: ...

    @property
    def debug(self) -> bool: ...

HealthCheckerProtocol

Bases: Protocol

Protocol for component health checks (DB, Redis, etc.).

Source code in servicewright/core/contracts/health.py
class HealthCheckerProtocol(Protocol):
    """Protocol for component health checks (DB, Redis, etc.)."""

    async def check(self) -> bool:
        """Check component health. Returns True if healthy."""
        ...

check() async

Check component health. Returns True if healthy.

Source code in servicewright/core/contracts/health.py
async def check(self) -> bool:
    """Check component health. Returns True if healthy."""
    ...

HealthRegistry

Holds readiness state and named component health checks.

Liveness reflects only that the process is alive and is independent of the registered checks. Readiness requires both the ready flag (flipped on by the Host once serving) and every registered check passing.

Source code in servicewright/core/health/registry.py
class HealthRegistry:
    """Holds readiness state and named component health checks.

    Liveness reflects only that the process is alive and is independent of the
    registered checks. Readiness requires both the ``ready`` flag (flipped on
    by the Host once serving) and every registered check passing.
    """

    def __init__(self, *, readiness_cache_ttl: float = 0.0) -> None:
        """Initialize the registry.

        Args:
            readiness_cache_ttl: When > 0, readiness results are cached for this
                many seconds to avoid hammering downstream checks.
        """
        self.ready: bool = False
        self._checks: dict[str, HealthCheckerProtocol] = {}
        self._readiness_cache_ttl = readiness_cache_ttl
        self._cached_report: HealthReport | None = None
        self._cached_at: float = 0.0

    def add_check(self, name: str, check: HealthCheckerProtocol) -> None:
        """Register a named component health check.

        Args:
            name: Unique label for the check (e.g. ``"postgres"``).
            check: Object implementing :class:`HealthCheckerProtocol`.
        """
        if name in self._checks:
            raise ValueError(f"Health check '{name}' is already registered")
        self._checks[name] = check
        self._invalidate_cache()

    @property
    def checks(self) -> dict[str, HealthCheckerProtocol]:
        """Return a copy of the registered checks mapping."""
        return dict(self._checks)

    def _invalidate_cache(self) -> None:
        self._cached_report = None
        self._cached_at = 0.0

    async def liveness(self) -> HealthReport:
        """Report process liveness (always healthy while the loop runs)."""
        return HealthReport(healthy=True, checks={})

    async def readiness(self) -> HealthReport:
        """Report readiness: ``ready`` flag AND all checks passing.

        Checks run concurrently; any check raising is treated as a failure.
        """
        if (
            self._readiness_cache_ttl > 0
            and self._cached_report is not None
            and (time.monotonic() - self._cached_at) < self._readiness_cache_ttl
        ):
            return self._cached_report

        results = await self._run_checks()
        healthy = self.ready and all(results.values())
        report = HealthReport(healthy=healthy, checks=results)

        if self._readiness_cache_ttl > 0:
            self._cached_report = report
            self._cached_at = time.monotonic()

        return report

    async def _run_checks(self) -> dict[str, bool]:
        if not self._checks:
            return {}

        names = list(self._checks)
        outcomes = await asyncio.gather(
            *(self._safe_check(name) for name in names),
            return_exceptions=False,
        )
        return dict(zip(names, outcomes, strict=True))

    async def _safe_check(self, name: str) -> bool:
        check = self._checks[name]
        try:
            return bool(await check.check())
        except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
            raise
        except Exception:
            logger.exception("Health check failed", extra={"check": name})
            return False

checks property

Return a copy of the registered checks mapping.

__init__(*, readiness_cache_ttl=0.0)

Initialize the registry.

Parameters:

Name Type Description Default
readiness_cache_ttl float

When > 0, readiness results are cached for this many seconds to avoid hammering downstream checks.

0.0
Source code in servicewright/core/health/registry.py
def __init__(self, *, readiness_cache_ttl: float = 0.0) -> None:
    """Initialize the registry.

    Args:
        readiness_cache_ttl: When > 0, readiness results are cached for this
            many seconds to avoid hammering downstream checks.
    """
    self.ready: bool = False
    self._checks: dict[str, HealthCheckerProtocol] = {}
    self._readiness_cache_ttl = readiness_cache_ttl
    self._cached_report: HealthReport | None = None
    self._cached_at: float = 0.0

add_check(name, check)

Register a named component health check.

Parameters:

Name Type Description Default
name str

Unique label for the check (e.g. "postgres").

required
check HealthCheckerProtocol

Object implementing :class:HealthCheckerProtocol.

required
Source code in servicewright/core/health/registry.py
def add_check(self, name: str, check: HealthCheckerProtocol) -> None:
    """Register a named component health check.

    Args:
        name: Unique label for the check (e.g. ``"postgres"``).
        check: Object implementing :class:`HealthCheckerProtocol`.
    """
    if name in self._checks:
        raise ValueError(f"Health check '{name}' is already registered")
    self._checks[name] = check
    self._invalidate_cache()

liveness() async

Report process liveness (always healthy while the loop runs).

Source code in servicewright/core/health/registry.py
async def liveness(self) -> HealthReport:
    """Report process liveness (always healthy while the loop runs)."""
    return HealthReport(healthy=True, checks={})

readiness() async

Report readiness: ready flag AND all checks passing.

Checks run concurrently; any check raising is treated as a failure.

Source code in servicewright/core/health/registry.py
async def readiness(self) -> HealthReport:
    """Report readiness: ``ready`` flag AND all checks passing.

    Checks run concurrently; any check raising is treated as a failure.
    """
    if (
        self._readiness_cache_ttl > 0
        and self._cached_report is not None
        and (time.monotonic() - self._cached_at) < self._readiness_cache_ttl
    ):
        return self._cached_report

    results = await self._run_checks()
    healthy = self.ready and all(results.values())
    report = HealthReport(healthy=healthy, checks=results)

    if self._readiness_cache_ttl > 0:
        self._cached_report = report
        self._cached_at = time.monotonic()

    return report

HealthReport dataclass

Result of a liveness or readiness probe.

Attributes:

Name Type Description
healthy bool

Aggregate health flag.

checks dict[str, bool]

Per-named-check results (empty for liveness).

Source code in servicewright/core/health/report.py
@dataclass(slots=True, frozen=True)
class HealthReport:
    """Result of a liveness or readiness probe.

    Attributes:
        healthy: Aggregate health flag.
        checks: Per-named-check results (empty for liveness).
    """

    healthy: bool
    checks: dict[str, bool] = field(default_factory=dict)

    @property
    def status(self) -> ProbeStatus:
        """Coarse probe status derived from :attr:`healthy`."""
        return ProbeStatus.from_bool(self.healthy)

status property

Coarse probe status derived from :attr:healthy.

Host

Runs an :class:AppSpec plus a list of :class:Entrypoint drivers.

Owns the unified lifecycle: Bootstrap -> Warmup -> Ready -> Serve -> Drain -> Cleanup. It treats every entrypoint identically and never branches on kind.

The run-loop reports failure by raising, so the process exit code is meaningful: an essential entrypoint that dies during serve propagates its exception out of :meth:run (after cleanup), and a shutdown step that blows past its budget raises :class:DrainTimeoutError/:class:CleanupTimeoutError when nothing else is already propagating.

Source code in servicewright/core/aio/host.py
class Host[TSettings: "BaseServiceSettingsProtocol", TContainer: "DependencyContainerProtocol"]:
    """Runs an :class:`AppSpec` plus a list of :class:`Entrypoint` drivers.

    Owns the unified lifecycle: Bootstrap -> Warmup -> Ready -> Serve -> Drain
    -> Cleanup. It treats every entrypoint identically and never branches on
    ``kind``.

    The run-loop reports failure by raising, so the process exit code is
    meaningful: an essential entrypoint that dies during ``serve`` propagates its
    exception out of :meth:`run` (after cleanup), and a shutdown step that blows
    past its budget raises :class:`DrainTimeoutError`/:class:`CleanupTimeoutError`
    when nothing else is already propagating.
    """

    def __init__(self, spec: AppSpec[TSettings, TContainer]) -> None:
        self.spec = spec
        self._entrypoints: list[Entrypoint] = []
        self._bound: list[Entrypoint] = []

    def add_entrypoint(self, entrypoint: Entrypoint) -> None:
        """Append an entrypoint (typically from a plugin's ``on_register``)."""
        self._entrypoints.append(entrypoint)

    def bootstrap(self, settings: TSettings) -> BootstrapContext[TSettings, TContainer]:
        """Build the container (the application scope is not yet entered)."""
        return BootstrapContext(
            settings=settings,
            service_name=self.spec.service_name,
            container=self.spec.create_container(settings),
            lifecycle=self.spec.lifecycle,
        )

    async def run(
        self,
        settings: TSettings,
        entrypoints: Iterable[Entrypoint] = (),
        *,
        plugins: Iterable[Plugin] = (),
        stop: asyncio.Event | None = None,
    ) -> None:
        """Run the full lifecycle, blocking until ``stop`` is set.

        Args:
            settings: Service settings.
            entrypoints: Drivers to run.
            plugins: Plugins applied via ``on_register`` before the run-loop.
            stop: Externally supplied stop event. When provided, OS signal
                handlers are NOT installed (the embedding/test path).

        Raises:
            Exception: Whatever an essential entrypoint raised while serving, or
                whatever startup raised — after cleanup has run.
            ServiceWrightError: If a shutdown step exceeded its budget and no
                other exception is propagating.
        """
        self._entrypoints = list(entrypoints)
        self._bound = []
        for plugin in plugins:
            plugin.on_register(self.spec, self)

        self.spec.observability.configure(settings, service_name=self.spec.service_name)
        uninstall_signal_handlers: Callable[[], None] | None = None
        timeouts: list[ServiceWrightError] = []
        try:
            bootstrap_ctx = self.bootstrap(settings)

            if stop is None:
                stop = asyncio.Event()
                uninstall_signal_handlers = install_signal_handlers(stop)

            await self._run_with_app_scope(bootstrap_ctx, stop)
        finally:
            if uninstall_signal_handlers is not None:
                uninstall_signal_handlers()
            timeouts.extend(await self._final_cleanup())

        # Only reachable when nothing else is propagating: a shutdown that blew
        # its budget must not mask the failure that caused the shutdown.
        if timeouts:
            raise timeouts[0]

    async def _final_cleanup(self) -> list[ServiceWrightError]:
        """Flush observability and run post-shutdown hooks, both time-boxed."""
        budget = self.spec.cleanup_timeout_seconds
        timeouts: list[ServiceWrightError] = []
        # Sink shutdown flushes (traces, sentry events) and joins the metrics
        # server thread — off the loop so it can never block the last steps.
        timeouts.extend(
            await self._shutdown_step(
                asyncio.to_thread(self.spec.observability.shutdown),
                budget=budget,
                error=CleanupTimeoutError,
                phase="observability shutdown",
            )
        )
        timeouts.extend(
            await self._shutdown_step(
                self.spec.lifecycle.run_post_shutdown_hooks(None),
                budget=budget,
                error=CleanupTimeoutError,
                phase="post-shutdown hooks",
            )
        )
        return timeouts

    async def _run_with_app_scope(
        self,
        bootstrap_ctx: BootstrapContext[TSettings, TContainer],
        stop: asyncio.Event,
    ) -> None:
        async with bootstrap_ctx.container.app_scope() as app_scope:
            service_ctx: ServiceContext[TSettings, TContainer] = ServiceContext(
                bootstrap=bootstrap_ctx,
                app_scope=app_scope,
                health=self.spec.health,
                observability=self.spec.observability,
            )
            timeouts: list[ServiceWrightError] = []
            try:
                if await self._startup(service_ctx, self._entrypoints, stop):
                    await self._serve(self._entrypoints, stop)
            finally:
                timeouts = await self._shutdown_in_scope(service_ctx)

            if timeouts:
                raise timeouts[0]

    async def _startup(
        self,
        service_ctx: ServiceContext[TSettings, TContainer],
        entrypoints: Sequence[Entrypoint],
        stop: asyncio.Event,
    ) -> bool:
        """Warm up, bind every entrypoint and flip readiness.

        Returns:
            ``True`` when the service is fully started and should serve.
            ``False`` when ``stop`` arrived first — startup is abandoned at the
            next phase boundary rather than binding ports and flipping readiness
            on a process that has already been asked to terminate.
        """
        if stop.is_set():
            return self._abandon_startup("before warmup")

        await self._collect_and_warmup(service_ctx, stop)
        if stop.is_set():
            return self._abandon_startup("during warmup")

        await self.spec.lifecycle.run_pre_start_hooks(service_ctx.app_scope)
        for entrypoint in entrypoints:
            if stop.is_set():
                return self._abandon_startup("during bind")
            # Recorded before the await: a bind that fails halfway through has
            # already allocated something, so it must still be torn down.
            self._bound.append(entrypoint)
            await entrypoint.bind(service_ctx)

        if stop.is_set():
            return self._abandon_startup("after bind")

        self.spec.health.ready = True
        await self.spec.lifecycle.run_post_start_hooks(service_ctx.app_scope)
        logger.info("Service ready", extra={"service": self.spec.service_name})
        return True

    def _abandon_startup(self, phase: str) -> bool:
        logger.info(
            "Stop requested during startup; skipping serve",
            extra={"service": self.spec.service_name, "phase": phase},
        )
        return False

    async def _collect_and_warmup(
        self,
        service_ctx: ServiceContext[TSettings, TContainer],
        stop: asyncio.Event,
    ) -> None:
        """Prime every warmer, abandoning the wait as soon as ``stop`` is set."""
        warmers = await collect_warmers(
            base_warmers=self.spec.warmers,
            warmers_factory=self.spec.warmers_factory,
            app_ctx=service_ctx,
        )
        warmup = asyncio.ensure_future(perform_warmup(self.spec.service_name, list(warmers)))
        waiter = asyncio.ensure_future(stop.wait())
        try:
            await asyncio.wait({warmup, waiter}, return_when=asyncio.FIRST_COMPLETED)
        finally:
            waiter.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await waiter

        if warmup.done():
            await warmup  # Propagate a warmup failure to the caller of run().
            return

        warmup.cancel()
        with contextlib.suppress(asyncio.CancelledError):
            await warmup
        logger.warning(
            "Warmup abandoned: stop requested",
            extra={"service": self.spec.service_name},
        )

    async def _serve(self, entrypoints: Sequence[Entrypoint], stop: asyncio.Event) -> None:
        """Serve every entrypoint until ``stop``; re-raise an essential failure."""
        if not entrypoints:
            await stop.wait()
            return

        async def runner(entrypoint: Entrypoint) -> None:
            try:
                await entrypoint.serve(stop=stop)
            except asyncio.CancelledError:
                raise
            except Exception:
                logger.exception(
                    "Entrypoint serve failed",
                    extra={"service": self.spec.service_name, "kind": entrypoint.kind},
                )
                if entrypoint.essential:
                    stop.set()
                    raise
                return
            if entrypoint.essential and not stop.is_set():
                logger.info(
                    "Essential entrypoint exited; stopping service",
                    extra={"service": self.spec.service_name, "kind": entrypoint.kind},
                )
                stop.set()

        failure: BaseException | None = None
        try:
            async with asyncio.TaskGroup() as group:
                for entrypoint in entrypoints:
                    group.create_task(runner(entrypoint))
        except* Exception as eg:
            logger.exception(
                "Serve loop aborted by entrypoint failure",
                extra={"service": self.spec.service_name, "errors": len(eg.exceptions)},
            )
            failure = _sole_cause(eg)

        # Only essential failures reach here (runner() swallows the rest), and an
        # essential failure must reach the caller: a process that dies mid-serve
        # may not exit 0, or every exit-code-based supervisor reads it as success.
        if failure is not None:
            raise failure

    async def _shutdown_in_scope(
        self,
        service_ctx: ServiceContext[TSettings, TContainer],
    ) -> list[ServiceWrightError]:
        """Drain, stop and run pre-shutdown hooks; return any budget overruns."""
        # Stop routing first (k8s/LB) before we stop accepting work.
        self.spec.health.ready = False

        grace = self.spec.drain_grace_seconds
        budget = self.spec.cleanup_timeout_seconds
        timeouts: list[ServiceWrightError] = []

        for entrypoint in reversed(self._bound):
            timeouts.extend(
                await self._shutdown_step(
                    entrypoint.drain(grace),
                    budget=grace + DEFAULT_DRAIN_TIMEOUT_BUFFER_SECONDS,
                    error=DrainTimeoutError,
                    phase="drain",
                    kind=entrypoint.kind,
                )
            )

        for entrypoint in reversed(self._bound):
            timeouts.extend(
                await self._shutdown_step(
                    entrypoint.stop(),
                    budget=budget,
                    error=CleanupTimeoutError,
                    phase="stop",
                    kind=entrypoint.kind,
                )
            )

        timeouts.extend(
            await self._shutdown_step(
                self.spec.lifecycle.run_pre_shutdown_hooks(service_ctx.app_scope),
                budget=budget,
                error=CleanupTimeoutError,
                phase="pre-shutdown hooks",
            )
        )

        logger.info("Service shutdown complete", extra={"service": self.spec.service_name})
        return timeouts

    async def _shutdown_step(
        self,
        step: Coroutine[object, object, None] | Awaitable[None],
        *,
        budget: float,
        error: type[ServiceWrightError],
        phase: str,
        kind: str | None = None,
    ) -> list[ServiceWrightError]:
        """Await one shutdown step under a budget, never letting it abort the rest.

        A failing step is logged and skipped so the remaining entrypoints still
        get their turn; a step that exceeds ``budget`` is returned to the caller,
        which raises it once every other step has had its chance.
        """
        extra = {"service": self.spec.service_name, "phase": phase, "kind": kind, "budget": budget}
        try:
            await asyncio.wait_for(step, timeout=budget)
        except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
            raise
        except TimeoutError as exc:
            timeout_error = error(f"{phase} did not finish within {budget}s")
            timeout_error.__cause__ = exc
            logger.warning("Shutdown step timed out", extra=extra, exc_info=timeout_error)
            return [timeout_error]
        except Exception:
            logger.exception("Shutdown step failed", extra=extra)
        return []

add_entrypoint(entrypoint)

Append an entrypoint (typically from a plugin's on_register).

Source code in servicewright/core/aio/host.py
def add_entrypoint(self, entrypoint: Entrypoint) -> None:
    """Append an entrypoint (typically from a plugin's ``on_register``)."""
    self._entrypoints.append(entrypoint)

bootstrap(settings)

Build the container (the application scope is not yet entered).

Source code in servicewright/core/aio/host.py
def bootstrap(self, settings: TSettings) -> BootstrapContext[TSettings, TContainer]:
    """Build the container (the application scope is not yet entered)."""
    return BootstrapContext(
        settings=settings,
        service_name=self.spec.service_name,
        container=self.spec.create_container(settings),
        lifecycle=self.spec.lifecycle,
    )

run(settings, entrypoints=(), *, plugins=(), stop=None) async

Run the full lifecycle, blocking until stop is set.

Parameters:

Name Type Description Default
settings TSettings

Service settings.

required
entrypoints Iterable[Entrypoint]

Drivers to run.

()
plugins Iterable[Plugin]

Plugins applied via on_register before the run-loop.

()
stop Event | None

Externally supplied stop event. When provided, OS signal handlers are NOT installed (the embedding/test path).

None

Raises:

Type Description
Exception

Whatever an essential entrypoint raised while serving, or whatever startup raised — after cleanup has run.

ServiceWrightError

If a shutdown step exceeded its budget and no other exception is propagating.

Source code in servicewright/core/aio/host.py
async def run(
    self,
    settings: TSettings,
    entrypoints: Iterable[Entrypoint] = (),
    *,
    plugins: Iterable[Plugin] = (),
    stop: asyncio.Event | None = None,
) -> None:
    """Run the full lifecycle, blocking until ``stop`` is set.

    Args:
        settings: Service settings.
        entrypoints: Drivers to run.
        plugins: Plugins applied via ``on_register`` before the run-loop.
        stop: Externally supplied stop event. When provided, OS signal
            handlers are NOT installed (the embedding/test path).

    Raises:
        Exception: Whatever an essential entrypoint raised while serving, or
            whatever startup raised — after cleanup has run.
        ServiceWrightError: If a shutdown step exceeded its budget and no
            other exception is propagating.
    """
    self._entrypoints = list(entrypoints)
    self._bound = []
    for plugin in plugins:
        plugin.on_register(self.spec, self)

    self.spec.observability.configure(settings, service_name=self.spec.service_name)
    uninstall_signal_handlers: Callable[[], None] | None = None
    timeouts: list[ServiceWrightError] = []
    try:
        bootstrap_ctx = self.bootstrap(settings)

        if stop is None:
            stop = asyncio.Event()
            uninstall_signal_handlers = install_signal_handlers(stop)

        await self._run_with_app_scope(bootstrap_ctx, stop)
    finally:
        if uninstall_signal_handlers is not None:
            uninstall_signal_handlers()
        timeouts.extend(await self._final_cleanup())

    # Only reachable when nothing else is propagating: a shutdown that blew
    # its budget must not mask the failure that caused the shutdown.
    if timeouts:
        raise timeouts[0]

HttpErrorRendererProtocol

Bases: Protocol

Turns a normalized :class:ErrorInfo into a wire response.

Implement this to own the error wire format end to end — a custom envelope, localized messages resolved from info.code + info.params, extra fields. Every default exception handler renders through the configured renderer, so one implementation switches the whole surface.

Source code in servicewright/core/errors.py
class HttpErrorRendererProtocol(Protocol):
    """Turns a normalized :class:`ErrorInfo` into a wire response.

    Implement this to own the error wire format end to end — a custom envelope,
    localized messages resolved from ``info.code`` + ``info.params``, extra
    fields. Every default exception handler renders through the configured
    renderer, so one implementation switches the whole surface.
    """

    def render(self, info: ErrorInfo) -> RenderedError:
        """Render one failure (already masked by the caller when non-public)."""
        ...

render(info)

Render one failure (already masked by the caller when non-public).

Source code in servicewright/core/errors.py
def render(self, info: ErrorInfo) -> RenderedError:
    """Render one failure (already masked by the caller when non-public)."""
    ...

KafkaProducerWarmupError

Bases: WarmupError

Raised when Kafka producer warmup fails.

Source code in servicewright/core/exceptions.py
class KafkaProducerWarmupError(WarmupError):
    """Raised when Kafka producer warmup fails."""

KeyRedactor

Masks values whose key contains a sensitive fragment (case-insensitive).

The match is by substring, which is what makes password cover password_hash and token cover access_token — but a short fragment such as code also masks status_code and error_code. safe_keys is the way to say that an exact name is not a secret: those names are never masked, whatever the fragments match.

The whole structure is walked — nested dicts and values inside lists and tuples. That matters because the payloads this redactor is threaded into are list-shaped where it counts: a Sentry event keeps stack-frame locals under exception.values[i].stacktrace.frames[j].vars and breadcrumb payloads under breadcrumbs.values[i].data, so a redactor that only recursed into dicts would mask the flat extra block and ship the locals in plaintext.

Parameters:

Name Type Description Default
sensitive_keys frozenset[str] | set[str]

Fragments that make a key sensitive, matched as case-insensitive substrings of the key name.

DEFAULT_SENSITIVE_KEYS
mask str

Value written in place of a sensitive one.

MASK
safe_keys frozenset[str] | set[str]

Exact key names, compared case-insensitively, that are never masked. Checked before the fragments.

frozenset()
Source code in servicewright/core/observability/redaction.py
class KeyRedactor:
    """Masks values whose key contains a sensitive fragment (case-insensitive).

    The match is by substring, which is what makes ``password`` cover
    ``password_hash`` and ``token`` cover ``access_token`` — but a short fragment
    such as ``code`` also masks ``status_code`` and ``error_code``. ``safe_keys``
    is the way to say that an exact name is not a secret: those names are never
    masked, whatever the fragments match.

    The whole structure is walked — nested dicts **and** values inside lists and
    tuples. That matters because the payloads this redactor is threaded into are
    list-shaped where it counts: a Sentry event keeps stack-frame locals under
    ``exception.values[i].stacktrace.frames[j].vars`` and breadcrumb payloads
    under ``breadcrumbs.values[i].data``, so a redactor that only recursed into
    dicts would mask the flat ``extra`` block and ship the locals in plaintext.

    Args:
        sensitive_keys: Fragments that make a key sensitive, matched as
            case-insensitive substrings of the key name.
        mask: Value written in place of a sensitive one.
        safe_keys: Exact key names, compared case-insensitively, that are never
            masked. Checked before the fragments.
    """

    def __init__(
        self,
        sensitive_keys: frozenset[str] | set[str] = DEFAULT_SENSITIVE_KEYS,
        mask: str = MASK,
        *,
        safe_keys: frozenset[str] | set[str] = frozenset(),
    ) -> None:
        self._sensitive_keys = frozenset(key.lower() for key in sensitive_keys)
        self._mask = mask
        self._safe_keys = frozenset(key.lower() for key in safe_keys)

    def __call__(self, data: dict[str, Any]) -> dict[str, Any]:
        """Return a redacted copy of ``data``."""
        return self._redact_mapping(data, frozenset())

    def _redact_mapping(self, data: dict[str, Any], seen: frozenset[int]) -> dict[str, Any]:
        seen = seen | {id(data)}
        redacted: dict[str, Any] = {}
        for key, value in data.items():
            redacted[key] = self._mask if self._is_sensitive(key) else self._redact_value(value, seen)
        return redacted

    def _redact_value(self, value: Any, seen: frozenset[int]) -> Any:
        """Walk containers; leave scalars untouched. Cycles are left as-is."""
        if id(value) in seen:
            return value
        if isinstance(value, dict):
            return self._redact_mapping(value, seen)
        if isinstance(value, list):
            return [self._redact_value(item, seen | {id(value)}) for item in value]
        if isinstance(value, tuple):
            return tuple(self._redact_value(item, seen | {id(value)}) for item in value)
        return value

    def _is_sensitive(self, key: object) -> bool:
        if not isinstance(key, str):
            return False
        lowered = key.lower()
        if lowered in self._safe_keys:
            return False
        return any(fragment in lowered for fragment in self._sensitive_keys)

__call__(data)

Return a redacted copy of data.

Source code in servicewright/core/observability/redaction.py
def __call__(self, data: dict[str, Any]) -> dict[str, Any]:
    """Return a redacted copy of ``data``."""
    return self._redact_mapping(data, frozenset())

Lifecycle

Manages service lifecycle with customizable hooks.

Provides extension points for custom logic at various stages of service lifecycle without requiring inheritance.

Source code in servicewright/core/lifecycle/manager.py
class Lifecycle:
    """Manages service lifecycle with customizable hooks.

    Provides extension points for custom logic at various
    stages of service lifecycle without requiring inheritance.
    """

    def __init__(self) -> None:
        self._pre_start_hooks: list[LifecycleHookProtocol] = []
        self._post_start_hooks: list[LifecycleHookProtocol] = []
        self._pre_shutdown_hooks: list[LifecycleHookProtocol] = []
        self._post_shutdown_hooks: list[LifecycleHookProtocol] = []

    def add_pre_start_hook(self, hook: LifecycleHookProtocol) -> None:
        """Add hook to execute before service starts.

        Args:
            hook: Async callable to execute before service initialization.
        """
        self._pre_start_hooks.append(hook)

    def add_post_start_hook(self, hook: LifecycleHookProtocol) -> None:
        """Add hook to execute after service starts.

        Args:
            hook: Async callable to execute after service is ready.
        """
        self._post_start_hooks.append(hook)

    def add_pre_shutdown_hook(self, hook: LifecycleHookProtocol) -> None:
        """Add hook to execute before service shutdown.

        Args:
            hook: Async callable to execute before service stops.
        """
        self._pre_shutdown_hooks.append(hook)

    def add_post_shutdown_hook(self, hook: LifecycleHookProtocol) -> None:
        """Add hook to execute after service shutdown.

        Args:
            hook: Async callable to execute after cleanup.
        """
        self._post_shutdown_hooks.append(hook)

    async def run_pre_start_hooks(self, app_scope: AppScopeProtocol | None = None) -> None:
        """Execute all pre-start hooks in registration order.

        Aborts startup if any hook fails.

        Args:
            app_scope: Optional application scope for dependency resolution.
        """
        await self._run_hooks(self._pre_start_hooks, "Pre-start", app_scope, raise_on_failure=True)

    async def run_post_start_hooks(self, app_scope: AppScopeProtocol) -> None:
        """Execute all post-start hooks in registration order.

        Aborts startup if any hook fails.

        Args:
            app_scope: Application scope for dependency resolution.
        """
        await self._run_hooks(self._post_start_hooks, "Post-start", app_scope, raise_on_failure=True)

    async def run_pre_shutdown_hooks(self, app_scope: AppScopeProtocol | None = None) -> None:
        """Execute all pre-shutdown hooks in registration order.

        Continues with other hooks even if one fails to ensure maximum cleanup.

        Args:
            app_scope: Optional application scope for dependency resolution.
        """
        await self._run_hooks(self._pre_shutdown_hooks, "Pre-shutdown", app_scope, raise_on_failure=False)

    async def run_post_shutdown_hooks(self, app_scope: AppScopeProtocol | None = None) -> None:
        """Execute all post-shutdown hooks in registration order.

        Continues with other hooks even if one fails to ensure maximum cleanup.

        Args:
            app_scope: Optional application scope for dependency resolution.
        """
        await self._run_hooks(self._post_shutdown_hooks, "Post-shutdown", app_scope, raise_on_failure=False)

    async def _run_hooks(
        self,
        hooks: list[LifecycleHookProtocol],
        label: str,
        app_scope: AppScopeProtocol | None,
        raise_on_failure: bool = True,
    ) -> None:
        """Internal helper to execute a list of hooks.

        Args:
            hooks: List of async callables to execute.
            label: Label for logging (e.g., 'Pre-start').
            app_scope: Application scope to pass to each hook.
            raise_on_failure: Whether to re-raise exceptions or just log them.
        """
        hooks_to_run = list(hooks)

        for hook in hooks_to_run:
            try:
                # Introspect signature BEFORE calling to avoid catching TypeErrors from hook body
                sig = inspect.signature(hook)
                params = list(sig.parameters.values())

                if len(params) == 0:
                    await hook()  # type: ignore[call-arg]
                else:
                    await hook(app_scope)
            except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
                raise
            except Exception:
                logger.exception(f"{label} hook failed", extra={"hook": repr(hook)})
                if raise_on_failure:
                    raise

add_post_shutdown_hook(hook)

Add hook to execute after service shutdown.

Parameters:

Name Type Description Default
hook LifecycleHookProtocol

Async callable to execute after cleanup.

required
Source code in servicewright/core/lifecycle/manager.py
def add_post_shutdown_hook(self, hook: LifecycleHookProtocol) -> None:
    """Add hook to execute after service shutdown.

    Args:
        hook: Async callable to execute after cleanup.
    """
    self._post_shutdown_hooks.append(hook)

add_post_start_hook(hook)

Add hook to execute after service starts.

Parameters:

Name Type Description Default
hook LifecycleHookProtocol

Async callable to execute after service is ready.

required
Source code in servicewright/core/lifecycle/manager.py
def add_post_start_hook(self, hook: LifecycleHookProtocol) -> None:
    """Add hook to execute after service starts.

    Args:
        hook: Async callable to execute after service is ready.
    """
    self._post_start_hooks.append(hook)

add_pre_shutdown_hook(hook)

Add hook to execute before service shutdown.

Parameters:

Name Type Description Default
hook LifecycleHookProtocol

Async callable to execute before service stops.

required
Source code in servicewright/core/lifecycle/manager.py
def add_pre_shutdown_hook(self, hook: LifecycleHookProtocol) -> None:
    """Add hook to execute before service shutdown.

    Args:
        hook: Async callable to execute before service stops.
    """
    self._pre_shutdown_hooks.append(hook)

add_pre_start_hook(hook)

Add hook to execute before service starts.

Parameters:

Name Type Description Default
hook LifecycleHookProtocol

Async callable to execute before service initialization.

required
Source code in servicewright/core/lifecycle/manager.py
def add_pre_start_hook(self, hook: LifecycleHookProtocol) -> None:
    """Add hook to execute before service starts.

    Args:
        hook: Async callable to execute before service initialization.
    """
    self._pre_start_hooks.append(hook)

run_post_shutdown_hooks(app_scope=None) async

Execute all post-shutdown hooks in registration order.

Continues with other hooks even if one fails to ensure maximum cleanup.

Parameters:

Name Type Description Default
app_scope AppScopeProtocol | None

Optional application scope for dependency resolution.

None
Source code in servicewright/core/lifecycle/manager.py
async def run_post_shutdown_hooks(self, app_scope: AppScopeProtocol | None = None) -> None:
    """Execute all post-shutdown hooks in registration order.

    Continues with other hooks even if one fails to ensure maximum cleanup.

    Args:
        app_scope: Optional application scope for dependency resolution.
    """
    await self._run_hooks(self._post_shutdown_hooks, "Post-shutdown", app_scope, raise_on_failure=False)

run_post_start_hooks(app_scope) async

Execute all post-start hooks in registration order.

Aborts startup if any hook fails.

Parameters:

Name Type Description Default
app_scope AppScopeProtocol

Application scope for dependency resolution.

required
Source code in servicewright/core/lifecycle/manager.py
async def run_post_start_hooks(self, app_scope: AppScopeProtocol) -> None:
    """Execute all post-start hooks in registration order.

    Aborts startup if any hook fails.

    Args:
        app_scope: Application scope for dependency resolution.
    """
    await self._run_hooks(self._post_start_hooks, "Post-start", app_scope, raise_on_failure=True)

run_pre_shutdown_hooks(app_scope=None) async

Execute all pre-shutdown hooks in registration order.

Continues with other hooks even if one fails to ensure maximum cleanup.

Parameters:

Name Type Description Default
app_scope AppScopeProtocol | None

Optional application scope for dependency resolution.

None
Source code in servicewright/core/lifecycle/manager.py
async def run_pre_shutdown_hooks(self, app_scope: AppScopeProtocol | None = None) -> None:
    """Execute all pre-shutdown hooks in registration order.

    Continues with other hooks even if one fails to ensure maximum cleanup.

    Args:
        app_scope: Optional application scope for dependency resolution.
    """
    await self._run_hooks(self._pre_shutdown_hooks, "Pre-shutdown", app_scope, raise_on_failure=False)

run_pre_start_hooks(app_scope=None) async

Execute all pre-start hooks in registration order.

Aborts startup if any hook fails.

Parameters:

Name Type Description Default
app_scope AppScopeProtocol | None

Optional application scope for dependency resolution.

None
Source code in servicewright/core/lifecycle/manager.py
async def run_pre_start_hooks(self, app_scope: AppScopeProtocol | None = None) -> None:
    """Execute all pre-start hooks in registration order.

    Aborts startup if any hook fails.

    Args:
        app_scope: Optional application scope for dependency resolution.
    """
    await self._run_hooks(self._pre_start_hooks, "Pre-start", app_scope, raise_on_failure=True)

LifecycleHookProtocol

Bases: Protocol

Protocol for lifecycle hook callbacks.

Source code in servicewright/core/contracts/lifecycle.py
class LifecycleHookProtocol(Protocol):
    """Protocol for lifecycle hook callbacks."""

    async def __call__(self, app_scope: AppScopeProtocol | None = None) -> None:
        """Execute lifecycle hook."""
        ...

__call__(app_scope=None) async

Execute lifecycle hook.

Source code in servicewright/core/contracts/lifecycle.py
async def __call__(self, app_scope: AppScopeProtocol | None = None) -> None:
    """Execute lifecycle hook."""
    ...

LoggingSettingsProtocol

Bases: Protocol

Logging concern: root level and rendering format.

Source code in servicewright/core/observability/protocols.py
class LoggingSettingsProtocol(Protocol):
    """Logging concern: root level and rendering format."""

    @property
    def level(self) -> LogLevelStr | str: ...

    @property
    def use_json(self) -> bool: ...

MetricsSettingsProtocol

Bases: Protocol

Metrics concern: standalone exposition endpoint.

Source code in servicewright/core/observability/protocols.py
class MetricsSettingsProtocol(Protocol):
    """Metrics concern: standalone exposition endpoint."""

    @property
    def enabled(self) -> bool: ...

    @property
    def port(self) -> int: ...

    @property
    def host(self) -> str: ...

    @property
    def prefix(self) -> str | None: ...

ObsConfig dataclass

App-wide add-on backend defaults (the process-global selection).

Selecting a backend here says which implementation to use when the concern is configured; whether the concern is active is decided by the settings (settings.error_tracking.dsn present, settings.tracing present, ...). Missing extra for a selected+configured backend hard-raises at Bootstrap.

Source code in servicewright/core/observability/config.py
@dataclass(frozen=True, slots=True)
class ObsConfig:
    """App-wide add-on backend defaults (the process-global selection).

    Selecting a backend here says *which implementation to use when the concern
    is configured*; whether the concern is active is decided by the settings
    (``settings.error_tracking.dsn`` present, ``settings.tracing`` present, ...).
    Missing extra for a selected+configured backend hard-raises at Bootstrap.
    """

    metrics: str | None = "prometheus"
    tracing: str | None = "otel"
    error_tracking: str | None = "sentry"
    logging: str | None = "structlog"

ObsSetupContext dataclass

Built by the manager from settings + spec and handed to each sink's setup().

Observability config must be reachable from settings (DSN, collector URL, tokens): setup() runs in Bootstrap, before the DI container exists, so container-resolved secrets are unavailable at sink setup time.

redactor arrives already resolved for the receiving sink's surface: the manager applies its per-surface overrides (log_redactor / error_redactor / trace_redactor) before handing the context over, so a sink never has to know which override chain produced it.

Source code in servicewright/core/observability/config.py
@dataclass(frozen=True, slots=True)
class ObsSetupContext:
    """Built by the manager from settings + spec and handed to each sink's ``setup()``.

    Observability config must be reachable from ``settings`` (DSN, collector URL,
    tokens): ``setup()`` runs in Bootstrap, *before* the DI container exists, so
    container-resolved secrets are unavailable at sink setup time.

    ``redactor`` arrives already resolved for the receiving sink's surface: the
    manager applies its per-surface overrides (``log_redactor`` /
    ``error_redactor`` / ``trace_redactor``) before handing the context over,
    so a sink never has to know which override chain produced it.
    """

    service_name: str
    app_version: str
    environment: str
    settings: BaseServiceSettingsProtocol
    redactor: Redactor | None = None

ObservabilityManager

Resolves, sets up and tears down the four add-on sinks.

Parameters:

Name Type Description Default
config ObsConfig | None

App-wide backend selection by name. None selects nothing (concerns without an instance stay NullObject) — the default for a bare AppSpec.

None
redactor Redactor | None

Cross-cutting sensitive-data filter threaded into the logging, error-tracking and tracing sinks (every payload surface; metrics carry no payloads and are exempt).

None
log_redactor Redactor | None

Per-surface override for the logging sink; wins over redactor there. Surfaces differ in volume by orders of magnitude — this is how a cheap masker goes on every log line while an expensive one guards only the error path.

None
error_redactor Redactor | None

Per-surface override for the error-tracking sink. The natural home for ML-grade maskers: events are rare, shipped off the request path, and leak the most (stack-frame locals, request bodies).

None
trace_redactor Redactor | None

Per-surface override for span attributes.

None
metrics MetricsSinkProtocol | None

Ready metrics sink instance (wins over config.metrics).

None
tracing TracingSinkProtocol | None

Ready tracing sink instance (wins over config.tracing).

None
error_tracking ErrorTrackingSinkProtocol | None

Ready error-tracking sink instance (wins over config.error_tracking).

None
logging LoggingSinkProtocol | None

Ready logging sink instance (wins over config.logging).

None
Source code in servicewright/core/observability/manager.py
class ObservabilityManager:
    """Resolves, sets up and tears down the four add-on sinks.

    Args:
        config: App-wide backend selection by name. ``None`` selects nothing
            (concerns without an instance stay NullObject) — the default for a
            bare ``AppSpec``.
        redactor: Cross-cutting sensitive-data filter threaded into the
            logging, error-tracking and tracing sinks (every payload surface;
            metrics carry no payloads and are exempt).
        log_redactor: Per-surface override for the logging sink; wins over
            ``redactor`` there. Surfaces differ in volume by orders of
            magnitude — this is how a cheap masker goes on every log line
            while an expensive one guards only the error path.
        error_redactor: Per-surface override for the error-tracking sink.
            The natural home for ML-grade maskers: events are rare, shipped
            off the request path, and leak the most (stack-frame locals,
            request bodies).
        trace_redactor: Per-surface override for span attributes.
        metrics: Ready metrics sink instance (wins over ``config.metrics``).
        tracing: Ready tracing sink instance (wins over ``config.tracing``).
        error_tracking: Ready error-tracking sink instance (wins over
            ``config.error_tracking``).
        logging: Ready logging sink instance (wins over ``config.logging``).
    """

    def __init__(
        self,
        config: ObsConfig | None = None,
        *,
        redactor: Redactor | None = None,
        log_redactor: Redactor | None = None,
        error_redactor: Redactor | None = None,
        trace_redactor: Redactor | None = None,
        metrics: MetricsSinkProtocol | None = None,
        tracing: TracingSinkProtocol | None = None,
        error_tracking: ErrorTrackingSinkProtocol | None = None,
        logging: LoggingSinkProtocol | None = None,
    ) -> None:
        self._config = config
        self._redactor = redactor
        self._surface_redactors: dict[str, Redactor | None] = {
            "logging": log_redactor,
            "error_tracking": error_redactor,
            "tracing": trace_redactor,
        }
        self._configured = False
        # (concern, sink) pairs in setup order; sinks are registry-resolved and
        # therefore dynamically typed at this boundary.
        self._active: list[tuple[str, Any]] = []

        self._provided: dict[str, Any] = {
            "logging": logging,
            "error_tracking": error_tracking,
            "tracing": tracing,
            "metrics": metrics,
        }

        self._metrics: MetricsSinkProtocol = NullMetricsSink()
        self._tracing: TracingSinkProtocol = NullTracingSink()
        self._error_tracking: ErrorTrackingSinkProtocol = NullErrorTrackingSink()
        self._logging: LoggingSinkProtocol = NullLoggingSink()

    @property
    def config(self) -> ObsConfig | None:
        """The app-wide backend selection."""
        return self._config

    @property
    def metrics(self) -> MetricsSinkProtocol:
        """The metrics sink (NullObject until configured)."""
        return self._metrics

    @property
    def tracing(self) -> TracingSinkProtocol:
        """The tracing sink (NullObject until configured)."""
        return self._tracing

    @property
    def error_tracking(self) -> ErrorTrackingSinkProtocol:
        """The error-tracking sink (NullObject until configured)."""
        return self._error_tracking

    @property
    def logging(self) -> LoggingSinkProtocol:
        """The logging sink (NullObject until configured)."""
        return self._logging

    def configure(self, settings: BaseServiceSettingsProtocol, *, service_name: str = "") -> None:
        """Resolve and set up every selected+configured sink (fail-fast).

        Setup order is ``logging -> error-tracking -> tracing -> metrics`` so the
        earliest failures are already logged and reported.
        """
        if self._configured:
            logger.warning("ObservabilityManager already configured, ignoring duplicate call")
            return

        ctx = self._build_setup_context(settings, service_name)

        gates = {
            "logging": self._is_logging_configured,
            "error_tracking": self._is_error_tracking_configured,
            "tracing": self._is_tracing_configured,
            "metrics": self._is_metrics_configured,
        }
        for concern in ("logging", "error_tracking", "tracing", "metrics"):
            # An explicit instance is unconditional (its setup() decides what to
            # do with settings); a by-name selection additionally requires the
            # concern to be configured in settings.
            sink = self._provided.get(concern)
            if sink is None:
                backend = getattr(self._config, concern, None) if self._config is not None else None
                if backend is None or not gates[concern](settings):
                    continue
                sink = resolve_sink(concern, backend)()
            # Each sink sees the redactor resolved for its own surface: the
            # per-surface override, else the cross-cutting one. Metrics carry
            # no payloads, so their ctx never advertises a redactor.
            sink.setup(replace(ctx, redactor=self._redactor_for(concern)))
            self._active.append((concern, sink))
            setattr(self, f"_{concern}", sink)
            logger.info(
                "Observability sink configured",
                extra={"concern": concern, "backend": getattr(sink, "backend", "?")},
            )

        self._configured = True

    def shutdown(self) -> None:
        """Tear down active sinks in reverse setup order (best-effort, never raises).

        Returns the manager to its pre-``configure`` state: the concerns fall
        back to their NullObject sinks and the manager can be configured again.
        A ``Service`` reuses one long-lived ``AppSpec`` across runs, so leaving
        the torn-down sinks in place would give run 2 a stack that reports the
        real sink type while logging, metrics and tracing all silently go
        nowhere. Per-run idempotency stays where it belongs — inside each sink's
        own ``setup``.
        """
        for concern, sink in reversed(self._active):
            try:
                sink.shutdown()
            except Exception:
                logger.exception("Observability sink shutdown failed", extra={"concern": concern})
        self._active.clear()

        self._metrics = NullMetricsSink()
        self._tracing = NullTracingSink()
        self._error_tracking = NullErrorTrackingSink()
        self._logging = NullLoggingSink()
        self._configured = False

    def _redactor_for(self, concern: str) -> Redactor | None:
        if concern == "metrics":
            return None
        override = self._surface_redactors.get(concern)
        return override if override is not None else self._redactor

    def _build_setup_context(self, settings: BaseServiceSettingsProtocol, service_name: str) -> ObsSetupContext:
        environment = getattr(settings, "environment", None)
        if not environment:
            error_settings = getattr(settings, "error_tracking", None)
            environment = getattr(error_settings, "environment", "") if error_settings else ""
        return ObsSetupContext(
            service_name=service_name,
            app_version=settings.get_app_version(),
            environment=environment or "",
            settings=settings,
            redactor=self._redactor,
        )

    @staticmethod
    def _is_logging_configured(settings: BaseServiceSettingsProtocol) -> bool:
        return getattr(settings, "logging", None) is not None

    @staticmethod
    def _is_error_tracking_configured(settings: BaseServiceSettingsProtocol) -> bool:
        error_settings = getattr(settings, "error_tracking", None)
        return error_settings is not None and bool(getattr(error_settings, "dsn", None))

    @staticmethod
    def _is_tracing_configured(settings: BaseServiceSettingsProtocol) -> bool:
        return getattr(settings, "tracing", None) is not None

    @staticmethod
    def _is_metrics_configured(settings: BaseServiceSettingsProtocol) -> bool:
        return getattr(settings, "metrics", None) is not None

config property

The app-wide backend selection.

error_tracking property

The error-tracking sink (NullObject until configured).

logging property

The logging sink (NullObject until configured).

metrics property

The metrics sink (NullObject until configured).

tracing property

The tracing sink (NullObject until configured).

configure(settings, *, service_name='')

Resolve and set up every selected+configured sink (fail-fast).

Setup order is logging -> error-tracking -> tracing -> metrics so the earliest failures are already logged and reported.

Source code in servicewright/core/observability/manager.py
def configure(self, settings: BaseServiceSettingsProtocol, *, service_name: str = "") -> None:
    """Resolve and set up every selected+configured sink (fail-fast).

    Setup order is ``logging -> error-tracking -> tracing -> metrics`` so the
    earliest failures are already logged and reported.
    """
    if self._configured:
        logger.warning("ObservabilityManager already configured, ignoring duplicate call")
        return

    ctx = self._build_setup_context(settings, service_name)

    gates = {
        "logging": self._is_logging_configured,
        "error_tracking": self._is_error_tracking_configured,
        "tracing": self._is_tracing_configured,
        "metrics": self._is_metrics_configured,
    }
    for concern in ("logging", "error_tracking", "tracing", "metrics"):
        # An explicit instance is unconditional (its setup() decides what to
        # do with settings); a by-name selection additionally requires the
        # concern to be configured in settings.
        sink = self._provided.get(concern)
        if sink is None:
            backend = getattr(self._config, concern, None) if self._config is not None else None
            if backend is None or not gates[concern](settings):
                continue
            sink = resolve_sink(concern, backend)()
        # Each sink sees the redactor resolved for its own surface: the
        # per-surface override, else the cross-cutting one. Metrics carry
        # no payloads, so their ctx never advertises a redactor.
        sink.setup(replace(ctx, redactor=self._redactor_for(concern)))
        self._active.append((concern, sink))
        setattr(self, f"_{concern}", sink)
        logger.info(
            "Observability sink configured",
            extra={"concern": concern, "backend": getattr(sink, "backend", "?")},
        )

    self._configured = True

shutdown()

Tear down active sinks in reverse setup order (best-effort, never raises).

Returns the manager to its pre-configure state: the concerns fall back to their NullObject sinks and the manager can be configured again. A Service reuses one long-lived AppSpec across runs, so leaving the torn-down sinks in place would give run 2 a stack that reports the real sink type while logging, metrics and tracing all silently go nowhere. Per-run idempotency stays where it belongs — inside each sink's own setup.

Source code in servicewright/core/observability/manager.py
def shutdown(self) -> None:
    """Tear down active sinks in reverse setup order (best-effort, never raises).

    Returns the manager to its pre-``configure`` state: the concerns fall
    back to their NullObject sinks and the manager can be configured again.
    A ``Service`` reuses one long-lived ``AppSpec`` across runs, so leaving
    the torn-down sinks in place would give run 2 a stack that reports the
    real sink type while logging, metrics and tracing all silently go
    nowhere. Per-run idempotency stays where it belongs — inside each sink's
    own ``setup``.
    """
    for concern, sink in reversed(self._active):
        try:
            sink.shutdown()
        except Exception:
            logger.exception("Observability sink shutdown failed", extra={"concern": concern})
    self._active.clear()

    self._metrics = NullMetricsSink()
    self._tracing = NullTracingSink()
    self._error_tracking = NullErrorTrackingSink()
    self._logging = NullLoggingSink()
    self._configured = False

OneShotEntrypoint

Bases: ScopedEntrypoint

Runs func exactly once inside a fresh unit scope, then returns.

Being essential by default, its return stops the whole service.

Source code in servicewright/adapters/builtin/oneshot.py
class OneShotEntrypoint(ScopedEntrypoint):
    """Runs ``func`` exactly once inside a fresh unit scope, then returns.

    Being ``essential`` by default, its return stops the whole service.
    """

    def __init__(
        self,
        func: Callable[[UnitScopeProtocol], Awaitable[None]],
        *,
        kind: str = "oneshot",
        essential: bool = True,
    ) -> None:
        super().__init__()
        self._func = func
        self.kind = kind
        self.essential = essential

    async def serve(self, *, stop: asyncio.Event) -> None:
        """Open a unit scope, run the function once, then return."""
        async with self.unit_scope() as scope:
            await self._func(scope)

bind(ctx) async

Capture the container so per-unit scopes can be opened.

Source code in servicewright/core/contracts/bases.py
async def bind(self, ctx: ServiceContext) -> None:
    """Capture the container so per-unit scopes can be opened."""
    self._container = ctx.container

drain(grace) async

Stop intake; let in-flight units finish.

Source code in servicewright/core/contracts/bases.py
async def drain(self, grace: float) -> None:
    """Stop intake; let in-flight units finish."""
    return None

serve(*, stop) async

Open a unit scope, run the function once, then return.

Source code in servicewright/adapters/builtin/oneshot.py
async def serve(self, *, stop: asyncio.Event) -> None:
    """Open a unit scope, run the function once, then return."""
    async with self.unit_scope() as scope:
        await self._func(scope)

stop() async

Hard stop / release resources.

Source code in servicewright/core/contracts/bases.py
async def stop(self) -> None:
    """Hard stop / release resources."""
    return None

unit_scope(context=None)

Open a per-unit-of-work DI scope.

Raises:

Type Description
RuntimeError

If called before :meth:bind.

Source code in servicewright/core/contracts/bases.py
def unit_scope(
    self, context: Mapping[Any, Any] | None = None
) -> contextlib.AbstractAsyncContextManager[UnitScopeProtocol]:
    """Open a per-unit-of-work DI scope.

    Raises:
        RuntimeError: If called before :meth:`bind`.
    """
    if self._container is None:
        raise RuntimeError("unit_scope() called before bind(); entrypoint is not bound to a container")
    return self._container.unit_scope(context)

Plugin

Bases: Protocol

Litestar on_app_init analogue.

A plugin mutates a neutral spec/host: append entrypoints, warmers, health checks, lifecycle hooks or DI providers. This is the only batteries-optional extension surface — never an entrypoint subclass.

Source code in servicewright/core/contracts/plugin.py
@runtime_checkable
class Plugin(Protocol):
    """Litestar ``on_app_init`` analogue.

    A plugin mutates a neutral spec/host: append entrypoints, warmers, health
    checks, lifecycle hooks or DI providers. This is the only batteries-optional
    extension surface — never an entrypoint subclass.
    """

    def on_register(self, spec: AppSpec, host: Host) -> None:
        """Mutate the spec/host before the run-loop starts."""
        ...

on_register(spec, host)

Mutate the spec/host before the run-loop starts.

Source code in servicewright/core/contracts/plugin.py
def on_register(self, spec: AppSpec, host: Host) -> None:
    """Mutate the spec/host before the run-loop starts."""
    ...

PostgresWarmupError

Bases: WarmupError

Raised when Postgres warmup fails.

Source code in servicewright/core/exceptions.py
class PostgresWarmupError(WarmupError):
    """Raised when Postgres warmup fails."""

ProbeStatus

Bases: StrEnum

Coarse status of a health probe.

Source code in servicewright/core/health/report.py
class ProbeStatus(StrEnum):
    """Coarse status of a health probe."""

    HEALTHY = "healthy"
    UNHEALTHY = "unhealthy"

    @classmethod
    def from_bool(cls, healthy: bool) -> ProbeStatus:
        """Map a boolean health flag to a probe status."""
        return cls.HEALTHY if healthy else cls.UNHEALTHY

from_bool(healthy) classmethod

Map a boolean health flag to a probe status.

Source code in servicewright/core/health/report.py
@classmethod
def from_bool(cls, healthy: bool) -> ProbeStatus:
    """Map a boolean health flag to a probe status."""
    return cls.HEALTHY if healthy else cls.UNHEALTHY

ProblemDetailsRenderer

The default renderer: RFC 9457 Problem Details (application/problem+json).

Body: type (a URI built from type_base and the code, or about:blank), title (the HTTP reason phrase), status, detail (when present) plus the extension members code and params (when non-empty).

Rendering is total: params are coerced with :func:to_json_safe (a UUID or datetime renders as its string form rather than turning the intended 404 into a masked 500) and the title tolerates non-IANA statuses.

Parameters:

Name Type Description Default
type_base str | None

Base URI for the type member; None renders about:blank per the RFC default.

None
Source code in servicewright/core/errors.py
class ProblemDetailsRenderer:
    """The default renderer: RFC 9457 Problem Details (``application/problem+json``).

    Body: ``type`` (a URI built from ``type_base`` and the code, or
    ``about:blank``), ``title`` (the HTTP reason phrase), ``status``,
    ``detail`` (when present) plus the extension members ``code`` and
    ``params`` (when non-empty).

    Rendering is total: ``params`` are coerced with :func:`to_json_safe` (a
    ``UUID`` or ``datetime`` renders as its string form rather than turning the
    intended 404 into a masked 500) and the title tolerates non-IANA statuses.

    Args:
        type_base: Base URI for the ``type`` member; ``None`` renders
            ``about:blank`` per the RFC default.
    """

    def __init__(self, *, type_base: str | None = None) -> None:
        self._type_base = type_base.rstrip("/") if type_base else None

    def render(self, info: ErrorInfo) -> RenderedError:
        """Render the failure as an RFC 9457 problem document."""
        status = info.http_status
        body: dict[str, Any] = {
            "type": f"{self._type_base}/{info.code}" if self._type_base else "about:blank",
            "title": status_title(status),
            "status": status,
            "code": info.code,
        }
        if info.detail:
            body["detail"] = info.detail
        if info.params:
            body["params"] = to_json_safe(dict(info.params))
        return RenderedError(status_code=status, body=body, headers=info.headers)

render(info)

Render the failure as an RFC 9457 problem document.

Source code in servicewright/core/errors.py
def render(self, info: ErrorInfo) -> RenderedError:
    """Render the failure as an RFC 9457 problem document."""
    status = info.http_status
    body: dict[str, Any] = {
        "type": f"{self._type_base}/{info.code}" if self._type_base else "about:blank",
        "title": status_title(status),
        "status": status,
        "code": info.code,
    }
    if info.detail:
        body["detail"] = info.detail
    if info.params:
        body["params"] = to_json_safe(dict(info.params))
    return RenderedError(status_code=status, body=body, headers=info.headers)

RedisWarmupError

Bases: WarmupError

Raised when Redis warmup fails.

Source code in servicewright/core/exceptions.py
class RedisWarmupError(WarmupError):
    """Raised when Redis warmup fails."""

RenderedError dataclass

A rendered wire response for one failure.

Source code in servicewright/core/errors.py
@dataclass(frozen=True, slots=True)
class RenderedError:
    """A rendered wire response for one failure."""

    status_code: int
    body: dict[str, Any]
    media_type: str = "application/problem+json"
    headers: Mapping[str, str] | None = None

ScopedEntrypoint

Bases: ABC

Base for loop/poll-driven entrypoints (scheduler, consumer, daemon, one-shot).

Provides the only sanctioned per-unit DI API: async with self.unit_scope(context) as scope: which delegates to the container.

Source code in servicewright/core/contracts/bases.py
class ScopedEntrypoint(abc.ABC):
    """Base for loop/poll-driven entrypoints (scheduler, consumer, daemon, one-shot).

    Provides the *only* sanctioned per-unit DI API: ``async with
    self.unit_scope(context) as scope:`` which delegates to the container.
    """

    kind: str = "scoped"
    essential: bool = True

    def __init__(self) -> None:
        self._container: DependencyContainerProtocol | None = None

    async def bind(self, ctx: ServiceContext) -> None:
        """Capture the container so per-unit scopes can be opened."""
        self._container = ctx.container

    def unit_scope(
        self, context: Mapping[Any, Any] | None = None
    ) -> contextlib.AbstractAsyncContextManager[UnitScopeProtocol]:
        """Open a per-unit-of-work DI scope.

        Raises:
            RuntimeError: If called before :meth:`bind`.
        """
        if self._container is None:
            raise RuntimeError("unit_scope() called before bind(); entrypoint is not bound to a container")
        return self._container.unit_scope(context)

    @abc.abstractmethod
    async def serve(self, *, stop: asyncio.Event) -> None:
        """Run the loop until ``stop`` is set."""
        raise NotImplementedError

    async def drain(self, grace: float) -> None:
        """Stop intake; let in-flight units finish."""
        return None

    async def stop(self) -> None:
        """Hard stop / release resources."""
        return None

bind(ctx) async

Capture the container so per-unit scopes can be opened.

Source code in servicewright/core/contracts/bases.py
async def bind(self, ctx: ServiceContext) -> None:
    """Capture the container so per-unit scopes can be opened."""
    self._container = ctx.container

drain(grace) async

Stop intake; let in-flight units finish.

Source code in servicewright/core/contracts/bases.py
async def drain(self, grace: float) -> None:
    """Stop intake; let in-flight units finish."""
    return None

serve(*, stop) abstractmethod async

Run the loop until stop is set.

Source code in servicewright/core/contracts/bases.py
@abc.abstractmethod
async def serve(self, *, stop: asyncio.Event) -> None:
    """Run the loop until ``stop`` is set."""
    raise NotImplementedError

stop() async

Hard stop / release resources.

Source code in servicewright/core/contracts/bases.py
async def stop(self) -> None:
    """Hard stop / release resources."""
    return None

unit_scope(context=None)

Open a per-unit-of-work DI scope.

Raises:

Type Description
RuntimeError

If called before :meth:bind.

Source code in servicewright/core/contracts/bases.py
def unit_scope(
    self, context: Mapping[Any, Any] | None = None
) -> contextlib.AbstractAsyncContextManager[UnitScopeProtocol]:
    """Open a per-unit-of-work DI scope.

    Raises:
        RuntimeError: If called before :meth:`bind`.
    """
    if self._container is None:
        raise RuntimeError("unit_scope() called before bind(); entrypoint is not bound to a container")
    return self._container.unit_scope(context)

ServerEntrypoint

Bases: ABC

Base for socket-serving entrypoints (FastAPI, gRPC, Litestar, Flask).

The framework's DI integration owns the per-request scope, so this base deliberately exposes no unit_scope and cannot double-open one.

Source code in servicewright/core/contracts/bases.py
class ServerEntrypoint(abc.ABC):
    """Base for socket-serving entrypoints (FastAPI, gRPC, Litestar, Flask).

    The framework's DI integration owns the per-request scope, so this base
    deliberately exposes no ``unit_scope`` and cannot double-open one.
    """

    kind: str = "server"
    essential: bool = True

    async def bind(self, ctx: ServiceContext) -> None:
        """Bind to the service context. Override to allocate the server."""
        return None

    @abc.abstractmethod
    async def serve(self, *, stop: asyncio.Event) -> None:
        """Run the server until ``stop`` is set."""
        raise NotImplementedError

    async def drain(self, grace: float) -> None:
        """Stop accepting new connections; let in-flight requests finish."""
        return None

    async def stop(self) -> None:
        """Hard stop the server."""
        return None

bind(ctx) async

Bind to the service context. Override to allocate the server.

Source code in servicewright/core/contracts/bases.py
async def bind(self, ctx: ServiceContext) -> None:
    """Bind to the service context. Override to allocate the server."""
    return None

drain(grace) async

Stop accepting new connections; let in-flight requests finish.

Source code in servicewright/core/contracts/bases.py
async def drain(self, grace: float) -> None:
    """Stop accepting new connections; let in-flight requests finish."""
    return None

serve(*, stop) abstractmethod async

Run the server until stop is set.

Source code in servicewright/core/contracts/bases.py
@abc.abstractmethod
async def serve(self, *, stop: asyncio.Event) -> None:
    """Run the server until ``stop`` is set."""
    raise NotImplementedError

stop() async

Hard stop the server.

Source code in servicewright/core/contracts/bases.py
async def stop(self) -> None:
    """Hard stop the server."""
    return None

Service

Declarative facade: an :class:AppSpec plus entrypoints and plugins.

service.run(settings) builds a :class:Host and blocks until a stop signal is received.

Source code in servicewright/core/service.py
class Service[TSettings: "BaseServiceSettingsProtocol", TContainer: "DependencyContainerProtocol"]:
    """Declarative facade: an :class:`AppSpec` plus entrypoints and plugins.

    ``service.run(settings)`` builds a :class:`Host` and blocks until a stop
    signal is received.
    """

    def __init__(
        self,
        spec: AppSpec[TSettings, TContainer],
        *,
        entrypoints: Iterable[Entrypoint] = (),
        plugins: Iterable[Plugin] = (),
    ) -> None:
        self.spec = spec
        self._entrypoints: list[Entrypoint] = list(entrypoints)
        self._plugins: list[Plugin] = list(plugins)

    @property
    def entrypoints(self) -> list[Entrypoint]:
        """Configured entrypoints."""
        return self._entrypoints

    @property
    def plugins(self) -> list[Plugin]:
        """Configured plugins."""
        return self._plugins

    async def run(self, settings: TSettings, *, stop: asyncio.Event | None = None) -> None:
        """Run the service, blocking until ``stop`` is set or a signal arrives."""
        host: Host[TSettings, TContainer] = Host(self.spec)
        await host.run(settings, self._entrypoints, plugins=self._plugins, stop=stop)

entrypoints property

Configured entrypoints.

plugins property

Configured plugins.

run(settings, *, stop=None) async

Run the service, blocking until stop is set or a signal arrives.

Source code in servicewright/core/service.py
async def run(self, settings: TSettings, *, stop: asyncio.Event | None = None) -> None:
    """Run the service, blocking until ``stop`` is set or a signal arrives."""
    host: Host[TSettings, TContainer] = Host(self.spec)
    await host.run(settings, self._entrypoints, plugins=self._plugins, stop=stop)

ServiceContext dataclass

Context available once the application scope is opened.

Source code in servicewright/core/spec.py
@dataclass(slots=True)
class ServiceContext[TSettings: "BaseServiceSettingsProtocol", TContainer: "DependencyContainerProtocol"]:
    """Context available once the application scope is opened."""

    bootstrap: BootstrapContext[TSettings, TContainer]
    app_scope: AppScopeProtocol
    health: HealthRegistry
    # The spec's manager; entrypoints mint their emit handles (recorders,
    # tracers, instrumentation) from here at bind time. Defaults to an
    # all-NullObject manager so hand-built contexts in tests stay cheap.
    observability: ObservabilityManager = field(default_factory=ObservabilityManager)

    @property
    def settings(self) -> TSettings:
        return self.bootstrap.settings

    @property
    def service_name(self) -> str:
        return self.bootstrap.service_name

    @property
    def container(self) -> TContainer:
        return self.bootstrap.container

    @property
    def lifecycle(self) -> Lifecycle:
        return self.bootstrap.lifecycle

ServiceError

Bases: Exception

Base class for typed business errors raised by application code.

Subclasses set kind (and optionally code) as class attributes; the code defaults to the snake_cased class name without the Error suffix. Every attribute can also be overridden per-instance via keyword arguments, and the resolved value is what the instance reports — exc.code, exc.kind and exc.public are always the values the transports act on, so except ServiceError as exc: if exc.code == "user_missing" works.

Parameters:

Name Type Description Default
detail str | None

Human-readable message shown to the client when the error is public (falls back to a generic phrase for the kind).

None
code str | None

Machine-readable error code (stable API for clients).

None
kind ErrorKind | None

The failure category driving the transport status.

None
params Mapping[str, Any] | None

Structured details for the client. Values that are not JSON primitives are coerced by the renderer, never dropped.

None
public bool | None

When False the transports mask everything: the client sees a generic internal error and the real code is only logged.

None
Source code in servicewright/core/errors.py
class ServiceError(Exception):
    """Base class for typed business errors raised by application code.

    Subclasses set ``kind`` (and optionally ``code``) as class attributes; the
    code defaults to the snake_cased class name without the ``Error`` suffix.
    Every attribute can also be overridden per-instance via keyword arguments,
    and the resolved value is what the instance reports — ``exc.code``,
    ``exc.kind`` and ``exc.public`` are always the values the transports act on,
    so ``except ServiceError as exc: if exc.code == "user_missing"`` works.

    Args:
        detail: Human-readable message shown to the client when the error is
            public (falls back to a generic phrase for the kind).
        code: Machine-readable error code (stable API for clients).
        kind: The failure category driving the transport status.
        params: Structured details for the client. Values that are not JSON
            primitives are coerced by the renderer, never dropped.
        public: When ``False`` the transports mask everything: the client sees
            a generic internal error and the real code is only logged.
    """

    # Declared as plain class attributes (not ClassVar): __init__ shadows them
    # with the resolved per-instance values, so both views always agree.
    kind: ErrorKind = ErrorKind.INTERNAL
    code: str | None = None
    public: bool = True
    detail: str | None = None
    params: Mapping[str, Any] = MappingProxyType({})

    def __init__(
        self,
        detail: str | None = None,
        *,
        code: str | None = None,
        kind: ErrorKind | None = None,
        params: Mapping[str, Any] | None = None,
        public: bool | None = None,
    ) -> None:
        cls = type(self)
        resolved_code = code if code is not None else (cls.code or _derive_code(cls.__name__))
        super().__init__(detail if detail is not None else resolved_code)
        self.detail = detail
        self.code = resolved_code
        self.kind = kind if kind is not None else cls.kind
        self.params = dict(params) if params else {}
        self.public = public if public is not None else cls.public

ServiceWrightError

Bases: Exception

Base class for all servicewright runtime errors.

Source code in servicewright/core/exceptions.py
class ServiceWrightError(Exception):
    """Base class for all servicewright runtime errors."""

TracingSettingsProtocol

Bases: Protocol

Tracing concern: exporter endpoint and sampling.

Source code in servicewright/core/observability/protocols.py
class TracingSettingsProtocol(Protocol):
    """Tracing concern: exporter endpoint and sampling."""

    @property
    def service_name(self) -> str: ...

    @property
    def collector_url(self) -> str | None: ...

    @property
    def sample_ratio(self) -> float: ...

    @property
    def insecure(self) -> bool: ...

    @property
    def enable_console_exporter(self) -> bool: ...

    @property
    def excluded_urls(self) -> str | None: ...

UnitScopeProtocol

Bases: Protocol

Protocol for unit-of-work-level dependency scope.

A unit scope is minted per unit of work (one request, message, job, task or activity) and carries that unit's payload as its context.

Source code in servicewright/core/contracts/container.py
class UnitScopeProtocol(Protocol):
    """Protocol for unit-of-work-level dependency scope.

    A unit scope is minted per unit of work (one request, message, job, task
    or activity) and carries that unit's payload as its ``context``.
    """

    @overload
    async def get(self, dependency_key: type[T]) -> T: ...

    @overload
    async def get(self, dependency_key: str) -> Any: ...

    async def get(self, dependency_key: type[T] | str) -> T | Any:
        """Retrieve dependency instance by key or type."""
        ...

get(dependency_key) async

get(dependency_key: type[T]) -> T
get(dependency_key: str) -> Any

Retrieve dependency instance by key or type.

Source code in servicewright/core/contracts/container.py
async def get(self, dependency_key: type[T] | str) -> T | Any:
    """Retrieve dependency instance by key or type."""
    ...

ValueRedactor

Lifts a value-level :class:Masker over every string value in a payload.

Same traversal as :class:KeyRedactor - nested dicts, lists and tuples, cycle-safe - but the decision is made by the masker looking at each string value, not by the field name. Keys are never masked.

Fail closed: if the masker raises on a value, that value becomes the mask (never the raw string), and one warning is logged per redactor instance - a broken masker is visible without a log storm and without dropping a single log line or event.

Source code in servicewright/core/observability/redaction.py
class ValueRedactor:
    """Lifts a value-level :class:`Masker` over every string value in a payload.

    Same traversal as :class:`KeyRedactor` - nested dicts, lists and tuples,
    cycle-safe - but the decision is made by the masker looking at each string
    *value*, not by the field name. Keys are never masked.

    Fail closed: if the masker raises on a value, that value becomes the mask
    (never the raw string), and one warning is logged per redactor instance -
    a broken masker is visible without a log storm and without dropping a
    single log line or event.
    """

    def __init__(self, masker: Masker, mask: str = MASK) -> None:
        self._masker = masker
        self._mask = mask
        self._warned = False

    def __call__(self, data: dict[str, Any]) -> dict[str, Any]:
        """Return a copy of ``data`` with every string value passed through the masker."""
        return self._redact_mapping(data, frozenset())

    def _redact_mapping(self, data: dict[str, Any], seen: frozenset[int]) -> dict[str, Any]:
        seen = seen | {id(data)}
        return {key: self._redact_value(value, seen) for key, value in data.items()}

    def _redact_value(self, value: Any, seen: frozenset[int]) -> Any:
        if isinstance(value, str):
            return self._mask_one(value)
        if id(value) in seen:
            return value
        if isinstance(value, dict):
            return self._redact_mapping(value, seen)
        if isinstance(value, list):
            return [self._redact_value(item, seen | {id(value)}) for item in value]
        if isinstance(value, tuple):
            return tuple(self._redact_value(item, seen | {id(value)}) for item in value)
        return value

    def _mask_one(self, value: str) -> str:
        try:
            return self._masker(value)
        except Exception:
            if not self._warned:
                self._warned = True
                logger.warning("value masker raised; emitting the mask instead of the value", exc_info=True)
            return self._mask

__call__(data)

Return a copy of data with every string value passed through the masker.

Source code in servicewright/core/observability/redaction.py
def __call__(self, data: dict[str, Any]) -> dict[str, Any]:
    """Return a copy of ``data`` with every string value passed through the masker."""
    return self._redact_mapping(data, frozenset())

WarmupError

Bases: ServiceWrightError

Base exception for infrastructure warmup errors.

Source code in servicewright/core/exceptions.py
class WarmupError(ServiceWrightError):
    """Base exception for infrastructure warmup errors."""

WarmupTimeoutError

Bases: ServiceWrightError, TimeoutError

Raised when infrastructure warmup does not finish within the allotted timeout.

Source code in servicewright/core/exceptions.py
class WarmupTimeoutError(ServiceWrightError, TimeoutError):
    """Raised when infrastructure warmup does not finish within the allotted timeout."""

bind_context(**values)

Context manager binding values for the duration of the block.

Source code in servicewright/core/context.py
@contextlib.contextmanager
def bind_context(**values: Any) -> Generator[None, None, None]:
    """Context manager binding ``values`` for the duration of the block."""
    remove = bind_context_values(values)
    try:
        yield
    finally:
        remove()

bind_context_values(values)

Bind values into the store; returns a remover that resets them all.

None values are skipped (absent, not bound). The remover resets in reverse binding order and is idempotent.

Source code in servicewright/core/context.py
def bind_context_values(values: Mapping[str, Any]) -> Callable[[], None]:
    """Bind ``values`` into the store; returns a remover that resets them all.

    ``None`` values are skipped (absent, not bound). The remover resets in
    reverse binding order and is idempotent.
    """
    tokens: list[tuple[str, Token[Any]]] = [
        (key, set_context_value(key, value)) for key, value in values.items() if value is not None
    ]

    def remove() -> None:
        while tokens:
            key, token = tokens.pop()
            reset_context_value(key, token)

    return remove

collect_warmers(base_warmers, warmers_factory, app_ctx) async

Collect warmers from base list and factory.

Source code in servicewright/core/warmup/orchestrator.py
async def collect_warmers(
    base_warmers: Sequence[AsyncWarmer] | None,
    warmers_factory: Callable[[ServiceContext[Any, Any]], Sequence[AsyncWarmer] | Awaitable[Sequence[AsyncWarmer]]]
    | None,
    app_ctx: ServiceContext[Any, Any],
) -> list[AsyncWarmer]:
    """Collect warmers from base list and factory."""
    warmers: list[AsyncWarmer] = list(base_warmers) if base_warmers else []

    if warmers_factory:
        res = await _resolve_warmers(warmers_factory(app_ctx))
        warmers.extend(res)

    return warmers

current_context()

Return every non-None value in the current context as a dict.

Source code in servicewright/core/context.py
def current_context() -> dict[str, Any]:
    """Return every non-``None`` value in the current context as a dict."""
    return {key: value for key, var in _CONTEXT_VARS.items() if (value := var.get()) is not None}

get_context_value(key, default=None)

Get key from the current context (default when unset).

Source code in servicewright/core/context.py
def get_context_value(key: str, default: Any = None) -> Any:
    """Get ``key`` from the current context (``default`` when unset)."""
    var = _CONTEXT_VARS.get(key)
    return default if var is None else var.get(default)

is_safe_context_id(value)

True when value is a well-formed correlation identifier.

Rejects empty, overlong (> 256 chars) and log-unsafe values (anything outside A-Za-z0-9 . _ - + = / space :). Transports use this to filter identifiers extracted from headers/metadata before binding them.

Source code in servicewright/core/context.py
def is_safe_context_id(value: str) -> bool:
    """True when ``value`` is a well-formed correlation identifier.

    Rejects empty, overlong (> 256 chars) and log-unsafe values (anything
    outside ``A-Za-z0-9 . _ - + = / space :``). Transports use this to filter
    identifiers extracted from headers/metadata before binding them.
    """
    return 0 < len(value) <= MAX_CONTEXT_ID_LENGTH and _SAFE_ID_PATTERN.fullmatch(value) is not None

mask_private_error(info)

Return the client-safe view: non-public errors collapse to a generic 500.

Public errors pass through unchanged. The caller is responsible for logging the original (that is where the real code/params must go).

Source code in servicewright/core/errors.py
def mask_private_error(info: ErrorInfo) -> ErrorInfo:
    """Return the client-safe view: non-public errors collapse to a generic 500.

    Public errors pass through unchanged. The caller is responsible for logging
    the original (that is where the real code/params must go).
    """
    if info.public:
        return info
    return ErrorInfo(kind=ErrorKind.INTERNAL, code=INTERNAL_ERROR_CODE, public=True)

perform_warmup(service_name, warmers, timeout=DEFAULT_WARMUP_TIMEOUT_SECONDS) async

Execute warmup with fail-fast behavior.

Source code in servicewright/core/warmup/orchestrator.py
async def perform_warmup(
    service_name: str,
    warmers: list[AsyncWarmer],
    timeout: float = DEFAULT_WARMUP_TIMEOUT_SECONDS,
) -> None:
    """Execute warmup with fail-fast behavior."""
    if not warmers:
        return

    if timeout <= 0:
        raise ValueError(f"timeout must be positive, got {timeout}")

    try:
        async with asyncio.timeout(timeout):
            await warmup_async(warmers=warmers, raise_on_failure=True)
        logger.info(
            "Infrastructure warmup completed",
            extra={"service": service_name, "warmers_count": len(warmers)},
        )
    except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
        raise
    except TimeoutError as exc:
        logger.exception(
            "Application warmup timed out",
            extra={
                "service": service_name,
                "timeout": timeout,
                "warmers_count": len(warmers),
            },
        )
        raise WarmupTimeoutError(
            f"Infrastructure warmup for service '{service_name}' did not finish within {timeout} seconds"
        ) from exc
    except Exception as exc:
        logger.exception(
            "Application warmup failed",
            extra={
                "service": service_name,
                "error_type": type(exc).__name__,
                "warmers_count": len(warmers),
            },
        )
        raise

propagation_metadata(keys=None)

Collect the current context as outbound headers / gRPC metadata.

Returns {header: value} for every mapped context key currently set — ready to merge into HTTP request headers or gRPC invocation metadata, e.g. as the callable feeding a client context interceptor.

Parameters:

Name Type Description Default
keys Mapping[str, str] | None

{context_key: header_name} mapping; None uses :data:STANDARD_PROPAGATION_HEADERS (request/user/tenant/trace).

None
Source code in servicewright/core/context.py
def propagation_metadata(keys: Mapping[str, str] | None = None) -> dict[str, str]:
    """Collect the current context as outbound headers / gRPC metadata.

    Returns ``{header: value}`` for every mapped context key currently set —
    ready to merge into HTTP request headers or gRPC invocation metadata, e.g.
    as the callable feeding a client context interceptor.

    Args:
        keys: ``{context_key: header_name}`` mapping; ``None`` uses
            :data:`STANDARD_PROPAGATION_HEADERS` (request/user/tenant/trace).
    """
    mapping = STANDARD_PROPAGATION_HEADERS if keys is None else keys
    metadata: dict[str, str] = {}
    for key, header in mapping.items():
        value = get_context_value(key)
        if value is not None:
            metadata[header] = str(value)
    return metadata

register_sink(concern, backend, target)

Register (or override) a backend as "module.path:ClassName".

Source code in servicewright/core/observability/registry.py
def register_sink(concern: str, backend: str, target: str) -> None:
    """Register (or override) a backend as ``"module.path:ClassName"``."""
    if ":" not in target:
        raise ValueError(f"Sink target must look like 'module.path:ClassName', got: {target!r}")
    _SINKS[(concern, backend)] = target

run(service, settings, *, stop=None) async

Module-level convenience: await servicewright.run(service, settings).

Source code in servicewright/core/service.py
async def run[TSettings: "BaseServiceSettingsProtocol", TContainer: "DependencyContainerProtocol"](
    service: Service[TSettings, TContainer],
    settings: TSettings,
    *,
    stop: asyncio.Event | None = None,
) -> None:
    """Module-level convenience: ``await servicewright.run(service, settings)``."""
    await service.run(settings, stop=stop)

set_context_value(key, value)

Set key in the current context; returns the token for reset.

Source code in servicewright/core/context.py
def set_context_value(key: str, value: Any) -> Token[Any]:
    """Set ``key`` in the current context; returns the token for reset."""
    return get_context_var(key).set(value)

warmup_async(warmers=None, raise_on_failure=True, timeout=None) async

Perform asynchronous warmup of provided infrastructure warmers.

Executes warmers in dependency order based on their priority. Warmers with the same priority are executed in parallel. Failures in one warmer do not cancel other warmers in the same priority group.

Parameters:

Name Type Description Default
warmers Sequence[AsyncWarmer] | None

A sequence of warmer instances to execute.

None
raise_on_failure bool

If True, raises WarmupError if any warmer fails. If False, only logs warnings.

True
timeout float | None

Maximum time to wait for all warmers to complete.

None

Raises:

Type Description
WarmupError

If any warmer fails and raise_on_failure is True.

Source code in servicewright/core/warmup/engine.py
async def warmup_async(
    warmers: Sequence[AsyncWarmer] | None = None,
    raise_on_failure: bool = True,
    timeout: float | None = None,
) -> None:
    """Perform asynchronous warmup of provided infrastructure warmers.

    Executes warmers in dependency order based on their priority.
    Warmers with the same priority are executed in parallel.
    Failures in one warmer do not cancel other warmers in the same priority group.

    Args:
        warmers: A sequence of warmer instances to execute.
        raise_on_failure: If True, raises WarmupError if any warmer fails.
            If False, only logs warnings.
        timeout: Maximum time to wait for all warmers to complete.

    Raises:
        WarmupError: If any warmer fails and raise_on_failure is True.
    """
    if not warmers:
        logger.debug("No infrastructure warmers provided")
        return

    logger.info(
        "Starting infrastructure warmup",
        extra={
            "count": len(warmers),
            "timeout": timeout,
            "warmers": [type(w).__name__ for w in warmers],
        },
    )

    # Sort and group warmers by priority, using index as tie-breaker to avoid comparing warmers themselves
    indexed_warmers: list[tuple[int, int, AsyncWarmer]] = [
        (int(warmer.priority), index, warmer) for index, warmer in enumerate(warmers)
    ]

    indexed_warmers.sort(key=lambda x: (x[0], x[1]))
    priority_groups: list[list[AsyncWarmer]] = [
        [item[2] for item in group] for _, group in itertools.groupby(indexed_warmers, key=lambda x: x[0])
    ]

    def process_result(warmer: AsyncWarmer, result: Any) -> Exception | None:
        if isinstance(result, asyncio.CancelledError):
            raise result
        if isinstance(result, KeyboardInterrupt | SystemExit):
            raise result
        if isinstance(result, Exception):
            warmer_raise_on_failure = warmer.raise_on_failure
            if not isinstance(warmer_raise_on_failure, bool):
                raise TypeError(f"raise_on_failure must be bool, got {type(warmer_raise_on_failure).__name__}")
            logger.warning(
                "Warmer failed",
                extra={
                    "warmer_type": type(warmer).__name__,
                    "error": str(result),
                    "type": type(result).__name__,
                    "raise_on_failure": warmer_raise_on_failure,
                },
            )
            if warmer_raise_on_failure:
                return result
        return None

    all_exceptions: list[Exception] = []

    async def run_groups() -> None:
        for group in priority_groups:
            # Execute group in parallel without cancelling each other on failure
            results = await asyncio.gather(
                *(w.warmup() for w in group),
                return_exceptions=True,
            )
            for warmer, result in zip(group, results, strict=True):
                exc = process_result(warmer, result)
                if exc:
                    all_exceptions.append(exc)
            if all_exceptions and raise_on_failure:
                break

    try:
        if timeout is not None:
            async with asyncio.timeout(timeout):
                await run_groups()
        else:
            async with contextlib.nullcontext():
                await run_groups()

    except TimeoutError as e:
        logger.exception("Infrastructure warmup timed out", extra={"timeout": timeout})
        if raise_on_failure:
            raise WarmupError(f"Infrastructure warmup timed out after {timeout}s") from e
    except (WarmupError, asyncio.CancelledError, KeyboardInterrupt, SystemExit):
        raise
    except Exception as e:
        # Unexpected errors outside of execution logic
        logger.exception("Unexpected error during infrastructure warmup", extra={"error_type": type(e).__name__})
        if raise_on_failure:
            raise WarmupError(f"Infrastructure warmup failed due to unexpected error: {e}") from e

    if all_exceptions and raise_on_failure:
        raise WarmupError(f"Infrastructure warmup failed with {len(all_exceptions)} errors")

    if not all_exceptions:
        logger.info("Infrastructure warmup completed successfully")
    else:
        logger.warning("Infrastructure warmup completed with some failures")