Skip to content

Configuration Reference

pico_actuator.config

Configuration primitives for pico-actuator.

Defines :class:ActuatorSettings (loaded from the actuator config prefix) and the :class:HealthIndicator / :class:InfoContributor protocols. Both protocols mirror the FastApiConfigurer idiom in pico-fastapi: implement the protocol, mark the class @component, and it is auto-collected by pico-ioc via List[...] injection.

HealthIndicator

Bases: Protocol

A component contributing one entry to /actuator/health.

Attributes:

Name Type Description
name str

Stable key shown under components in the health payload.

The check method may be sync or async and may return either a mapping ({"status": "UP", ...}) or a plain truthy/falsy value.

Source code in src/pico_actuator/config.py
@runtime_checkable
class HealthIndicator(Protocol):
    """A component contributing one entry to ``/actuator/health``.

    Attributes:
        name: Stable key shown under ``components`` in the health payload.

    The ``check`` method may be sync or async and may return either a mapping
    (``{"status": "UP", ...}``) or a plain truthy/falsy value.
    """

    name: str

    def check(self) -> Dict[str, Any] | bool:
        """Probe the dependency. Return a mapping or a truthy value for UP."""
        ...

check()

Probe the dependency. Return a mapping or a truthy value for UP.

Source code in src/pico_actuator/config.py
def check(self) -> Dict[str, Any] | bool:
    """Probe the dependency. Return a mapping or a truthy value for UP."""
    ...

InfoContributor

Bases: Protocol

A component contributing key/values to /actuator/info.

Source code in src/pico_actuator/config.py
@runtime_checkable
class InfoContributor(Protocol):
    """A component contributing key/values to ``/actuator/info``."""

    def contribute(self) -> Dict[str, Any]:
        """Return a mapping merged into the /info payload."""
        ...

contribute()

Return a mapping merged into the /info payload.

Source code in src/pico_actuator/config.py
def contribute(self) -> Dict[str, Any]:
    """Return a mapping merged into the /info payload."""
    ...

ActuatorSettings dataclass

Type-safe actuator settings, populated from the actuator prefix.

Mirrors FastApiSettings in pico-fastapi.

Attributes:

Name Type Description
enabled bool

Master switch for the actuator endpoints.

show_components bool

Include per-indicator detail in /health.

check_timeout_seconds float

Per-indicator time budget; a slower check() reports DOWN with a timeout error instead of hanging the endpoint.

info Dict[str, Any]

Static key/values exposed by /info (build, git sha, ...).

Example

.. code-block:: yaml

# application.yaml
actuator:
  enabled: true
  show_components: true
  check_timeout_seconds: 5.0
  info:
    app: my-service
    build: "2026.06"
Source code in src/pico_actuator/config.py
@configured(target="self", prefix="actuator", mapping="tree")
@dataclass
class ActuatorSettings:
    """Type-safe actuator settings, populated from the ``actuator`` prefix.

    Mirrors ``FastApiSettings`` in pico-fastapi.

    Attributes:
        enabled: Master switch for the actuator endpoints.
        show_components: Include per-indicator detail in ``/health``.
        check_timeout_seconds: Per-indicator time budget; a slower ``check()``
            reports ``DOWN`` with a timeout error instead of hanging the endpoint.
        info: Static key/values exposed by ``/info`` (build, git sha, ...).

    Example:
        .. code-block:: yaml

            # application.yaml
            actuator:
              enabled: true
              show_components: true
              check_timeout_seconds: 5.0
              info:
                app: my-service
                build: "2026.06"
    """

    enabled: bool = True
    show_components: bool = True
    check_timeout_seconds: float = 5.0
    info: Dict[str, Any] = field(default_factory=dict)

pico_actuator.controller

Actuator endpoints.

Reuses pico-fastapi's @controller / @get instead of hand-rolling an APIRouter, so route wiring, DI resolution and result normalization are inherited for free. Dependencies (settings, indicators, contributors) are constructor-injected; List[Protocol] injection is the same mechanism pico-fastapi uses for List[FastApiConfigurer].

ActuatorController

Source code in src/pico_actuator/controller.py
@controller(prefix="/actuator", tags=["actuator"])
class ActuatorController:
    def __init__(
        self,
        settings: ActuatorSettings,
        indicators: List[HealthIndicator],
        contributors: List[InfoContributor],
        container: PicoContainer,
    ):
        self.settings = settings
        self.indicators = indicators
        self.contributors = contributors
        self.container = container

    async def _gather(self):
        return await gather(self.indicators, timeout=self.settings.check_timeout_seconds)

    @get("/health")
    async def health(self):
        """Full health: overall status plus per-component detail."""
        if not self.settings.enabled:
            return _DISABLED
        overall, components = await self._gather()
        body = {"status": overall}
        if self.settings.show_components:
            body["components"] = components
        # (content, status) tuple — pico-fastapi normalizes this to a JSONResponse.
        return body, 200 if overall == UP else 503

    @get("/health/live")
    async def liveness(self):
        """Liveness: is the process responding at all? Cheap, touches no deps."""
        if not self.settings.enabled:
            return _DISABLED
        return {"status": UP}

    @get("/health/ready")
    async def readiness(self):
        """Readiness: are dependencies healthy? Aggregate of all indicators."""
        if not self.settings.enabled:
            return _DISABLED
        overall, _ = await self._gather()
        return {"status": overall}, 200 if overall == UP else 503

    @get("/info")
    async def info(self):
        """Static config info merged with dynamic contributors."""
        if not self.settings.enabled:
            return _DISABLED
        data = dict(self.settings.info)
        for c in self.contributors:
            data.update(c.contribute())
        return data

    @post("/refresh")
    async def refresh(self):
        """Re-read config sources; report the changed top-level prefixes.

        Mirror of Spring Cloud's ``POST /actuator/refresh``: components
        subscribed to ``ConfigChanged`` re-read their config.
        """
        if not self.settings.enabled:
            return _DISABLED
        return {"changed": sorted(self.container.refresh_config())}

    @get("/metrics")
    async def metrics(self, request: Request):
        """Prometheus/OpenMetrics exposition of the default registry.

        Honors the ``Accept`` header (``application/openmetrics-text`` or
        Prometheus text 0.0.4). Needs the ``metrics`` extra; answers 501
        when prometheus-client is not installed.
        """
        if not self.settings.enabled:
            return _DISABLED
        try:
            from prometheus_client import REGISTRY
            from prometheus_client.exposition import choose_encoder
        except ImportError:
            return {"detail": "prometheus-client not installed; pip install pico-actuator[metrics]"}, 501
        encoder, content_type = choose_encoder(request.headers.get("accept", ""))
        return Response(content=encoder(REGISTRY), media_type=content_type)

health() async

Full health: overall status plus per-component detail.

Source code in src/pico_actuator/controller.py
@get("/health")
async def health(self):
    """Full health: overall status plus per-component detail."""
    if not self.settings.enabled:
        return _DISABLED
    overall, components = await self._gather()
    body = {"status": overall}
    if self.settings.show_components:
        body["components"] = components
    # (content, status) tuple — pico-fastapi normalizes this to a JSONResponse.
    return body, 200 if overall == UP else 503

liveness() async

Liveness: is the process responding at all? Cheap, touches no deps.

Source code in src/pico_actuator/controller.py
@get("/health/live")
async def liveness(self):
    """Liveness: is the process responding at all? Cheap, touches no deps."""
    if not self.settings.enabled:
        return _DISABLED
    return {"status": UP}

readiness() async

Readiness: are dependencies healthy? Aggregate of all indicators.

Source code in src/pico_actuator/controller.py
@get("/health/ready")
async def readiness(self):
    """Readiness: are dependencies healthy? Aggregate of all indicators."""
    if not self.settings.enabled:
        return _DISABLED
    overall, _ = await self._gather()
    return {"status": overall}, 200 if overall == UP else 503

info() async

Static config info merged with dynamic contributors.

Source code in src/pico_actuator/controller.py
@get("/info")
async def info(self):
    """Static config info merged with dynamic contributors."""
    if not self.settings.enabled:
        return _DISABLED
    data = dict(self.settings.info)
    for c in self.contributors:
        data.update(c.contribute())
    return data

refresh() async

Re-read config sources; report the changed top-level prefixes.

Mirror of Spring Cloud's POST /actuator/refresh: components subscribed to ConfigChanged re-read their config.

Source code in src/pico_actuator/controller.py
@post("/refresh")
async def refresh(self):
    """Re-read config sources; report the changed top-level prefixes.

    Mirror of Spring Cloud's ``POST /actuator/refresh``: components
    subscribed to ``ConfigChanged`` re-read their config.
    """
    if not self.settings.enabled:
        return _DISABLED
    return {"changed": sorted(self.container.refresh_config())}

metrics(request) async

Prometheus/OpenMetrics exposition of the default registry.

Honors the Accept header (application/openmetrics-text or Prometheus text 0.0.4). Needs the metrics extra; answers 501 when prometheus-client is not installed.

Source code in src/pico_actuator/controller.py
@get("/metrics")
async def metrics(self, request: Request):
    """Prometheus/OpenMetrics exposition of the default registry.

    Honors the ``Accept`` header (``application/openmetrics-text`` or
    Prometheus text 0.0.4). Needs the ``metrics`` extra; answers 501
    when prometheus-client is not installed.
    """
    if not self.settings.enabled:
        return _DISABLED
    try:
        from prometheus_client import REGISTRY
        from prometheus_client.exposition import choose_encoder
    except ImportError:
        return {"detail": "prometheus-client not installed; pip install pico-actuator[metrics]"}, 501
    encoder, content_type = choose_encoder(request.headers.get("accept", ""))
    return Response(content=encoder(REGISTRY), media_type=content_type)

pico_actuator.health

Health aggregation.

Pure logic on purpose: no FastAPI and no DI here, so it can be unit-tested without booting an app. The controller layer just calls :func:gather.

Indicators run concurrently, each under its own timeout. Sync check() methods are pushed to a worker thread so a slow blocking probe cannot freeze the event loop.

gather(indicators, timeout=DEFAULT_TIMEOUT) async

Aggregate all indicators into an (overall, components) pair.

Indicators are probed concurrently; each gets its own timeout seconds.

Parameters:

Name Type Description Default
indicators List[HealthIndicator]

The health indicators to probe.

required
timeout float

Per-indicator time budget in seconds.

DEFAULT_TIMEOUT

Returns:

Type Description
str

overall is DOWN if any component is DOWN, else UP.

Dict[str, Any]

components maps each indicator name to its status dict.

Source code in src/pico_actuator/health.py
async def gather(indicators: List["HealthIndicator"], timeout: float = DEFAULT_TIMEOUT) -> Tuple[str, Dict[str, Any]]:
    """Aggregate all indicators into an ``(overall, components)`` pair.

    Indicators are probed concurrently; each gets its own *timeout* seconds.

    Args:
        indicators: The health indicators to probe.
        timeout: Per-indicator time budget in seconds.

    Returns:
        ``overall`` is ``DOWN`` if any component is ``DOWN``, else ``UP``.
        ``components`` maps each indicator name to its status dict.
    """
    results = await asyncio.gather(*(_result(ind, timeout) for ind in indicators))
    components: Dict[str, Any] = {
        getattr(ind, "name", ind.__class__.__name__): res for ind, res in zip(indicators, results)
    }
    overall = DOWN if any(c["status"] == DOWN for c in components.values()) else UP
    return overall, components

pico_actuator.exceptions

Custom exceptions for pico-actuator.

PicoActuatorError

Bases: Exception

Base exception for all pico-actuator errors.

Catch this at startup boundaries to handle any pico-actuator failure without matching individual subclasses.

Source code in src/pico_actuator/exceptions.py
4
5
6
7
8
9
class PicoActuatorError(Exception):
    """Base exception for all pico-actuator errors.

    Catch this at startup boundaries to handle any pico-actuator failure
    without matching individual subclasses.
    """