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