Source code for fastrag.ingestion.policy

"""Execution policy controlling operational constraints for pipeline steps."""

from __future__ import annotations

from dataclasses import dataclass, field, fields
from typing import Any, Literal

from fastrag.exceptions import ConfigurationError

#: Failure handling strategy a step's evaluator applies to a raised exception.
OnError = Literal["skip", "raise", "retry"]


class _Unset:
    """Sentinel marking a ``Policy`` field the caller did not pass."""

    __slots__ = ()

    def __repr__(self) -> str:
        return "UNSET"


_UNSET: Any = _Unset()


[docs] @dataclass(frozen=True, slots=True) class Policy: """Execution policy controlling operational constraints for pipeline steps. Attributes: retries: Maximum number of execution retries on failure. backoff: Initial exponential backoff delay in seconds. max_backoff: Ceiling delay for exponential backoff in seconds. timeout: Maximum execution duration per item or batch in seconds. on_error: Failure handling strategy (``"skip"``, ``"raise"``, ``"retry"``). concurrency: Max concurrent worker tasks. batch_size: Processing batch size for batched transforms. cache_batch_size: Cache lookup window, independent of transform concurrency. recreate_cache: Skip cached results and overwrite them with freshly recomputed values. A step's policy is layered over its parent's (the pipeline's, or an enclosing router/aggregator's) with :meth:`merge`: only the fields the caller *explicitly* passed override the parent; the rest are inherited. """ retries: int = _UNSET backoff: float = _UNSET max_backoff: float = _UNSET timeout: float = _UNSET on_error: OnError = _UNSET concurrency: int = _UNSET batch_size: int = _UNSET cache_batch_size: int = _UNSET recreate_cache: bool = _UNSET _explicit: frozenset[str] = field( default=frozenset(), init=False, repr=False, compare=False ) def __post_init__(self) -> None: """Fill unset fields with defaults, then validate.""" explicit: set[str] = set() for spec in fields(self): if spec.name == "_explicit": continue if getattr(self, spec.name) is _UNSET: default = _DEFAULTS[spec.name] object.__setattr__(self, spec.name, default) else: explicit.add(spec.name) object.__setattr__(self, "_explicit", frozenset(explicit)) if self.concurrency < 1: raise ConfigurationError("Policy.concurrency must be at least 1") if self.retries < 0: raise ConfigurationError("Policy.retries cannot be negative") if self.batch_size < 1: raise ConfigurationError("Policy.batch_size must be at least 1") if self.cache_batch_size < 1: raise ConfigurationError("Policy.cache_batch_size must be at least 1")
[docs] def merge(self, base: Policy | None) -> Policy: """Return this policy layered over ``base``. Fields this policy set explicitly win; every other field is taken from ``base``. The result's explicit set is the union of both, so a chain of merges keeps resolving correctly. Args: base: The inherited (parent) policy, or ``None`` for no parent. """ if base is None or base is self: return self values = { name: getattr(self if name in self._explicit else base, name) for name in _DEFAULTS if name in self._explicit or name in base._explicit } return Policy(**values)
[docs] def delay_for(self, attempt: int) -> float: """Return the capped exponential backoff delay before ``attempt``. Args: attempt: The retry attempt number, 1 for the first retry. Returns: ``backoff * 2 ** (attempt - 1)``, capped at ``max_backoff``. """ return min(self.backoff * (2 ** (attempt - 1)), self.max_backoff)
_DEFAULTS: dict[str, Any] = { "retries": 3, "backoff": 1.0, "max_backoff": 10.0, "timeout": 30.0, "on_error": "skip", "concurrency": 8, "batch_size": 16, "cache_batch_size": 128, "recreate_cache": False, }