Files
boc/iom/observation/observation_api.py
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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)