diff --git a/app/store.py b/app/store.py index 8a1b1e0..2602d04 100644 --- a/app/store.py +++ b/app/store.py @@ -1,68 +1,81 @@ import asyncio from uuid import UUID, uuid4 -from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate, utcnow +from app.models import CurrentStatus, Monitor, MonitorInput, State, utcnow + + +class NotFoundError(Exception): + pass + + +class CapacityError(Exception): + pass + + +class StaleCheckError(Exception): + pass class MonitorStore: - """Lock-protected process-local storage returning defensive copies.""" - - def __init__(self) -> None: - self._lock = asyncio.Lock() + def __init__(self, capacity: int) -> None: self._items: dict[UUID, Monitor] = {} - self._revisions: dict[UUID, int] = {} + self._lock = asyncio.Lock() + self._capacity = capacity - 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 def create(self, data: MonitorInput) -> Monitor: async with self._lock: + if len(self._items) >= self._capacity: + raise CapacityError + now = utcnow() + item = Monitor( + id=uuid4(), name=data.name, url=data.url, created_at=now, + updated_at=now, current_status=CurrentStatus(), + ) 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] + return [item.model_copy(deep=True) for item in self._items.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 def get(self, monitor_id: UUID) -> Monitor: 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] + raise NotFoundError + return item.model_copy(deep=True) - async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor | None: + async def replace(self, monitor_id: UUID, data: MonitorInput) -> Monitor: 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) + current = self._items.get(monitor_id) + if current is None: + raise NotFoundError + status = current.current_status + if str(current.url) != str(data.url): + status = CurrentStatus(state=State.UNKNOWN) + item = current.model_copy(update={ + "name": data.name, "url": data.url, "updated_at": utcnow(), + "current_status": status, + }) + self._items[monitor_id] = item + return item.model_copy(deep=True) - async def delete(self, monitor_id: UUID) -> bool: + async def delete(self, monitor_id: UUID) -> None: async with self._lock: - existed = self._items.pop(monitor_id, None) is not None - self._revisions.pop(monitor_id, None) - return existed + if self._items.pop(monitor_id, None) is None: + raise NotFoundError - async def record_status(self, monitor_id: UUID, revision: int, - status: CurrentStatus) -> Monitor | None: + async def publish_status( + self, monitor_id: UUID, expected_url: str, status: CurrentStatus + ) -> Monitor: 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) + current = self._items.get(monitor_id) + if current is None: + raise NotFoundError + if str(current.url) != expected_url: + raise StaleCheckError + item = current.model_copy(update={ + "current_status": status, "updated_at": utcnow() + }) + self._items[monitor_id] = item + return item.model_copy(deep=True)