Spaces:
Sleeping
Sleeping
File size: 4,940 Bytes
94ca7cd fb8c550 5a0e691 bac1801 5a0e691 f42f5a6 fb8c550 5a0e691 94ca7cd 5a0e691 2f67ecf 5a0e691 f42f5a6 76fb273 5a0e691 76fb273 bac1801 76fb273 5a0e691 990c867 5a0e691 76fb273 f8dfa58 f0b48d6 bac1801 07e58f6 305458b fb8c550 5a0e691 94ca7cd 9d6a43d 94ca7cd 5a0e691 fb8c550 5a0e691 94ca7cd 5a0e691 fb8c550 5a0e691 bac1801 5a0e691 e5d75ff 5a0e691 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | import logging
import os
import sentry_sdk
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, export, api_keys, cache_stats, docking
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)
app.include_router(export.router, prefix="/api/export", tags=["export"])
app.include_router(api_keys.router, prefix="/api/keys", tags=["api_keys"])
app.include_router(cache_stats.router)
app.include_router(docking.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():
sentry_sdk.init(
dsn=settings.SENTRY_DSN,
environment=os.getenv("ENVIRONMENT", "development"),
traces_sample_rate=0.1,
)
init_redis()
await _fail_stuck_jobs()
@app.get("/health")
async def health():
from app.services.cache import get_cache_stats
stats = get_cache_stats()
return {"status": "ok", "cache": stats}
@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"},
)
|