diff --git a/app/store.py b/app/store.py new file mode 100644 index 0000000..8a1b1e0 --- /dev/null +++ b/app/store.py @@ -0,0 +1,68 @@ +import asyncio +from uuid import UUID, uuid4 + +from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate, utcnow + + +class MonitorStore: + """Lock-protected process-local storage returning defensive copies.""" + + def __init__(self) -> None: + self._lock = asyncio.Lock() + self._items: dict[UUID, Monitor] = {} + self._revisions: dict[UUID, int] = {} + + async def create(self, data: MonitorCreate) -> Monitor: + now = utcnow() + item = Monitor(id=uuid4(), name=data.name, url=data.url, created_at=now, + updated_at=now, current_status=CurrentStatus()) + async with self._lock: + self._items[item.id] = item + self._revisions[item.id] = 0 + return item.model_copy(deep=True) + + async def list(self) -> list[Monitor]: + async with self._lock: + values = sorted(self._items.values(), key=lambda item: item.created_at) + return [item.model_copy(deep=True) for item in values] + + async def get(self, monitor_id: UUID) -> Monitor | None: + async with self._lock: + item = self._items.get(monitor_id) + return item.model_copy(deep=True) if item else None + + async def snapshot(self, monitor_id: UUID) -> tuple[Monitor, int] | None: + async with self._lock: + item = self._items.get(monitor_id) + if item is None: + return None + return item.model_copy(deep=True), self._revisions[monitor_id] + + async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor | None: + async with self._lock: + item = self._items.get(monitor_id) + if item is None: + return None + changes = data.model_dump(exclude_none=True) + changes["updated_at"] = utcnow() + updated = item.model_copy(update=changes, deep=True) + self._items[monitor_id] = updated + self._revisions[monitor_id] += 1 + return updated.model_copy(deep=True) + + async def delete(self, monitor_id: UUID) -> bool: + async with self._lock: + existed = self._items.pop(monitor_id, None) is not None + self._revisions.pop(monitor_id, None) + return existed + + async def record_status(self, monitor_id: UUID, revision: int, + status: CurrentStatus) -> Monitor | None: + async with self._lock: + item = self._items.get(monitor_id) + if item is None or self._revisions[monitor_id] != revision: + return None + updated = item.model_copy(update={"current_status": status, + "updated_at": utcnow()}, deep=True) + self._items[monitor_id] = updated + return updated.model_copy(deep=True)