Files
boc/iom/api/main.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

478 lines
14 KiB
Python

"""
IOM API - FastAPI application for Infrastructure Object Model
Combines all layers into a unified API
"""
from fastapi import FastAPI, HTTPException, Query, Depends, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.responses import JSONResponse
from typing import List, Optional, Dict
from datetime import datetime, date
import time
app = FastAPI(
title="IOM API",
description="Infrastructure Object Model API",
version="1.0.0",
dependencies=[Depends(HTTPBearer(auto_error=False))]
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# Import modules
import sys
sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/core')
sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/observation')
sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/defect')
sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/risk')
sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/ledger')
sys.path.insert(0, '/home/bernt/.openclaw/workspace/iom/visual_geolocation')
from goid_generator import GOIDGenerator, validate_goid, parse_goid
from observation_models import Observation, ObservationSummary
from defect_codes import DefectRegistry, DefectCategory
from risk_model import RiskCalculator, RiskScores
from iom_ledger_mapping import IOMLedgerMapper
from api.auth import auth_manager, UserRole, Permission
# Visual Geolocation
from visual_geolocation.pipeline import VisualGeolocationPipeline
from visual_geolocation.temporal_analysis import TemporalAnalyzer
# Initialize components
goid_gen = GOIDGenerator()
defect_reg = DefectRegistry()
risk_calc = RiskCalculator()
ledger_mapper = IOMLedgerMapper()
visual_geo = VisualGeolocationPipeline(use_real_ai=True)
temporal_analyzer = TemporalAnalyzer()
# Auth helper
def require_auth():
return Depends(auth_manager.get_current_user)
def require_permission(permission: Permission):
return Depends(auth_manager.require_permission(permission))
# === Auth Endpoints ===
class AuthRequest:
def __init__(self, username: str, password: str, role: str = "readonly"):
self.username = username
self.password = password
self.role = role
@app.post("/auth/register")
async def register_user(request: Dict):
"""Register a new user"""
username = request.get("username")
password = request.get("password")
role = request.get("role", "readonly")
if not username or not password:
raise HTTPException(status_code=400, detail="Username and password required")
try:
user_role = UserRole(role)
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid role: {role}")
# In production, hash password and store in database
token = auth_manager.create_token(username, user_role)
return {"token": token, "role": role}
@app.post("/auth/login")
async def login(request: Dict):
"""Login and get token"""
username = request.get("username")
password = request.get("password")
if not username or not password:
raise HTTPException(status_code=400, detail="Username and password required")
# In production, verify password against database
token = auth_manager.create_token(username, UserRole.READONLY)
return {"token": token}
# === GOID Endpoints ===
@app.get("/goid/validate/{goid}")
async def validate_goid_endpoint(goid: str, user: Dict = Depends(auth_manager.get_current_user)):
"""Validate a GOID"""
is_valid, error = validate_goid(goid)
return {"valid": is_valid, "error": error}
@app.get("/goid/parse/{goid}")
async def parse_goid_endpoint(goid: str, user: Dict = Depends(auth_manager.get_current_user)):
"""Parse a GOID into components"""
try:
parsed = parse_goid(goid)
return parsed
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/goid/generate")
async def generate_goid(
domain: str,
system: str,
subsystem: str,
obj_type: str,
location_hash: Optional[str] = None,
user: Dict = Depends(auth_manager.get_current_user)
):
"""Generate a new GOID"""
try:
goid = goid_gen.generate(domain, system, subsystem, obj_type, location_hash)
return {"goid": goid}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/taxonomy/domains")
async def list_domains(user: Dict = Depends(auth_manager.get_current_user)):
"""List all domains"""
return goid_gen.list_domains()
@app.get("/taxonomy/domains/{domain}/systems")
async def list_systems(domain: str, user: Dict = Depends(auth_manager.get_current_user)):
"""List systems in a domain"""
try:
return goid_gen.list_systems(domain)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.get("/taxonomy/domains/{domain}/systems/{system}/objects")
async def list_objects(domain: str, system: str, user: Dict = Depends(auth_manager.get_current_user)):
"""List objects in a system"""
try:
return goid_gen.list_objects(domain, system)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
# === Observation Endpoints ===
@app.post("/observations")
async def create_observation(observation: Observation, user: Dict = Depends(auth_manager.get_current_user)):
"""Create a new observation"""
# In production, this would save to database
return {
"id": observation.id or f"OBS-{datetime.now().year}-0000001",
"status": "created",
"object_goid": observation.object_goid
}
@app.get("/observations/{observation_id}")
async def get_observation(observation_id: str, user: Dict = Depends(auth_manager.get_current_user)):
"""Get observation by ID"""
# In production, this would fetch from database
raise HTTPException(status_code=404, detail="Observation not found")
@app.get("/objects/{goid}/observations")
async def get_object_observations(
goid: str,
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
user: Dict = Depends(auth_manager.get_current_user)
):
"""Get observations for an object"""
# In production, this would fetch from database
return {
"object_goid": goid,
"observations": [],
"total": 0
}
@app.get("/objects/{goid}/summary")
async def get_object_summary(goid: str, user: Dict = Depends(auth_manager.get_current_user)):
"""Get observation summary for an object"""
# In production, this would fetch from database
return {
"object_goid": goid,
"observation_count": 0,
"latest_condition": None
}
# === Defect Endpoints ===
@app.get("/defects")
async def list_defects(
category: Optional[str] = None,
lang: str = "sv",
user: Dict = Depends(auth_manager.get_current_user)
):
"""List all defect codes"""
if category:
try:
cat = DefectCategory(category)
codes = defect_reg.get_by_category(cat)
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid category: {category}")
else:
codes = list(defect_reg._codes.values())
return [code.to_dict(lang) for code in codes]
@app.get("/defects/{code}")
async def get_defect(code: str, lang: str = "sv", user: Dict = Depends(auth_manager.get_current_user)):
"""Get defect code by code"""
defect = defect_reg.get(code)
if not defect:
raise HTTPException(status_code=404, detail=f"Defect code not found: {code}")
return defect.to_dict(lang)
@app.get("/defects/search/{query}")
async def search_defects(query: str, lang: str = "sv", user: Dict = Depends(auth_manager.get_current_user)):
"""Search defect codes"""
results = defect_reg.search(query, lang)
return [code.to_dict(lang) for code in results]
@app.post("/defects/suggest")
async def suggest_defects(keywords: List[str], user: Dict = Depends(auth_manager.get_current_user)):
"""Suggest defect codes based on AI keywords"""
suggestions = defect_reg.get_ai_suggestions(keywords)
return [
{
"code": code.code,
"name": code.name_en,
"confidence": confidence
}
for code, confidence in suggestions
]
# === Risk Endpoints ===
@app.post("/risk/calculate")
async def calculate_risk(
scores: RiskScores,
object_type: str = "default",
user: Dict = Depends(auth_manager.get_current_user)
):
"""Calculate risk score"""
result = risk_calc.calculate(scores, object_type)
return result
@app.post("/risk/calculate-from-observation")
async def calculate_risk_from_observation(
condition: int,
defect_codes: List[str],
object_type: str = "default",
user: Dict = Depends(auth_manager.get_current_user)
):
"""Calculate risk from observation data"""
result = risk_calc.calculate_from_observation(condition, defect_codes, object_type)
return result
@app.get("/risk/weights/{object_type}")
async def get_risk_weights(object_type: str, user: Dict = Depends(auth_manager.get_current_user)):
"""Get risk weights for object type"""
weights = risk_calc.get_weights_for_type(object_type)
return {"object_type": object_type, "weights": weights}
# === Ledger Endpoints ===
@app.post("/ledger/installation")
async def map_installation(
goid: str,
object_type: str,
cost: float,
installation_date: date,
contractor: str = "",
project: str = "",
user: Dict = Depends(auth_manager.get_current_user)
):
"""Map installation to ledger transactions"""
transactions = ledger_mapper.map_installation(
goid, object_type, cost, installation_date, contractor, project
)
return {
"goid": goid,
"transactions": [t.to_dict() for t in transactions]
}
@app.post("/ledger/maintenance")
async def map_maintenance(
goid: str,
cost: float,
maintenance_date: date,
observation_id: str = "",
defect_code: str = "",
contractor: str = "",
description: str = "",
user: Dict = Depends(auth_manager.get_current_user)
):
"""Map maintenance to ledger transactions"""
transactions = ledger_mapper.map_maintenance(
goid, cost, maintenance_date, observation_id, defect_code, contractor, description
)
return {
"goid": goid,
"transactions": [t.to_dict() for t in transactions]
}
@app.post("/ledger/depreciation")
async def map_depreciation(
goid: str,
object_type: str,
acquisition_value: float,
useful_life: int,
depreciation_date: date,
user: Dict = Depends(auth_manager.get_current_user)
):
"""Map depreciation to ledger transactions"""
transactions = ledger_mapper.map_depreciation(
goid, object_type, acquisition_value, useful_life, depreciation_date
)
return {
"goid": goid,
"transactions": [t.to_dict() for t in transactions]
}
# === Visual Geolocation Endpoints ===
@app.post("/visual-geolocation/analyze")
async def analyze_image(request: Dict):
"""Analyze image and extract evidence package"""
image_path = request.get("image_path")
image_id = request.get("image_id")
if not image_path:
raise HTTPException(status_code=400, detail="image_path required")
try:
result = visual_geo.process_image(image_path, image_id)
return {
"image_id": image_id or "unknown",
"position": {
"lat": result.lat,
"lng": result.lng,
"accuracy": result.accuracy,
"confidence": result.confidence
},
"method": result.method,
"evidence_summary": result.evidence_summary,
"confidence_report": {
"overall": result.confidence_report.overall_confidence,
"uncertainty_radius": result.confidence_report.uncertainty_radius,
"supporting_evidence_count": len(result.confidence_report.supporting_evidence),
"contradicting_evidence_count": len(result.confidence_report.contradicting_evidence)
},
"map_matches": result.map_matches,
"similar_images": result.similar_images
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/visual-geolocation/batch")
async def analyze_batch(request: Dict):
"""Analyze multiple images"""
images = request.get("images", [])
if not images:
raise HTTPException(status_code=400, detail="images array required")
results = []
for img in images:
try:
result = visual_geo.process_image(img.get("path"), img.get("id"))
results.append({
"image_id": img.get("id"),
"position": {
"lat": result.lat,
"lng": result.lng,
"accuracy": result.accuracy
},
"confidence": result.confidence
})
except Exception as e:
results.append({
"image_id": img.get("id"),
"error": str(e)
})
return {"results": results}
@app.post("/visual-geolocation/temporal")
async def temporal_analysis(request: Dict):
"""Analyze temporal changes between observations"""
observations = request.get("observations", [])
if len(observations) < 2:
raise HTTPException(status_code=400, detail="At least 2 observations required")
# Add observations to analyzer
for obs in observations:
# In production, load from database
# For now, create from request
pass
# Compare consecutive observations
comparisons = []
for i in range(len(observations) - 1):
# In production, use actual evidence packages
comparisons.append({
"from": observations[i].get("id"),
"to": observations[i + 1].get("id"),
"status": "compared"
})
return {
"comparisons": len(comparisons),
"results": comparisons
}
# === Health Check ===
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"version": "1.0.0",
"components": {
"goid": "ok",
"taxonomy": "ok",
"defects": "ok",
"risk": "ok",
"ledger": "ok",
"auth": "ok",
"visual_geolocation": "ok"
}
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)