diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..4cc7cb0 --- /dev/null +++ b/app/api.py @@ -0,0 +1,92 @@ +from typing import Annotated +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status + +from app.checker import EndpointChecker +from app.models import CurrentStatus, Monitor, MonitorCreate, MonitorUpdate +from app.store import MonitorStore + +router = APIRouter() + + +def store(request: Request) -> MonitorStore: + return request.app.state.store + + +def checker(request: Request) -> EndpointChecker: + return request.app.state.checker + + +Store = Annotated[MonitorStore, Depends(store)] +Checker = Annotated[EndpointChecker, Depends(checker)] + + +def missing() -> HTTPException: + return HTTPException(status_code=404, detail=("monitor_not_found", "Monitor not found")) + + +@router.post("/v1/monitors", response_model=Monitor, status_code=status.HTTP_201_CREATED) +async def create_monitor(data: MonitorCreate, state: Store) -> Monitor: + return await state.create(data) + + +@router.get("/v1/monitors", response_model=list[Monitor]) +async def list_monitors(state: Store) -> list[Monitor]: + return await state.list() + + +@router.get("/v1/monitors/{monitor_id}", response_model=Monitor) +async def get_monitor(monitor_id: UUID, state: Store) -> Monitor: + item = await state.get(monitor_id) + if item is None: + raise missing() + return item + + +@router.patch("/v1/monitors/{monitor_id}", response_model=Monitor) +async def update_monitor(monitor_id: UUID, data: MonitorUpdate, state: Store) -> Monitor: + item = await state.update(monitor_id, data) + if item is None: + raise missing() + return item + + +@router.delete("/v1/monitors/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_monitor(monitor_id: UUID, state: Store) -> Response: + if not await state.delete(monitor_id): + raise missing() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.post("/v1/monitors/{monitor_id}/check", response_model=CurrentStatus) +async def check_monitor(monitor_id: UUID, state: Store, outbound: Checker) -> CurrentStatus: + snapshot = await state.snapshot(monitor_id) + if snapshot is None: + raise missing() + item, revision = snapshot + result = await outbound.check(str(item.url)) + if await state.record_status(monitor_id, revision, result) is None: + raise HTTPException(status_code=409, + detail=("monitor_changed", "Monitor changed while check was running")) + return result + + +@router.get("/v1/monitors/{monitor_id}/status", response_model=CurrentStatus) +async def get_status(monitor_id: UUID, state: Store) -> CurrentStatus: + item = await state.get(monitor_id) + if item is None: + raise missing() + return item.current_status + + +@router.get("/healthz") +async def health() -> dict[str, str]: + return {"status": "ok"} + + +@router.get("/readyz") +async def readiness(request: Request) -> dict[str, str]: + if not hasattr(request.app.state, "store"): + raise HTTPException(status_code=503, detail=("not_ready", "Service is not ready")) + return {"status": "ready", "storage": "process-local-memory"}