Source code for fastrag.ingestion.filters.base

"""``Filter``: the composable predicate base every concrete filter builds on.

A filter is a small predicate over a :class:`~fastrag.ingestion.models.Source`
or :class:`~fastrag.models.Document` that composes with ``&``, ``|`` and ``~``
instead of being written as one growing conditional, e.g.
``html_filter & agrospai_filter``.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Generic, TypeVar

T_contra = TypeVar("T_contra", contravariant=True)


[docs] class Filter(ABC, Generic[T_contra]): """Base abstract class for composable entity filtering.""" @property def header_safe(self) -> bool: """Whether matching uses identity/metadata only, never content/vectors. Custom filters default to full records. Override only for predicates that remain correct on the index-only cache projection. """ return False
[docs] @abstractmethod def matches(self, item: T_contra) -> bool: """Evaluate whether an entity passes the filter criteria. Args: item: Item to evaluate (e.g. a ``Source`` or ``Document``). Returns: True if the item satisfies the condition, False otherwise. """ ...
def __call__(self, item: T_contra) -> bool: """Evaluate the filter against ``item``. Equivalent to :meth:`matches`.""" return self.matches(item) def __and__[U](self: Filter[U], other: Filter[U]) -> Filter[U]: """Compose two filters using logical AND.""" return _AndFilter(self, other) def __or__[U](self: Filter[U], other: Filter[U]) -> Filter[U]: """Compose two filters using logical OR.""" return _OrFilter(self, other) def __invert__(self) -> Filter[T_contra]: """Invert the filter using logical NOT.""" return _NotFilter(self)
class _AndFilter(Filter[T_contra]): """Matches when both composed filters match.""" def __init__(self, left: Filter[T_contra], right: Filter[T_contra]) -> None: self._left = left self._right = right @property def header_safe(self) -> bool: """Both predicates must accept cache headers.""" return self._left.header_safe and self._right.header_safe def matches(self, item: T_contra) -> bool: """True when both composed filters match ``item``.""" return self._left.matches(item) and self._right.matches(item) class _OrFilter(Filter[T_contra]): """Matches when either composed filter matches.""" def __init__(self, left: Filter[T_contra], right: Filter[T_contra]) -> None: self._left = left self._right = right @property def header_safe(self) -> bool: """Both predicates must accept cache headers.""" return self._left.header_safe and self._right.header_safe def matches(self, item: T_contra) -> bool: """True when either composed filter matches ``item``.""" return self._left.matches(item) or self._right.matches(item) class _NotFilter(Filter[T_contra]): """Matches when the wrapped filter does not.""" def __init__(self, inner: Filter[T_contra]) -> None: self._inner = inner @property def header_safe(self) -> bool: """The wrapped predicate must accept cache headers.""" return self._inner.header_safe def matches(self, item: T_contra) -> bool: """True when the wrapped filter does not match ``item``.""" return not self._inner.matches(item)