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
218 lines
7.0 KiB
Python
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)
|