""" 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)