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"},
    )