from __future__ import annotations from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from .models import ( GraphResponse, NeighborsRequest, NeighborsResponse, SparqlQueryRequest, StatsResponse, ) from .pipelines.layout_dag_radial import CycleError from .pipelines.selection_neighbors import fetch_neighbor_ids_for_selection from .pipelines.snapshot_service import GraphSnapshotService from .sparql_engine import SparqlEngine, create_sparql_engine from .settings import Settings settings = Settings() @asynccontextmanager async def lifespan(app: FastAPI): sparql: SparqlEngine = create_sparql_engine(settings) await sparql.startup() app.state.sparql = sparql app.state.snapshot_service = GraphSnapshotService(sparql=sparql, settings=settings) yield await sparql.shutdown() app = FastAPI(title="visualizador_instanciados backend", lifespan=lifespan) cors_origins = settings.cors_origin_list() app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) @app.get("/api/health") def health() -> dict[str, str]: return {"status": "ok"} @app.get("/api/stats", response_model=StatsResponse) async def stats() -> StatsResponse: # Stats reflect exactly what we send to the frontend (/api/graph), not global graph size. svc: GraphSnapshotService = app.state.snapshot_service try: snap = await svc.get(node_limit=50_000, edge_limit=100_000) except CycleError as e: raise HTTPException(status_code=422, detail=str(e)) from None meta = snap.meta return StatsResponse( backend=meta.backend if meta else app.state.sparql.name, ttl_path=meta.ttl_path if meta else None, sparql_endpoint=meta.sparql_endpoint if meta else None, parsed_triples=len(snap.edges), nodes=len(snap.nodes), edges=len(snap.edges), ) @app.post("/api/sparql") async def sparql_query(req: SparqlQueryRequest) -> dict: sparql: SparqlEngine = app.state.sparql data = await sparql.query_json(req.query) return data @app.post("/api/neighbors", response_model=NeighborsResponse) async def neighbors(req: NeighborsRequest) -> NeighborsResponse: svc: GraphSnapshotService = app.state.snapshot_service snap = await svc.get(node_limit=req.node_limit, edge_limit=req.edge_limit) sparql: SparqlEngine = app.state.sparql neighbor_ids = await fetch_neighbor_ids_for_selection( sparql, snapshot=snap, selected_ids=req.selected_ids, include_bnodes=settings.include_bnodes, ) return NeighborsResponse(selected_ids=req.selected_ids, neighbor_ids=neighbor_ids) @app.get("/api/graph", response_model=GraphResponse) async def graph( node_limit: int = Query(default=50_000, ge=1, le=200_000), edge_limit: int = Query(default=100_000, ge=1, le=500_000), ) -> GraphResponse: svc: GraphSnapshotService = app.state.snapshot_service try: return await svc.get(node_limit=node_limit, edge_limit=edge_limit) except CycleError as e: raise HTTPException(status_code=422, detail=str(e)) from None