bio-nexus-api / app /main.py
Samad14's picture
feat: add phylo router (NJ/UPGMA/ML) with PhyML binary
3caaead verified
Raw
History Blame Contribute Delete
4.4 kB
import logging
from dotenv import load_dotenv
load_dotenv()
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from app.config import settings
from app.routers import pipelines, pipeline_v2, ai, jobs, share, profile, sequences, uniprot, alignment, structures, pathways, domains, interactions, primers, structure_analysis, phylo
from app.services.cache import init_redis
logger = logging.getLogger(__name__)
limiter = Limiter(key_func=get_remote_address, default_limits=["30/minute"])
app = FastAPI(title="Bio Nexus API", version="0.2.0")
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
PROD_ORIGIN = settings.CORS_ORIGIN
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:3001",
PROD_ORIGIN,
"https://bioai-platform.vercel.app",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(pipelines.router, prefix="/api/pipelines", tags=["pipelines"])
app.include_router(pipeline_v2.router, prefix="/api/pipeline/v2", tags=["pipeline_v2"])
app.include_router(ai.router, prefix="/api/ai", tags=["ai"])
app.include_router(jobs.router, prefix="/api/jobs", tags=["jobs"])
app.include_router(share.router, prefix="/api/share", tags=["share"])
app.include_router(profile.router, prefix="/api/profile", tags=["profile"])
app.include_router(sequences.router, prefix="/api/sequences", tags=["sequences"])
app.include_router(uniprot.router, prefix="/api/uniprot", tags=["uniprot"])
app.include_router(alignment.router, prefix="/api/alignment", tags=["alignment"])
app.include_router(structures.router, prefix="/api/structures", tags=["structures"])
app.include_router(pathways.router, prefix="/api/pathways", tags=["pathways"])
app.include_router(domains.router)
app.include_router(interactions.router)
app.include_router(primers.router)
app.include_router(structure_analysis.router)
app.include_router(phylo.router)
TERMINAL_STATUSES = {"complete", "failed"}
NON_TERMINAL_STATUSES = {
"submitted_to_ncbi", "polling_ncbi", "parsing",
"fetching_uniprot", "fetching_alphafold", "interpreting",
}
async def _fail_stuck_jobs():
try:
import httpx
from app.config import settings
headers = {
"apikey": settings.SUPABASE_SERVICE_ROLE_KEY,
"Authorization": f"Bearer {settings.SUPABASE_SERVICE_ROLE_KEY}",
"Content-Type": "application/json",
"Prefer": "return=minimal",
}
url = f"{settings.SUPABASE_URL}/rest/v1/jobs"
quoted = ",".join(f'"{s}"' for s in NON_TERMINAL_STATUSES)
select_url = f"{url}?select=id&status=in.({quoted})"
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(select_url, headers=headers)
if resp.status_code != 200:
logger.warning(f"Startup resume: failed to query jobs ({resp.status_code})")
return
stuck = resp.json()
for job in stuck:
jid = job["id"]
logger.info(f"Startup resume: marking stuck job {jid} as failed")
await client.patch(
f"{url}?id=eq.{jid}",
headers=headers,
json={"status": "failed", "error": "Worker lost on restart — please re-run"},
)
if stuck:
logger.info(f"Startup resume: marked {len(stuck)} stuck job(s) as failed")
except Exception as e:
logger.warning(f"Startup resume: error: {e}")
@app.on_event("startup")
async def startup():
init_redis()
await _fail_stuck_jobs()
@app.get("/health")
async def health():
return {"status": "ok"}
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
if isinstance(exc, HTTPException):
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
logger.exception("Unhandled exception")
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"},
)