Source code for uniqc.gateway.server

"""FastAPI application for the uniqc gateway management UI."""

from __future__ import annotations

from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as package_version
from pathlib import Path

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException

from uniqc.gateway import api
from uniqc.gateway.ws import broadcaster

GITHUB_URL = "https://github.com/IAI-USTC-Quantum/UnifiedQuantum"
DOCS_URL = "https://iai-ustc-quantum.github.io/UnifiedQuantum/docs/"


def _uniqc_version() -> str:
    """Return the installed uniqc package version used by this process."""
    try:
        return package_version("unified-quantum")
    except PackageNotFoundError:
        try:
            from uniqc import __version__
        except Exception:
            return "0.0.0+unknown"
        return __version__


# ---------------------------------------------------------------------------
# App factory (importable from uvicorn)
# ---------------------------------------------------------------------------


[docs] def create_app() -> FastAPI: app = FastAPI( title="uniqc Gateway", description="Management UI for uniqc quantum backends and tasks", version=_uniqc_version(), ) # CORS — allow local dev app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # REST API app.include_router(api.router, prefix="/api") # Health check @app.get("/api/health") def health(): return {"status": "ok"} @app.get("/api/version") def version(): return { "version": _uniqc_version(), "github_url": GITHUB_URL, "docs_url": DOCS_URL, } # WebSocket endpoint @app.websocket("/ws/events") async def ws_events(websocket: WebSocket): await broadcaster.connect(websocket) try: while True: # Keep-alive: client can send any text; we just don't disconnect await websocket.receive_text() except WebSocketDisconnect: broadcaster.disconnect(websocket) # Static file serving (SPA fallback) _mount_frontend(app) return app
# --------------------------------------------------------------------------- # Frontend static mount # --------------------------------------------------------------------------- def _frontend_dist_dir() -> Path: """Return the built frontend distribution directory.""" return Path(__file__).resolve().parents[2] / "frontend" / "dist" class _SPAStaticFiles(StaticFiles): """Serve Vite static files with React Router history fallback.""" async def get_response(self, path: str, scope): try: return await super().get_response(path, scope) except StarletteHTTPException as exc: if exc.status_code != 404 or _looks_like_static_asset(path): raise return await super().get_response("index.html", scope) def _looks_like_static_asset(path: str) -> bool: """Return True for paths that should keep normal 404 semantics.""" normalized = path.strip("/") if not normalized: return False if normalized.startswith("assets/"): return True return Path(normalized).suffix != "" def _mount_frontend(app: FastAPI) -> None: """Mount the built React SPA from frontend/dist/ if it exists.""" frontend_dist = _frontend_dist_dir() if not frontend_dist.exists(): return # Dev mode: no built frontend yet app.mount("/", _SPAStaticFiles(directory=str(frontend_dist), html=True), name="static") # --------------------------------------------------------------------------- # Entry point (run directly with: python -m uniqc.gateway.server) # --------------------------------------------------------------------------- if __name__ == "__main__": import uvicorn from uniqc.gateway.config import load_gateway_config cfg = load_gateway_config() uvicorn.run( "uniqc.gateway.server:create_app", factory=True, host=cfg["host"], port=cfg["port"], reload=False, )