Samad14's picture
Add sequencing pipeline (FASTQ -> QC -> alignment -> variant calling -> report)
da793ed verified
Raw
History Blame Contribute Delete
4.58 kB
"""
Parse NCBI BLAST XML output into structured hit list.
Raw XML is always stored to R2 first; parsing happens from
the stored copy, never inline with the API request.
"""
import xml.etree.ElementTree as ET
from typing import List, Optional
def parse_blast_xml(raw_xml: str) -> dict:
try:
root = ET.fromstring(raw_xml)
except ET.ParseError as e:
return {"error": f"XML parse error: {e}", "hits": []}
ns = {"": "http://www.ncbi.nlm.nih.gov"}
query_len_el = root.find(".//BlastOutput_query-len")
query_len = int(query_len_el.text) if query_len_el is not None else 0
hits = []
for iteration in root.findall(".//Iteration"):
for hit_el in iteration.findall(".//Hit"):
hit = _parse_hit(hit_el)
if hit is not None:
hits.append(hit)
return {
"query_length": query_len,
"hits": hits,
"count": len(hits),
}
def _parse_hit(hit_el: ET.Element) -> Optional[dict]:
acc = _text(hit_el, "Hit_accession")
if not acc:
return None
hit_id = _text(hit_el, "Hit_id")
def_line = _text(hit_el, "Hit_def")
accession = acc
description = def_line or ""
if " " in def_line:
parts = def_line.split(" ", 1)
if parts[0] == acc or parts[0] == hit_id:
description = parts[1] if len(parts) > 1 else ""
organism = ""
if "[" in description and "]" in description:
organism = description.split("[")[-1].rstrip("]")
description = description.split("[")[0].strip()
hsps = hit_el.findall(".//Hsp")
top_hsp = _parse_hsp(hsps[0]) if hsps else None
return {
"accession": accession,
"id": hit_id,
"description": description,
"organism": organism,
"length": int(_text(hit_el, "Hit_len") or 0),
"score": top_hsp.get("score", 0) if top_hsp else 0,
"bit_score": top_hsp.get("bit_score", 0) if top_hsp else 0,
"evalue": top_hsp.get("evalue", 0) if top_hsp else 0,
"evalue_raw": top_hsp.get("evalue_raw", "0") if top_hsp else "0",
"identity": top_hsp.get("identity", 0) if top_hsp else 0,
"identity_pct": top_hsp.get("identity_pct", 0) if top_hsp else 0,
"positive": top_hsp.get("positive", 0) if top_hsp else 0,
"gaps": top_hsp.get("gaps", 0) if top_hsp else 0,
"alignment_length": top_hsp.get("alignment_length", 0) if top_hsp else 0,
"query_from": top_hsp.get("query_from", 0) if top_hsp else 0,
"query_to": top_hsp.get("query_to", 0) if top_hsp else 0,
"hit_from": top_hsp.get("hit_from", 0) if top_hsp else 0,
"hit_to": top_hsp.get("hit_to", 0) if top_hsp else 0,
"query_alignment": top_hsp.get("query_alignment", "") if top_hsp else "",
"hit_alignment": top_hsp.get("hit_alignment", "") if top_hsp else "",
"midline": top_hsp.get("midline", "") if top_hsp else "",
}
def _parse_hsp(hsp_el: ET.Element) -> dict:
score = int(_text(hsp_el, "Hsp_score") or 0)
bit_score = float(_text(hsp_el, "Hsp_bit-score") or 0)
evalue_raw = _text(hsp_el, "Hsp_evalue") or "0"
evalue = float(evalue_raw)
identity = int(_text(hsp_el, "Hsp_identity") or 0)
positive = int(_text(hsp_el, "Hsp_positive") or 0)
gaps = int(_text(hsp_el, "Hsp_gaps") or 0)
align_len = int(_text(hsp_el, "Hsp_align-len") or 0)
query_from = int(_text(hsp_el, "Hsp_query-from") or 0)
query_to = int(_text(hsp_el, "Hsp_query-to") or 0)
hit_from = int(_text(hsp_el, "Hsp_hit-from") or 0)
hit_to = int(_text(hsp_el, "Hsp_hit-to") or 0)
qseq = _text(hsp_el, "Hsp_qseq") or ""
hseq = _text(hsp_el, "Hsp_hseq") or ""
mid = _text(hsp_el, "Hsp_midline") or ""
identity_pct = round(identity / align_len * 100, 1) if align_len > 0 else 0
return {
"score": score,
"bit_score": bit_score,
"evalue": evalue,
"evalue_raw": evalue_raw,
"identity": identity,
"identity_pct": identity_pct,
"positive": positive,
"gaps": gaps,
"alignment_length": align_len,
"query_from": query_from,
"query_to": query_to,
"hit_from": hit_from,
"hit_to": hit_to,
"query_alignment": qseq,
"hit_alignment": hseq,
"midline": mid,
}
def _text(el: ET.Element, path: str) -> str:
found = el.find(path)
return found.text if found is not None and found.text else ""