Files
boc/iom/risk/risk_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

218 lines
7.0 KiB
Python

"""
IOM Lager 8 — Riskmodell API
FastAPI endpoints för riskberäkning.
"""
from __future__ import annotations
from typing import List, Optional, Dict, Any
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from risk_model import (
RiskScores,
ObjectContext,
RiskResult,
RiskDimension,
calculate_risk,
batch_calculate_risk,
register_custom_weights,
OBJECT_TYPE_WEIGHTS,
DEFAULT_WEIGHTS,
RISK_DIMENSIONS,
)
app = FastAPI(
title="IOM Risk Model API",
description="Infrastructure Object Model — Lager 8: Riskberäkningsmotor",
version="1.0.0",
)
# ─── Pydantic-scheman ────────────────────────────────────────────────────────
class RiskScoresInput(BaseModel):
safety: float = Field(0.0, ge=0.0, le=10.0)
economic: float = Field(0.0, ge=0.0, le=10.0)
environmental: float = Field(0.0, ge=0.0, le=10.0)
operational: float = Field(0.0, ge=0.0, le=10.0)
legal: float = Field(0.0, ge=0.0, le=10.0)
aesthetic: float = Field(0.0, ge=0.0, le=10.0)
def to_domain(self) -> RiskScores:
return RiskScores(
safety=self.safety,
economic=self.economic,
environmental=self.environmental,
operational=self.operational,
legal=self.legal,
aesthetic=self.aesthetic,
)
class ObjectContextInput(BaseModel):
goid: str = Field(..., pattern=r"^[A-Z]{3}(-[A-Z]{3})+-[0-9]+$")
object_type_code: str
domain: str = Field(..., pattern=r"^[A-Z]{3}$")
condition: int = Field(3, ge=1, le=5)
age_years: int = Field(0, ge=0)
design_lifespan: int = Field(50, ge=1)
function_status: str = "operational"
risk_weights: Optional[Dict[str, float]] = None
def to_domain(self) -> ObjectContext:
return ObjectContext(
goid=self.goid,
object_type_code=self.object_type_code,
domain=self.domain,
condition=self.condition,
age_years=self.age_years,
design_lifespan=self.design_lifespan,
function_status=self.function_status,
risk_weights=self.risk_weights,
)
class RiskCalculationRequest(BaseModel):
observation_scores: RiskScoresInput
object_context: ObjectContextInput
fault_codes: Optional[List[str]] = Field(default=None, description="Felkoder från Lager 7")
class BatchRiskCalculationRequest(BaseModel):
items: List[RiskCalculationRequest]
class RiskResultOutput(BaseModel):
goid: str
total_risk: float
risk_level: int
dimension_scores: Dict[str, float]
weighted_scores: Dict[str, float]
weights_used: Dict[str, float]
condition: int
age_years: int
design_lifespan: int
function_status: str
degradation_factor: float
alert_triggered: bool
alert_level: Optional[str]
@classmethod
def from_domain(cls, result: RiskResult) -> "RiskResultOutput":
return cls(
goid=result.goid,
total_risk=result.total_risk,
risk_level=result.risk_level,
dimension_scores=result.dimension_scores,
weighted_scores=result.weighted_scores,
weights_used=result.weights_used,
condition=result.condition,
age_years=result.age_years,
design_lifespan=result.design_lifespan,
function_status=result.function_status,
degradation_factor=result.degradation_factor,
alert_triggered=result.alert_triggered,
alert_level=result.alert_level,
)
class WeightsRegistrationRequest(BaseModel):
object_type_code: str
weights: Dict[str, float]
# ─── Endpoints ───────────────────────────────────────────────────────────────
@app.get("/")
async def root() -> Dict[str, str]:
return {"service": "IOM Risk Model API", "version": "1.0.0"}
@app.get("/dimensions")
async def list_dimensions() -> Dict[str, Any]:
"""Lista alla riskdimensioner."""
return {
"dimensions": [
{"id": d.value, "name_sv": _dim_name_sv(d)}
for d in RISK_DIMENSIONS
]
}
@app.get("/weights")
async def list_weights() -> Dict[str, Any]:
"""Lista alla registrerade viktmatriser."""
return {
"default": DEFAULT_WEIGHTS,
"object_types": OBJECT_TYPE_WEIGHTS,
}
@app.post("/calculate", response_model=RiskResultOutput)
async def calculate_risk_endpoint(request: RiskCalculationRequest) -> RiskResultOutput:
"""Beräkna risk för ett enskilt objekt."""
try:
result = calculate_risk(
observation_scores=request.observation_scores.to_domain(),
object_context=request.object_context.to_domain(),
fault_codes=request.fault_codes,
)
return RiskResultOutput.from_domain(result)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))
@app.post("/calculate/batch", response_model=List[RiskResultOutput])
async def batch_calculate_risk_endpoint(
request: BatchRiskCalculationRequest,
) -> List[RiskResultOutput]:
"""Batch-beräkning för flera objekt."""
try:
domain_items = [
(
item.observation_scores.to_domain(),
item.object_context.to_domain(),
item.fault_codes,
)
for item in request.items
]
results = batch_calculate_risk(domain_items)
return [RiskResultOutput.from_domain(r) for r in results]
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))
@app.post("/weights/register")
async def register_weights_endpoint(request: WeightsRegistrationRequest) -> Dict[str, str]:
"""Registrera anpassade vikter för en objekttyp."""
register_custom_weights(request.object_type_code, request.weights)
return {"status": "registered", "object_type_code": request.object_type_code}
@app.get("/health")
async def health_check() -> Dict[str, str]:
return {"status": "healthy"}
# ─── Hjälpfunktioner ─────────────────────────────────────────────────────────
def _dim_name_sv(dim: RiskDimension) -> str:
names = {
RiskDimension.SAFETY: "Säkerhet",
RiskDimension.ECONOMIC: "Ekonomi",
RiskDimension.ENVIRONMENTAL: "Miljö",
RiskDimension.OPERATIONAL: "Drift",
RiskDimension.LEGAL: "Juridik",
RiskDimension.AESTHETIC: "Estetik",
}
return names.get(dim, dim.value)
# ─── Körbar direkt ───────────────────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8008)