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
478 lines
14 KiB
Python
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)
|