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

103 lines
4.5 KiB
Python

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import cast
from uuid import UUID
from fastapi import FastAPI, HTTPException, Request, Response, status
from app.checker import EndpointChecker
from app.config import Settings, get_settings
from app.logging_config import configure_logging
from app.models import CurrentStatus, Health, Monitor, MonitorCreate, MonitorUpdate
from app.security import UnsafeTarget
from app.store import MonitorNotFound, MonitorStore
def create_app(settings: Settings | None = None, checker: EndpointChecker | None = None) -> FastAPI:
runtime_settings = settings or get_settings()
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
configure_logging(runtime_settings.log_level)
app.state.store = MonitorStore()
app.state.checker = checker or EndpointChecker(runtime_settings)
app.state.ready = True
yield
app.state.ready = False
app = FastAPI(title="Endpoint Monitor", version="1.0.0", lifespan=lifespan)
def store(request: Request) -> MonitorStore:
return cast(MonitorStore, request.app.state.store)
@app.post("/monitors", response_model=Monitor, status_code=status.HTTP_201_CREATED)
async def create_monitor(data: MonitorCreate, request: Request) -> Monitor:
return await store(request).create(data)
@app.get("/monitors", response_model=list[Monitor])
async def list_monitors(request: Request) -> list[Monitor]:
return await store(request).list()
@app.get("/monitors/{monitor_id}", response_model=Monitor)
async def get_monitor(monitor_id: UUID, request: Request) -> Monitor:
try:
return await store(request).get(monitor_id)
except MonitorNotFound as exc:
raise HTTPException(status_code=404, detail="monitor not found") from exc
@app.put("/monitors/{monitor_id}", response_model=Monitor)
async def update_monitor(monitor_id: UUID, data: MonitorUpdate, request: Request) -> Monitor:
try:
return await store(request).update(monitor_id, data)
except MonitorNotFound as exc:
raise HTTPException(status_code=404, detail="monitor not found") from exc
@app.delete("/monitors/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_monitor(monitor_id: UUID, request: Request) -> Response:
try:
await store(request).delete(monitor_id)
except MonitorNotFound as exc:
raise HTTPException(status_code=404, detail="monitor not found") from exc
return Response(status_code=status.HTTP_204_NO_CONTENT)
@app.post("/monitors/{monitor_id}/check", response_model=CurrentStatus)
async def check_monitor(monitor_id: UUID, request: Request) -> CurrentStatus:
monitor_store = store(request)
try:
monitor = await monitor_store.get(monitor_id)
except MonitorNotFound as exc:
raise HTTPException(status_code=404, detail="monitor not found") from exc
active_checker = cast(EndpointChecker, request.app.state.checker)
try:
result = await active_checker.check(str(monitor.id), str(monitor.target_url))
except UnsafeTarget as exc:
blocked = EndpointChecker._status(0 if False else __import__("time").monotonic(), monitor.current_status.state, error=None)
del blocked # Policy failures are mapped to 400 and do not overwrite the last completed status.
raise HTTPException(status_code=400, detail=str(exc)) from exc
try:
return await monitor_store.set_status(monitor_id, result)
except MonitorNotFound as exc:
raise HTTPException(status_code=404, detail="monitor deleted while check was running") from exc
@app.get("/monitors/{monitor_id}/status", response_model=CurrentStatus)
async def get_monitor_status(monitor_id: UUID, request: Request) -> CurrentStatus:
try:
return (await store(request).get(monitor_id)).current_status
except MonitorNotFound as exc:
raise HTTPException(status_code=404, detail="monitor not found") from exc
@app.get("/health/live", response_model=Health)
async def liveness() -> Health:
return Health(status="ok")
@app.get("/health/ready", response_model=Health)
async def readiness(request: Request) -> Health:
if not getattr(request.app.state, "ready", False):
raise HTTPException(status_code=503, detail="not ready")
return Health(status="ready")
return app
app = create_app()