← Files Life Sciences DatabasesARCHIVED FILE
scripts/database_variant_resolution.py
13.8 KB · Sep 30, 2026 · 23:00 UTC
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any
import requests
ENSEMBL_GRCH38 = "https://rest.ensembl.org"
ENSEMBL_GRCH37 = "https://grch37.rest.ensembl.org"
DEFAULT_TIMEOUT_S = 15
USER_AGENT = "variant-coordinate-finder/1.0 (+requests)"
SEP_RE = re.compile(r"[-:_/\s]+")
CHR_RE = re.compile(r"^(?:chr)?([0-9]{1,2}|X|Y|M|MT)$", re.IGNORECASE)
ALLELE_RE = re.compile(r"^[A-Za-z*]+$")
def _safe_lookup_error(operation: str, exc: Exception) -> str:
response = getattr(exc, "response", None)
status_code = getattr(response, "status_code", None)
if isinstance(status_code, int):
return f"{operation} failed with HTTP {status_code}."
return f"{operation} failed ({type(exc).__name__})."
class VariantResolutionError(Exception):
def __init__(self, code: str, message: str, warnings: list[str] | None = None):
super().__init__(message)
self.code = code
self.message = message
self.warnings = warnings or []
@dataclass
class Coord:
chr: str
pos: int
ref: str | None
alts: list[str]
def build_key_for(build: str) -> str:
return "grch37" if build in {"GRCh37", "hg19"} else "grch38"
def build_variant_record(
chrom: str,
pos: int,
ref: str | None,
alt: str | None,
) -> dict[str, Any]:
record: dict[str, Any] = {
"chr": chrom,
"pos": pos,
"ref": ref,
"alt": alt,
}
if ref is not None and alt is not None:
record["canonical"] = f"{chrom}:{pos}-{ref}-{alt}"
return record
def parse_variant_string(value: str) -> tuple[str, int, str, str]:
raw = value.strip()
if not raw:
raise ValueError("Variant string is empty.")
parts = [part for part in SEP_RE.split(raw) if part]
if len(parts) != 4:
raise ValueError(
"Invalid variant format. Expected chrom-pos-ref-alt with flexible separators."
)
chrom_raw, pos_raw, ref_raw, alt_raw = parts
match = CHR_RE.match(chrom_raw)
if not match:
raise ValueError(f"Invalid chromosome: {chrom_raw!r}")
chrom = match.group(1).upper()
if chrom == "M":
chrom = "MT"
try:
pos = int(pos_raw)
except ValueError as exc:
raise ValueError(f"Invalid position: {pos_raw!r}") from exc
if pos <= 0:
raise ValueError("Position must be > 0.")
ref = ref_raw.upper()
alt = alt_raw.upper()
if not ALLELE_RE.match(ref):
raise ValueError(f"Invalid REF allele: {ref_raw!r}")
if not ALLELE_RE.match(alt):
raise ValueError(f"Invalid ALT allele: {alt_raw!r}")
return chrom, pos, ref, alt
def extract_variant_input(payload: Any, *, default_build_key: str) -> tuple[str, str]:
if isinstance(payload, str):
return default_build_key, payload.strip()
if not isinstance(payload, dict):
raise ValueError("Input must be a JSON string or object.")
provided: list[tuple[str, str]] = []
for key in ("rsid", "grch37", "grch38", "variant"):
value = payload.get(key)
if isinstance(value, str) and value.strip():
provided.append((key, value.strip()))
if not provided:
raise ValueError(
f"Provide exactly one of `rsid`, `grch37`, `grch38`, or `{default_build_key}`."
)
if len(provided) != 1:
raise ValueError(
"Provide exactly one variant input: `rsid`, `grch37`, `grch38`, or `variant`."
)
input_type, input_value = provided[0]
if input_type == "variant":
input_type = default_build_key
return input_type, input_value
def _server_for(build: str) -> str:
return ENSEMBL_GRCH37 if build in {"GRCh37", "hg19"} else ENSEMBL_GRCH38
def _assembly_cmp(build: str) -> str:
return "GRCh37" if build in {"GRCh37", "hg19"} else "GRCh38"
def _get_json(url: str, *, timeout: int = DEFAULT_TIMEOUT_S) -> Any:
headers = {
"Accept": "application/json",
"User-Agent": USER_AGENT,
}
response = requests.get(url, headers=headers, timeout=timeout)
response.raise_for_status()
return response.json()
def lookup_rsid(rsid: str, build: str = "GRCh38") -> Coord | None:
server = _server_for(build)
asm = _assembly_cmp(build)
url = (
f"{server}/variation/human/{requests.utils.quote(rsid, safe='')}"
"?content-type=application/json"
)
data = _get_json(url, timeout=DEFAULT_TIMEOUT_S)
mappings = data.get("mappings") if isinstance(data, dict) else None
if not mappings:
return None
for mapping in mappings:
if (
isinstance(mapping, dict)
and mapping.get("assembly_name") == asm
and mapping.get("seq_region_name")
and mapping.get("start") is not None
):
allele_string = mapping.get("allele_string")
alleles = allele_string.split("/") if isinstance(allele_string, str) else []
ref = alleles[0] if alleles else None
alts = alleles[1:] if len(alleles) > 1 else []
return Coord(
chr=str(mapping["seq_region_name"]),
pos=int(mapping["start"]),
ref=ref,
alts=alts,
)
return None
def lookup_position(
chrom: str,
pos: int,
build: str = "GRCh38",
ref: str | None = None,
alt: str | None = None,
) -> tuple[str, str | None, list[str]] | None:
server = _server_for(build)
url = (
f"{server}/overlap/region/human/{chrom}:{pos}-{pos}"
"?feature=variation;content-type=application/json"
)
data = _get_json(url, timeout=DEFAULT_TIMEOUT_S)
if not isinstance(data, list) or not data:
return None
expected_ref = ref.upper() if ref else None
expected_alt = alt.upper() if alt else None
for variant in data:
if (
isinstance(variant, dict)
and isinstance(variant.get("id"), str)
and variant["id"].startswith("rs")
):
raw_alleles = variant.get("alleles") if isinstance(variant.get("alleles"), list) else []
alleles = [str(allele).upper() for allele in raw_alleles]
resolved_ref = alleles[0] if alleles else None
alts = alleles[1:] if len(alleles) > 1 else []
if expected_ref is not None and resolved_ref != expected_ref:
continue
if expected_alt is not None and expected_alt not in alts:
continue
return variant["id"], resolved_ref, alts
return None
def resolve_rsid_both_builds(rsid: str) -> dict[str, Any]:
g38 = None
g37 = None
warnings: list[str] = []
try:
g38 = lookup_rsid(rsid, "GRCh38")
except Exception as exc: # noqa: BLE001
warnings.append(_safe_lookup_error("GRCh38 lookup", exc))
try:
g37 = lookup_rsid(rsid, "GRCh37")
except Exception as exc: # noqa: BLE001
warnings.append(_safe_lookup_error("GRCh37 lookup", exc))
ref = (g38.ref if g38 else None) or (g37.ref if g37 else None)
alts = (g38.alts if (g38 and g38.alts) else []) or (g37.alts if g37 else [])
return {
"rsid": rsid,
"grch38": {"chr": g38.chr if g38 else None, "pos": g38.pos if g38 else None},
"grch37": {"chr": g37.chr if g37 else None, "pos": g37.pos if g37 else None},
"ref": ref,
"alts": alts,
"warnings": warnings,
}
def resolve_position_both_builds(
chrom: str,
pos: int,
build: str,
ref: str | None = None,
alt: str | None = None,
) -> dict[str, Any] | None:
is_hg19 = build in {"hg19", "GRCh37"}
other_build = "GRCh38" if is_hg19 else "GRCh37"
pos_result = lookup_position(chrom, pos, build, ref, alt)
if not pos_result:
return None
rsid, ref, alts = pos_result
other = None
warnings: list[str] = []
try:
other = lookup_rsid(rsid, other_build)
except Exception as exc: # noqa: BLE001
warnings.append(_safe_lookup_error("Other-build lookup", exc))
if is_hg19:
return {
"rsid": rsid,
"grch38": {
"chr": other.chr if other else None,
"pos": other.pos if other else None,
},
"grch37": {"chr": chrom, "pos": pos},
"ref": (other.ref if other and other.ref else ref),
"alts": (other.alts if other and other.alts else alts),
"warnings": warnings,
}
return {
"rsid": rsid,
"grch38": {"chr": chrom, "pos": pos},
"grch37": {
"chr": other.chr if other else None,
"pos": other.pos if other else None,
},
"ref": ref,
"alts": alts,
"warnings": warnings,
}
def resolve_variant(input_type: str, input_value: str) -> dict[str, Any]:
warnings: list[str] = []
if input_type == "rsid":
rsid = input_value.strip()
if not rsid.startswith("rs"):
raise ValueError("rsid must start with 'rs'.")
resolved = resolve_rsid_both_builds(rsid)
warnings.extend(resolved.get("warnings", []))
ref = resolved.get("ref")
alts = resolved.get("alts") or []
alt = alts[0] if alts else None
g37 = None
if resolved.get("grch37", {}).get("chr") and resolved.get("grch37", {}).get("pos"):
g37 = build_variant_record(
resolved["grch37"]["chr"],
resolved["grch37"]["pos"],
ref,
alt,
)
g38 = None
if resolved.get("grch38", {}).get("chr") and resolved.get("grch38", {}).get("pos"):
g38 = build_variant_record(
resolved["grch38"]["chr"],
resolved["grch38"]["pos"],
ref,
alt,
)
return {
"input": {"type": "rsid", "value": rsid},
"rsid": rsid,
"grch37": g37,
"grch38": g38,
"warnings": warnings,
}
if input_type not in {"grch37", "grch38"}:
raise ValueError(f"Unsupported input type: {input_type!r}")
build = "GRCh37" if input_type == "grch37" else "GRCh38"
chrom, pos, ref_in, alt_in = parse_variant_string(input_value)
resolved = resolve_position_both_builds(chrom, pos, build, ref_in, alt_in)
if not resolved:
raise VariantResolutionError(
"not_found",
f"No rsID matching {ref_in}>{alt_in} was found at "
f"{chrom}:{pos} on {build} via Ensembl overlap endpoint.",
)
warnings.extend(resolved.get("warnings", []))
ref = resolved.get("ref")
alts = resolved.get("alts") or []
if ref and ref_in != ref:
warnings.append(f"Input ref {ref_in} != resolved ref {ref}; keeping resolved ref.")
alt = alt_in if alt_in in alts else (alts[0] if alts else alt_in)
if alts and alt_in not in alts:
warnings.append(f"Input alt {alt_in} not among resolved alts {alts}; using {alt}.")
rsid = resolved.get("rsid")
g37 = None
if resolved.get("grch37", {}).get("chr") and resolved.get("grch37", {}).get("pos"):
g37 = build_variant_record(
resolved["grch37"]["chr"],
resolved["grch37"]["pos"],
ref,
alt,
)
g38 = None
if resolved.get("grch38", {}).get("chr") and resolved.get("grch38", {}).get("pos"):
g38 = build_variant_record(
resolved["grch38"]["chr"],
resolved["grch38"]["pos"],
ref,
alt,
)
return {
"input": {"type": input_type, "value": input_value},
"rsid": rsid,
"grch37": g37,
"grch38": g38,
"warnings": warnings,
}
def resolve_query_variant(
*,
input_type: str,
input_value: str,
target_build: str,
) -> dict[str, Any]:
target_key = build_key_for(target_build)
if input_type == target_key:
chrom, pos, ref, alt = parse_variant_string(input_value)
target_variant = build_variant_record(chrom, pos, ref, alt)
return {
"input": {"type": input_type, "value": input_value},
"query_variant": target_variant,
"rsid": None,
"grch37": target_variant if target_key == "grch37" else None,
"grch38": target_variant if target_key == "grch38" else None,
"warnings": [],
}
if input_type == "rsid":
rsid = input_value.strip()
if not rsid.startswith("rs"):
raise ValueError("rsid must start with 'rs'.")
try:
target_coord = lookup_rsid(rsid, target_build)
except Exception: # noqa: BLE001 - preserve existing dual-build fallback behavior
target_coord = None
if target_coord is not None and target_coord.ref and target_coord.alts:
target_variant = build_variant_record(
target_coord.chr,
target_coord.pos,
target_coord.ref,
target_coord.alts[0],
)
return {
"input": {"type": "rsid", "value": rsid},
"query_variant": target_variant,
"rsid": rsid,
"grch37": target_variant if target_key == "grch37" else None,
"grch38": target_variant if target_key == "grch38" else None,
"warnings": [],
}
resolved = resolve_variant(input_type, input_value)
target_variant = resolved.get(target_key)
if not isinstance(target_variant, dict) or not target_variant.get("canonical"):
raise VariantResolutionError(
"resolution_failed",
f"Could not resolve input variant to {target_build}.",
warnings=list(resolved.get("warnings") or []),
)
return {
"input": resolved["input"],
"query_variant": target_variant,
"rsid": resolved.get("rsid"),
"grch37": resolved.get("grch37"),
"grch38": resolved.get("grch38"),
"warnings": list(resolved.get("warnings") or []),
}
SHA-256: 8a5dc20e9349223b47e6c05172ead8a07a075081f6f8c338f7c36ff2322b3d95