diff --git a/app/store.py b/app/store.py index 2602d04..3298c5d 100644 --- a/app/store.py +++ b/app/store.py @@ -1,81 +1,65 @@ import asyncio -from uuid import UUID, uuid4 +from uuid import UUID -from app.models import CurrentStatus, Monitor, MonitorInput, State, utcnow +from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate, now_utc -class NotFoundError(Exception): - pass - - -class CapacityError(Exception): - pass - - -class StaleCheckError(Exception): +class MonitorNotFoundError(KeyError): pass class MonitorStore: - def __init__(self, capacity: int) -> None: + """Concurrency-safe, process-local monitor storage. + + Models are frozen and every read returns an immutable value, so callers cannot mutate + state outside the lock. All read-modify-write operations happen under one asyncio lock. + """ + + def __init__(self) -> None: self._items: dict[UUID, Monitor] = {} self._lock = asyncio.Lock() - self._capacity = capacity - async def create(self, data: MonitorInput) -> Monitor: + async def create(self, data: MonitorCreate) -> Monitor: + monitor = Monitor(**data.model_dump()) 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 - return item.model_copy(deep=True) + self._items[monitor.id] = monitor + return monitor async def list(self) -> list[Monitor]: async with self._lock: - return [item.model_copy(deep=True) for item in self._items.values()] + return list(self._items.values()) async def get(self, monitor_id: UUID) -> Monitor: async with self._lock: - item = self._items.get(monitor_id) - if item is None: - raise NotFoundError - return item.model_copy(deep=True) + try: + return self._items[monitor_id] + except KeyError as exc: + raise MonitorNotFoundError(monitor_id) from exc - async def replace(self, monitor_id: UUID, data: MonitorInput) -> Monitor: + async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor: + changes = data.model_dump(exclude_unset=True, exclude_none=True) async with self._lock: - 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) + try: + existing = self._items[monitor_id] + except KeyError as exc: + raise MonitorNotFoundError(monitor_id) from exc + updated = existing.model_copy(update={**changes, "updated_at": now_utc()}) + self._items[monitor_id] = updated + return updated async def delete(self, monitor_id: UUID) -> None: async with self._lock: if self._items.pop(monitor_id, None) is None: - raise NotFoundError + raise MonitorNotFoundError(monitor_id) - async def publish_status( - self, monitor_id: UUID, expected_url: str, status: CurrentStatus - ) -> Monitor: + async def set_status(self, monitor_id: UUID, status: CurrentStatus) -> Monitor: async with self._lock: - 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) + try: + existing = self._items[monitor_id] + except KeyError as exc: + raise MonitorNotFoundError(monitor_id) from exc + updated = existing.model_copy( + update={"current_status": status, "updated_at": now_utc()} + ) + self._items[monitor_id] = updated + return updated