Files
crucible-agent-build-fastap…/app/main.py

102 lines
3.8 KiB
Python

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()