diff --git a/app/store.py b/app/store.py index 3298c5d..d0107cb 100644 --- a/app/store.py +++ b/app/store.py @@ -1,65 +1,53 @@ import asyncio +from datetime import UTC, datetime from uuid import UUID -from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate, now_utc - - -class MonitorNotFoundError(KeyError): - pass +from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate class MonitorStore: - """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. - """ + """Concurrency-safe process-local state; all values cross the lock as copies.""" def __init__(self) -> None: self._items: dict[UUID, Monitor] = {} self._lock = asyncio.Lock() async def create(self, data: MonitorCreate) -> Monitor: - monitor = Monitor(**data.model_dump()) + monitor = Monitor(name=data.name, url=data.url) async with self._lock: self._items[monitor.id] = monitor - return monitor + return monitor.model_copy(deep=True) async def list(self) -> list[Monitor]: async with self._lock: - return list(self._items.values()) + return [item.model_copy(deep=True) for item in self._items.values()] - async def get(self, monitor_id: UUID) -> Monitor: + async def get(self, monitor_id: UUID) -> Monitor | None: async with self._lock: - try: - return self._items[monitor_id] - except KeyError as exc: - raise MonitorNotFoundError(monitor_id) from exc + item = self._items.get(monitor_id) + return item.model_copy(deep=True) if item else None - async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor: + async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor | None: changes = data.model_dump(exclude_unset=True, exclude_none=True) async with self._lock: - 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()}) + item = self._items.get(monitor_id) + if item is None: + return None + updated = item.model_copy(update={**changes, "updated_at": datetime.now(UTC)}) self._items[monitor_id] = updated - return updated + return updated.model_copy(deep=True) - async def delete(self, monitor_id: UUID) -> None: + async def delete(self, monitor_id: UUID) -> bool: async with self._lock: - if self._items.pop(monitor_id, None) is None: - raise MonitorNotFoundError(monitor_id) + return self._items.pop(monitor_id, None) is not None - async def set_status(self, monitor_id: UUID, status: CurrentStatus) -> Monitor: + async def set_status(self, monitor_id: UUID, status: CurrentStatus) -> Monitor | None: async with self._lock: - 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()} + item = self._items.get(monitor_id) + if item is None: + return None + updated = item.model_copy( + update={"current_status": status, "updated_at": datetime.now(UTC)} ) self._items[monitor_id] = updated - return updated + return updated.model_copy(deep=True)