from contextlib import asynccontextmanager from typing import AsyncIterator from uuid import UUID from fastapi import Depends, FastAPI, HTTPException, Request, Response, status from app.checker import EndpointChecker from app.config import Settings, get_settings from app.logging import configure_logging from app.models import CheckResponse, CurrentStatus, Monitor, MonitorCreate, MonitorUpdate from app.store import MonitorStore def not_found() -> HTTPException: return HTTPException(status_code=404, detail={"code": "monitor_not_found", "message": "monitor does not exist"}) def get_store(request: Request) -> MonitorStore: return request.app.state.store def get_checker(request: Request) -> EndpointChecker: return request.app.state.checker def create_app( settings: Settings | None = None, store: MonitorStore | None = None, checker: EndpointChecker | None = None, ) -> FastAPI: config = settings or get_settings() @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: configure_logging(config.log_level) yield app = FastAPI(title="Endpoint Monitor Service", version="0.1.0", lifespan=lifespan) app.state.store = store or MonitorStore() app.state.checker = checker or EndpointChecker(config) @app.get("/health") async def health() -> dict[str, str]: return {"status": "ok"} @app.get("/ready") async def ready() -> dict[str, str]: return {"status": "ready", "storage": "process-local-memory"} @app.post("/monitors", response_model=Monitor, status_code=status.HTTP_201_CREATED) async def create_monitor(value: MonitorCreate, state: MonitorStore = Depends(get_store)) -> Monitor: return await state.create(value) @app.get("/monitors", response_model=list[Monitor]) async def list_monitors(state: MonitorStore = Depends(get_store)) -> list[Monitor]: return await state.list() @app.get("/monitors/{monitor_id}", response_model=Monitor) async def read_monitor(monitor_id: UUID, state: MonitorStore = Depends(get_store)) -> Monitor: item = await state.get(monitor_id) if item is None: raise not_found() return item @app.put("/monitors/{monitor_id}", response_model=Monitor) async def update_monitor(monitor_id: UUID, value: MonitorUpdate, state: MonitorStore = Depends(get_store)) -> Monitor: item = await state.update(monitor_id, value) if item is None: raise not_found() return item @app.delete("/monitors/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_monitor(monitor_id: UUID, state: MonitorStore = Depends(get_store)) -> Response: if not await state.delete(monitor_id): raise not_found() return Response(status_code=status.HTTP_204_NO_CONTENT) @app.post("/monitors/{monitor_id}/check", response_model=CheckResponse) async def run_check( monitor_id: UUID, state: MonitorStore = Depends(get_store), endpoint_checker: EndpointChecker = Depends(get_checker), ) -> CheckResponse: snapshot = await state.get(monitor_id) if snapshot is None: raise not_found() result = await endpoint_checker.check(str(snapshot.url), str(monitor_id)) applied = await state.set_status_if_current(monitor_id, snapshot.revision, result) return CheckResponse(monitor_id=monitor_id, status=result, applied=applied) @app.get("/monitors/{monitor_id}/status", response_model=CurrentStatus | None) async def current_status(monitor_id: UUID, state: MonitorStore = Depends(get_store)) -> CurrentStatus | None: item = await state.get(monitor_id) if item is None: raise not_found() return item.current_status return app app = create_app()