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