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

106 lines
4.6 KiB
Python

from contextlib import asynccontextmanager
from typing import AsyncIterator, cast
from uuid import UUID
import httpx
from fastapi import Depends, FastAPI, HTTPException, Request, Response, status
from app.checker import EndpointChecker, UnsafeTarget
from app.config import Settings, get_settings
from app.logging import configure_logging
from app.models import (CheckResult, CheckStatus, HealthResponse, Monitor,
MonitorCreate, MonitorUpdate)
from app.store import MonitorStore, Snapshot
def error(status_code: int, code: str, message: str) -> HTTPException:
return HTTPException(status_code=status_code,
detail={"code": code, "message": message})
def create_app(settings: Settings | None = None) -> FastAPI:
config = settings or get_settings()
configure_logging(config.log_level)
@asynccontextmanager
async def lifespan(application: FastAPI) -> AsyncIterator[None]:
application.state.store = MonitorStore()
async with httpx.AsyncClient() as client:
application.state.checker = EndpointChecker(client, config)
yield
application = FastAPI(title=config.app_name, version="1.0.0", lifespan=lifespan)
def store(request: Request) -> MonitorStore:
return cast(MonitorStore, request.app.state.store)
async def existing(monitor_id: UUID, repository: MonitorStore = Depends(store)) -> Snapshot:
snapshot = await repository.snapshot(monitor_id)
if snapshot is None:
raise error(404, "monitor_not_found", "Monitor not found")
return snapshot
@application.post("/v1/monitors", response_model=Monitor,
status_code=status.HTTP_201_CREATED)
async def create_monitor(data: MonitorCreate,
repository: MonitorStore = Depends(store)) -> Monitor:
return await repository.create(data)
@application.get("/v1/monitors", response_model=list[Monitor])
async def list_monitors(repository: MonitorStore = Depends(store)) -> list[Monitor]:
return await repository.list()
@application.get("/v1/monitors/{monitor_id}", response_model=Monitor)
async def get_monitor(snapshot: Snapshot = Depends(existing)) -> Monitor:
return snapshot.monitor
@application.patch("/v1/monitors/{monitor_id}", response_model=Monitor)
async def update_monitor(monitor_id: UUID, data: MonitorUpdate,
repository: MonitorStore = Depends(store)) -> Monitor:
item = await repository.update(monitor_id, data)
if item is None:
raise error(404, "monitor_not_found", "Monitor not found")
return item
@application.delete("/v1/monitors/{monitor_id}", status_code=204)
async def delete_monitor(monitor_id: UUID,
repository: MonitorStore = Depends(store)) -> Response:
if not await repository.delete(monitor_id):
raise error(404, "monitor_not_found", "Monitor not found")
return Response(status_code=204)
@application.post("/v1/monitors/{monitor_id}/checks", response_model=CheckResult)
async def run_check(request: Request, snapshot: Snapshot = Depends(existing),
repository: MonitorStore = Depends(store)) -> CheckResult:
checker = cast(EndpointChecker, request.app.state.checker)
try:
check_status = await checker.check(str(snapshot.monitor.url))
except UnsafeTarget as exc:
blocked = CheckStatus(state="error", error=str(exc))
await repository.apply_status(snapshot.monitor.id, snapshot.revision, blocked)
raise error(400, "unsafe_target", str(exc)) from exc
applied = await repository.apply_status(
snapshot.monitor.id, snapshot.revision, check_status
)
return CheckResult(monitor_id=snapshot.monitor.id, status_applied=applied,
**check_status.model_dump())
@application.get("/v1/monitors/{monitor_id}/status", response_model=CheckStatus)
async def current_status(snapshot: Snapshot = Depends(existing)) -> CheckStatus:
return snapshot.monitor.status
@application.get("/healthz", response_model=HealthResponse)
async def health() -> HealthResponse:
return HealthResponse(status="ok")
@application.get("/readyz", response_model=HealthResponse)
async def ready(request: Request) -> HealthResponse:
if not hasattr(request.app.state, "store") or not hasattr(request.app.state, "checker"):
raise error(503, "not_ready", "Service dependencies are not initialized")
return HealthResponse(status="ready")
return application
app = create_app()