bio-nexus-api / app /routers /alignment.py
Samad14's picture
fix(blast): poll cap 65min, DNA validation, program/db/max_hits params
cd0c7a9 verified
Raw History Blame Contribute Delete
3.31 kB
import asyncio
import logging
import httpx
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from app.config import settings
logger = logging.getLogger(__name__)
router = APIRouter()
EBI_BASE = "https://www.ebi.ac.uk/Tools/services/rest/clustalo"
POLL_INTERVAL = 2
MAX_POLLS = 120
# Valid result type names for Clustal Omega (confirmed via live API testing)
# `fa` = FASTA alignment, `out` = stdout log, `phylotree` = Newick tree
TREE_TYPES = ["phylotree"]
class AlignRequest(BaseModel):
sequence: str = Field(..., min_length=1, description="Two or more sequences in FASTA format")
stype: str = Field("protein", description="Sequence type: protein or dna")
async def _fetch_result(client: httpx.AsyncClient, job_id: str, type_name: str) -> str | None:
for attempt in range(3):
try:
resp = await client.get(
f"{EBI_BASE}/result/{job_id}/{type_name}",
headers={"Accept": "text/plain"},
)
if resp.status_code == 200:
return resp.text
except Exception:
pass
if attempt < 2:
await asyncio.sleep(1)
return None
@router.post("/run")
async def run_alignment(req: AlignRequest):
email = settings.NCBI_EMAIL or "bioflow@example.com"
async with httpx.AsyncClient(timeout=30) as client:
submit_resp = await client.post(
f"{EBI_BASE}/run",
data={"email": email, "stype": req.stype, "sequence": req.sequence},
headers={"Accept": "text/plain"},
)
if submit_resp.status_code != 200:
detail = submit_resp.text[:200] if submit_resp.text else "EBI alignment submission failed"
raise HTTPException(status_code=502, detail=f"EBI submission failed: {detail}")
job_id = submit_resp.text.strip()
logger.info(f"EBI alignment job submitted: {job_id}")
for _ in range(MAX_POLLS):
await asyncio.sleep(POLL_INTERVAL)
try:
status_resp = await client.get(f"{EBI_BASE}/status/{job_id}")
status = status_resp.text.strip()
except Exception as e:
logger.warning(f"EBI status poll failed: {e}")
continue
logger.info(f"EBI alignment status ({job_id}): {status}")
if status == "FINISHED":
break
if status == "ERROR":
raise HTTPException(status_code=502, detail="EBI alignment job failed")
else:
raise HTTPException(status_code=504, detail="EBI alignment timed out")
await asyncio.sleep(1)
# Fetch FASTA alignment (result type `fa` — NOT `aln-fasta`)
fasta_text = await _fetch_result(client, job_id, "fa")
if fasta_text is None:
raise HTTPException(status_code=502, detail="Failed to fetch alignment result from EBI")
# Try phylogenetic tree (best-effort)
tree_text = None
for t in TREE_TYPES:
tree_text = await _fetch_result(client, job_id, t)
if tree_text:
break
return {
"job_id": job_id,
"aln_fasta": fasta_text,
"aln_clustal": "",
"phylotree": tree_text or "",
"stype": req.stype,
}