Spaces:
Running
Running
File size: 5,078 Bytes
f0b48d6 cc7ed3f f0b48d6 cc7ed3f f0992bc f0b48d6 cc7ed3f f0b48d6 f0992bc 8321d59 f0992bc 8321d59 f0992bc 8321d59 f0992bc 8321d59 f0992bc 8321d59 f0992bc 8321d59 f0992bc f0b48d6 cc7ed3f f0b48d6 96668fa cc7ed3f f0b48d6 cc7ed3f f0b48d6 cc7ed3f 7a98820 cc7ed3f f0b48d6 cc7ed3f f0b48d6 | 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 130 131 132 133 134 135 136 137 138 139 140 | import logging
from typing import Any
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from app.config import settings
from app.tools.ebi_msa import EBI_TOOLS, run_ebi_msa
from app.tools.pairwise_alignment import PairwiseAlignError, pairwise_align
from app.tools.sequence_fetch import fetch_sequence_by_accession
logger = logging.getLogger(__name__)
router = APIRouter()
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")
method: str = Field(
"clustalo",
description="MSA method: clustalo, muscle, kalign, mafft, or tcoffee",
)
class PairwiseAlignRequest(BaseModel):
hit_accession: str = Field("", description="Subject accession to fetch and align against (optional when subject_sequence is given)")
subject_sequence: str = Field("", description="Full subject sequence (overrides hit_accession)")
query_sequence: str = Field("", description="Full query sequence (overrides query_accession)")
query_accession: str = Field("", description="Query accession; used when query_sequence is empty")
mode: str = Field("global", description="global (Needleman-Wunsch, default) or local (Smith-Waterman)")
matrix: str = Field("blosum62", description="blosum62 (default) or pam250")
open_gap_score: float = Field(-10, description="Gap-open penalty")
extend_gap_score: float = Field(-1, description="Gap-extension penalty")
source: str = Field("auto", description="auto|ncbi|uniprot — where to fetch sequences")
def _strip_fasta_header(seq: str) -> str:
"""Return the sequence body of a raw or FASTA-formatted sequence (no header letters)."""
lines = (seq or "").strip().splitlines()
lines = [ln for ln in lines if not ln.strip().startswith(">")]
return "\n".join(lines)
class PairwiseAlignResponse(BaseModel):
mode: str
matrix: str
score: float
aligned_query: str
aligned_hit: str
alignment_length: int
identity: int
pct_identity: float
gaps_total: int
gap_positions: list[dict[str, Any]]
query_start: int
query_end: int
hit_start: int
hit_end: int
query_length: int
hit_length: int
hit_source: str
@router.post("/pairwise", response_model=PairwiseAlignResponse)
async def run_pairwise(req: PairwiseAlignRequest):
if req.mode not in ("global", "local"):
raise HTTPException(status_code=400, detail="mode must be 'global' or 'local'")
if req.matrix not in ("blosum62", "pam250"):
raise HTTPException(status_code=400, detail="matrix must be 'blosum62' or 'pam250'")
query_seq = _strip_fasta_header(req.query_sequence or "")
if not query_seq:
if not req.query_accession:
raise HTTPException(status_code=400, detail="Provide a query_sequence or query_accession")
query = await fetch_sequence_by_accession(req.query_accession, req.source)
if "error" in query:
raise HTTPException(status_code=400, detail=f"Query fetch failed: {query['error']}")
query_seq = query["sequence"]
hit_seq = _strip_fasta_header(req.subject_sequence or "")
hit_source = ""
if not hit_seq:
if not req.hit_accession:
raise HTTPException(
status_code=400,
detail="Provide a subject_sequence or hit_accession",
)
hit = await fetch_sequence_by_accession(req.hit_accession, req.source)
if "error" in hit:
raise HTTPException(
status_code=400,
detail=f"Could not fetch subject sequence for {req.hit_accession}: {hit['error']}",
)
hit_seq = hit["sequence"]
hit_source = hit.get("source", "")
try:
result = pairwise_align(
seq_a=query_seq,
seq_b=hit_seq,
mode=req.mode,
matrix=req.matrix,
open_gap_score=req.open_gap_score,
extend_gap_score=req.extend_gap_score,
)
except PairwiseAlignError as e:
raise HTTPException(status_code=400, detail=str(e))
result["hit_source"] = hit_source
return result
@router.post("/run")
async def run_alignment(req: AlignRequest):
if req.method not in EBI_TOOLS:
raise HTTPException(
status_code=400,
detail=f"method must be one of: {', '.join(sorted(EBI_TOOLS))}",
)
email = settings.NCBI_EMAIL or "bioflow@example.com"
try:
result = await run_ebi_msa(
base_url=EBI_TOOLS[req.method],
sequence=req.sequence,
stype=req.stype,
email=email,
)
except ValueError as e:
raise HTTPException(status_code=502, detail=str(e))
return {
"job_id": result["job_id"],
"aln_fasta": result["aln_fasta"],
"aln_clustal": "",
"phylotree": result["phylotree"],
"stype": req.stype,
"method": result["method"],
}
|