bio-nexus-api / app /services /ncbi_service.py
Samad14's picture
fix(blast): poll cap 65min, DNA validation, program/db/max_hits params
cd0c7a9 verified
Raw
History Blame Contribute Delete
3.91 kB
from Bio import Entrez, SeqIO
from io import StringIO
from typing import Optional
from app.config import settings
from app.services.cache import ttl_cache
Entrez.email = "bioflow@example.com"
def _detect_db(accession: str) -> str:
accession = accession.strip().upper()
if accession.startswith(("NP_", "XP_", "YP_", "AP_", "WP_")):
return "protein"
if accession.startswith(("NM_", "XM_", "NR_", "XR_")):
return "nucleotide"
if accession.startswith("NG_"):
return "nucleotide"
if accession.startswith(("NC_", "NT_", "NW_")):
return "nucleotide"
if accession.startswith(("AC_", "AE_")):
return "nucleotide"
return "protein"
def _detect_sequence_type(seq: str) -> str:
clean = seq.upper().replace("-", "").replace(".", "")
if not clean:
return "unknown"
dna_chars = set("ACGTUN")
rna_chars = set("ACGUN")
protein_chars = set("ACDEFGHIKLMNPQRSTVWY")
seq_set = set(clean)
if seq_set.issubset(dna_chars):
if seq_set.intersection({"T", "U"}):
return "dna"
if seq_set.issubset(rna_chars):
return "rna"
if seq_set.issubset(protein_chars):
return "protein"
if seq_set.issubset(dna_chars.union({"N"})):
return "dna"
return "unknown"
class NCBIService:
@ttl_cache(ttl=86400, prefix="ncbi_seq")
async def fetch_by_accession(self, accession: str) -> dict:
accession = accession.strip().upper()
db = _detect_db(accession)
try:
handle = Entrez.efetch(db=db, id=accession, rettype="fasta", retmode="text")
fasta_text = handle.read()
handle.close()
if not fasta_text.strip():
return {"error": f"Accession '{accession}' not found in NCBI"}
record = SeqIO.read(StringIO(fasta_text), "fasta")
seq_str = str(record.seq)
seq_type = _detect_sequence_type(seq_str)
desc = record.description
header_parts = desc.split(" ", 1)
acc_from_header = header_parts[0]
description = header_parts[1] if len(header_parts) > 1 else ""
organism = ""
if "[" in desc and "]" in desc:
organism = desc.split("[")[-1].rstrip("]")
return {
"accession": acc_from_header,
"db_source": "ncbi",
"database": db,
"sequence_type": seq_type,
"sequence": seq_str,
"length": len(seq_str),
"organism": organism,
"description": description,
"from_cache": False,
}
except Exception as e:
return {"error": str(e)}
@ttl_cache(ttl=86400, prefix="ncbi_search")
async def search_by_name(self, term: str, db: str = "protein", max_results: int = 10) -> dict:
try:
handle = Entrez.esearch(db=db, term=term, retmax=max_results)
result = Entrez.read(handle)
handle.close()
ids = result.get("IdList", [])
if not ids:
return {"error": f"No results found for '{term}'", "results": []}
handle = Entrez.esummary(db=db, id=",".join(ids))
summaries = Entrez.read(handle)
handle.close()
results = []
for docsum in summaries:
if hasattr(docsum, "items"):
results.append({
"accession": str(docsum.get("AccessionVersion", "")),
"title": str(docsum.get("Title", "")),
"organism": str(docsum.get("Organism", "")),
"length": int(docsum.get("Length", 0) or 0),
})
return {"results": results, "count": len(results), "query": term}
except Exception as e:
return {"error": str(e)}