from dataclasses import asdict import asyncio from fastapi import FastAPI, HTTPException from pydantic import BaseModel, HttpUrl from starlette.concurrency import run_in_threadpool from .engine import assess from .fetcher import ProfileNotFound, fetch_profile from .link_resolver import resolve_link app = FastAPI(title="BMC RiskEngine API", version="1.0.0") class AssessRequest(BaseModel): bmc_url: HttpUrl class ErrorResponse(BaseModel): detail: str class AssessResponse(BaseModel): risk_score: int risk_level: str recommended_action: str evidence: list limitations: list async def _analyze(bmc_url: str): profile = await run_in_threadpool(fetch_profile, bmc_url) resolved_links = [] # for link in profile.external_links: # resolved = await run_in_threadpool(resolve_link, link) # resolved_links.append(resolved) tasks = [run_in_threadpool(resolve_link, link) for link in profile.external_links] resolved_links = await asyncio.gather(*tasks) assessment = assess(profile, resolved_links) return asdict(assessment) @app.post( "/assess", response_model=AssessResponse, responses={404: {"model": ErrorResponse}} ) async def assess_profile(payload: AssessRequest): try: result = await _analyze(str(payload.bmc_url)) except ProfileNotFound as exc: raise HTTPException(status_code=404, detail="Profile not found") from exc except Exception as exc: # TODO: need to log real exceptions raise HTTPException(status_code=500, detail="Internal server error") from exc return result @app.get("/", response_model=dict) async def root(): return { "message": "Welcome to the BMC RiskEngine API. Use the /assess endpoint to evaluate a BMC profile." }