bio-nexus-api / app /routers /alignment.py
Samad14's picture
Add sequencing pipeline (FASTQ -> QC -> alignment -> variant calling -> report)
da793ed verified
Raw
History Blame Contribute Delete
3.4 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,
}