Skip to content

fathom.InMemoryFactStore

fathom.InMemoryFactStore

Default in-memory implementation of :class:FactStore.

Source code in src/fathom/fleet.py
class InMemoryFactStore:
    """Default in-memory implementation of :class:`FactStore`."""

    def __init__(self) -> None:
        # template -> {fact_id -> data}
        self._facts: dict[str, dict[FactId, FactData]] = defaultdict(dict)
        # template -> [callback, ...]
        self._subscribers: dict[str, list[_SubscriptionCallback]] = defaultdict(list)

    # ------------------------------------------------------------------
    # helpers
    # ------------------------------------------------------------------

    def _matches(self, data: FactData, fact_filter: FactFilter | None) -> bool:
        """Return True if *data* satisfies every key/value pair in *fact_filter*."""
        if not fact_filter:
            return True
        return all(data.get(k) == v for k, v in fact_filter.items())

    async def _notify(self, notification: FactChangeNotification) -> None:
        for cb in self._subscribers.get(notification.template, []):
            await cb(notification)

    # ------------------------------------------------------------------
    # FactStore interface
    # ------------------------------------------------------------------

    async def assert_fact(self, template: str, data: FactData) -> FactId:
        fact_id = uuid.uuid4().hex
        self._facts[template][fact_id] = data
        await self._notify(
            FactChangeNotification(template=template, fact_id=fact_id, action="assert", data=data)
        )
        return fact_id

    async def query(self, template: str, fact_filter: FactFilter | None = None) -> list[FactData]:
        return [
            {"fact_id": fid, **data}
            for fid, data in self._facts.get(template, {}).items()
            if self._matches(data, fact_filter)
        ]

    async def retract(self, template: str, fact_filter: FactFilter | None = None) -> int:
        to_remove = [
            fid
            for fid, data in self._facts.get(template, {}).items()
            if self._matches(data, fact_filter)
        ]
        for fid in to_remove:
            data = self._facts[template].pop(fid)
            await self._notify(
                FactChangeNotification(template=template, fact_id=fid, action="retract", data=data)
            )
        return len(to_remove)

    async def count(self, template: str, fact_filter: FactFilter | None = None) -> int:
        return sum(
            1
            for data in self._facts.get(template, {}).values()
            if self._matches(data, fact_filter)
        )

    async def subscribe(
        self,
        template: str,
        callback: _SubscriptionCallback,
    ) -> Callable[[], None]:
        self._subscribers[template].append(callback)

        def unsubscribe() -> None:
            with contextlib.suppress(ValueError):
                self._subscribers[template].remove(callback)

        return unsubscribe