bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
252 lines
9.0 KiB
Python
252 lines
9.0 KiB
Python
"""
|
|
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)
|