import asyncio from datetime import UTC, datetime from uuid import UUID, uuid4 from .models import CheckResult, Monitor, MonitorCreate, MonitorStatus, MonitorUpdate class StoreError(Exception): pass class NotFound(StoreError): pass class Conflict(StoreError): pass class MonitorStore: def __init__(self, max_monitors: int) -> None: self._items: dict[UUID, Monitor] = {} self._lock = asyncio.Lock() self._max = max_monitors async def create(self, data: MonitorCreate) -> Monitor: async with self._lock: if len(self._items) >= self._max: raise Conflict("monitor capacity reached") if any(item.name == data.name for item in self._items.values()): raise Conflict("monitor name already exists") now = datetime.now(UTC) item = Monitor(id=uuid4(), name=data.name, url=data.url, created_at=now, updated_at=now, revision=1) self._items[item.id] = item return item.model_copy(deep=True) async def list(self) -> list[Monitor]: async with self._lock: return [item.model_copy(deep=True) for item in 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 NotFound("monitor not found") return item.model_copy(deep=True) async def update(self, monitor_id: UUID, data: MonitorUpdate) -> Monitor: async with self._lock: item = self._items.get(monitor_id) if item is None: raise NotFound("monitor not found") changes = data.model_dump(exclude_unset=True) if "name" in changes and changes["name"] is None: changes.pop("name") if "url" in changes and changes["url"] is None: changes.pop("url") name = changes.get("name") if name and any(other.id != monitor_id and other.name == name for other in self._items.values()): raise Conflict("monitor name already exists") if "url" in changes and changes["url"] != item.url: changes.update(status=MonitorStatus.UNKNOWN, last_check=None) changes.update(updated_at=datetime.now(UTC), revision=item.revision + 1) item = item.model_copy(update=changes) self._items[monitor_id] = item return item.model_copy(deep=True) async def delete(self, monitor_id: UUID) -> None: async with self._lock: if self._items.pop(monitor_id, None) is None: raise NotFound("monitor not found") async def record_check(self, monitor_id: UUID, result: CheckResult) -> Monitor: async with self._lock: item = self._items.get(monitor_id) if item is None: raise NotFound("monitor not found") item = item.model_copy(update={"status": result.status, "last_check": result, "updated_at": datetime.now(UTC), "revision": item.revision + 1}) self._items[monitor_id] = item return item.model_copy(deep=True)