| """MuSProt-only FastAPI application for Hugging Face Docker Spaces.""" |
| from __future__ import annotations |
|
|
| import logging |
| import os |
| from contextlib import asynccontextmanager |
| from pathlib import Path |
|
|
| from fastapi import FastAPI, HTTPException |
| from fastapi.responses import FileResponse, PlainTextResponse, RedirectResponse |
| from fastapi.staticfiles import StaticFiles |
|
|
| from app.protein.api.routes.protein import router as protein_router |
| from app.protein.api.routes.protein import set_data_manager |
| from app.protein.config import get_database_path, get_docs_path, get_plots_dir |
| from app.protein.data_loader import DataManager |
| from app.protein import tsv_loader |
|
|
|
|
| logging.basicConfig( |
| level=os.getenv("LOG_LEVEL", "INFO"), |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| BACKEND_DIR = Path(__file__).resolve().parent |
| FRONTEND_DIR = Path(os.getenv("MUSPROT_FRONTEND_DIR", BACKEND_DIR / "frontend")) |
|
|
|
|
| @asynccontextmanager |
| async def lifespan(_: FastAPI): |
| """Initialize the shared read-only database manager.""" |
| db_path = get_database_path() |
| logger.info("Using read-only MuSProt database: %s", db_path) |
| manager = DataManager(db_path) |
| manager.load_data() |
| set_data_manager(manager) |
|
|
| if os.getenv("MUSPROT_PRELOAD_NODE_INDEX", "0") == "1": |
| tsv_loader._get_index() |
|
|
| yield |
| set_data_manager(None) |
|
|
|
|
| app = FastAPI( |
| title="MuSProt", |
| description="Multistate protein structure dataset explorer", |
| version="1.0.0", |
| lifespan=lifespan, |
| docs_url="/api/docs", |
| redoc_url="/api/redoc", |
| openapi_url="/api/openapi.json", |
| ) |
| app.include_router(protein_router, prefix="/api/protein", tags=["MuSProt"]) |
|
|
|
|
| @app.get("/health") |
| async def health(): |
| return {"status": "healthy", "database": str(get_database_path())} |
|
|
|
|
| @app.get("/api/protein/docs", response_class=PlainTextResponse) |
| async def protein_docs(): |
| path = get_docs_path() |
| if not path: |
| raise HTTPException(status_code=404, detail="MuSProt documentation is not available") |
| return path.read_text(encoding="utf-8") |
|
|
|
|
| @app.get("/api/protein/download/db") |
| async def download_database(): |
| repo_id = os.getenv("MUSPROT_DATASET_REPO") |
| if repo_id: |
| revision = os.getenv("MUSPROT_DATASET_REVISION", "main") |
| filename = os.getenv("MUSPROT_DB_FILENAME", "MuSProt.db") |
| return RedirectResponse( |
| f"https://huggingface.co/datasets/{repo_id}/resolve/{revision}/{filename}?download=true" |
| ) |
|
|
| return FileResponse( |
| get_database_path(), |
| media_type="application/x-sqlite3", |
| filename="MuSProt.db", |
| ) |
|
|
|
|
| plots_dir = get_plots_dir() |
| if plots_dir: |
| app.mount("/api/protein/plots", StaticFiles(directory=plots_dir), name="protein-plots") |
|
|
| assets_dir = FRONTEND_DIR / "assets" |
| if assets_dir.is_dir(): |
| app.mount("/assets", StaticFiles(directory=assets_dir), name="frontend-assets") |
|
|
|
|
| @app.get("/{path:path}", include_in_schema=False) |
| async def frontend(path: str): |
| """Serve the React SPA and its pre-rendered routes.""" |
| requested = FRONTEND_DIR / path |
| if path and requested.is_file(): |
| return FileResponse(requested) |
|
|
| prerendered = requested / "index.html" |
| if path and prerendered.is_file(): |
| return FileResponse(prerendered) |
|
|
| index = FRONTEND_DIR / "index.html" |
| if not index.is_file(): |
| raise HTTPException(status_code=503, detail="Frontend build is not available") |
| return FileResponse(index) |
|
|