""" IOM Lager 6 — Observationer FastAPI endpoints för CRUD. """ from __future__ import annotations from datetime import datetime from typing import Any from fastapi import APIRouter, Depends, FastAPI, HTTPException, Query, status from pydantic import BaseModel, Field from observation_models import ( AIAnalysis, ConditionGrade, Finding, GeoTag, Media, Observation, RiskAssessment, Weather, ) from observation_store import ObservationStore, get_store # ────────────────────────────────────────────────────────────── # FastAPI app & router # ────────────────────────────────────────────────────────────── app = FastAPI( title="IOM Observation API", description="Lager 6 — Observationer för Infrastructure Object Model", version="1.0.0", ) router = APIRouter(prefix="/observations", tags=["observations"]) # ────────────────────────────────────────────────────────────── # Dependency injection # ────────────────────────────────────────────────────────────── async def get_observation_store() -> ObservationStore: store = get_store() try: yield store finally: await store.close() # ────────────────────────────────────────────────────────────── # Request / Response DTOs # ────────────────────────────────────────────────────────────── class ObservationCreate(BaseModel): id: str = Field(..., pattern=r"^OBS-[0-9]{4}-[0-9]{7}$") timestamp: datetime observer: str | None = None object_goid: str = Field(..., pattern=r"^[A-Z]{3}(-[A-Z]{3})+-[0-9]+$") findings: list[Finding] = Field(default_factory=list) media: list[Media] = Field(default_factory=list) weather: Weather | None = None ai_analysis: AIAnalysis | None = None risk_assessment: RiskAssessment | None = None condition: ConditionGrade | None = None notes: str | None = None extra: dict[str, Any] | None = None class ObservationUpdate(BaseModel): timestamp: datetime | None = None observer: str | None = None findings: list[Finding] | None = None media: list[Media] | None = None weather: Weather | None = None ai_analysis: AIAnalysis | None = None risk_assessment: RiskAssessment | None = None condition: ConditionGrade | None = None notes: str | None = None extra: dict[str, Any] | None = None class ObservationListResponse(BaseModel): items: list[Observation] total: int limit: int offset: int class HealthResponse(BaseModel): status: str version: str timestamp: datetime # ────────────────────────────────────────────────────────────── # Endpoints # ────────────────────────────────────────────────────────────── @router.post( "", response_model=Observation, status_code=status.HTTP_201_CREATED, summary="Skapa en ny observation", ) async def create_observation( payload: ObservationCreate, store: ObservationStore = Depends(get_observation_store), ) -> Observation: existing = await store.get(payload.id) if existing is not None: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Observation {payload.id!r} already exists", ) observation = Observation(**payload.model_dump(exclude_none=True)) return await store.create(observation) @router.get( "/{observation_id}", response_model=Observation, summary="Hämta en observation med ID", ) async def get_observation( observation_id: str, store: ObservationStore = Depends(get_observation_store), ) -> Observation: observation = await store.get(observation_id) if observation is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Observation {observation_id!r} not found", ) return observation @router.patch( "/{observation_id}", response_model=Observation, summary="Uppdatera en observation", ) async def update_observation( observation_id: str, payload: ObservationUpdate, store: ObservationStore = Depends(get_observation_store), ) -> Observation: existing = await store.get(observation_id) if existing is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Observation {observation_id!r} not found", ) update_data = payload.model_dump(exclude_unset=True, exclude_none=True) for key, value in update_data.items(): setattr(existing, key, value) existing.updated_at = datetime.utcnow() return await store.update(existing) @router.delete( "/{observation_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Ta bort en observation", ) async def delete_observation( observation_id: str, store: ObservationStore = Depends(get_observation_store), ) -> None: deleted = await store.delete(observation_id) if not deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Observation {observation_id!r} not found", ) @router.get( "", response_model=ObservationListResponse, summary="Lista observationer med filtrering", ) async def list_observations( object_goid: str | None = Query(None, description="Filtrera på GOID"), observer: str | None = Query(None, description="Filtrera på observer"), finding_code: str | None = Query(None, description="Filtrera på felkod"), start: datetime | None = Query(None, description="Startdatum (ISO 8601)"), end: datetime | None = Query(None, description="Slutdatum (ISO 8601)"), limit: int = Query(100, ge=1, le=1000), offset: int = Query(0, ge=0), store: ObservationStore = Depends(get_observation_store), ) -> ObservationListResponse: items: list[Observation] if object_goid: items = await store.list_by_object(object_goid, limit=limit, offset=offset) elif observer: items = await store.list_by_observer(observer, limit=limit, offset=offset) elif finding_code: items = await store.list_by_finding_code( finding_code, limit=limit, offset=offset ) elif start and end: items = await store.list_by_date_range(start, end, limit=limit, offset=offset) else: # Fallback: hämta alla (sorterat efter senaste) now = datetime.utcnow() items = await store.list_by_date_range( datetime.min.replace(tzinfo=None), now, limit=limit, offset=offset, ) return ObservationListResponse( items=items, total=len(items), limit=limit, offset=offset, ) # ────────────────────────────────────────────────────────────── # Hälsocheck # ────────────────────────────────────────────────────────────── @app.get("/health", response_model=HealthResponse, tags=["health"]) async def health_check() -> HealthResponse: return HealthResponse( status="ok", version="1.0.0", timestamp=datetime.utcnow(), ) # ────────────────────────────────────────────────────────────── # Registrera router # ────────────────────────────────────────────────────────────── app.include_router(router) # ────────────────────────────────────────────────────────────── # Entrypoint (uvicorn) # ────────────────────────────────────────────────────────────── if __name__ == "__main__": import uvicorn uvicorn.run("observation_api:app", host="0.0.0.0", port=8000, reload=True)