From cd9450c5ce48044ed308d7ae52a10155c0d92a05 Mon Sep 17 00:00:00 2001 From: David Reinhart Date: Tue, 7 Jul 2026 11:17:34 -0700 Subject: [PATCH 1/2] feat(uniprot): support Ensembl protein accessions in UniProt ID mapping infer_db_name_from_sequence_accession now recognizes ENSP accessions and maps them to the Ensembl_Protein source, so accession-based target genes with Ensembl protein IDs resolve to a UniProt ID instead of raising NotImplementedError. Also make validate_ensembl_identifier accept versioned Ensembl stable IDs (e.g. ENSP00000369497.3) by stripping the trailing . before the idutils.is_ensembl check, matching how validate_refseq_identifier already handles versioned RefSeq accessions. Related to varianteffect/dcd_mapping2#110 --- src/mavedb/lib/uniprot/utils.py | 14 +++++++++++--- src/mavedb/lib/validation/identifier.py | 8 +++++++- tests/lib/uniprot/test_utils.py | 8 ++++++++ tests/validation/test_identifier.py | 4 ++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/mavedb/lib/uniprot/utils.py b/src/mavedb/lib/uniprot/utils.py index 7f87524b2..11c13a8f4 100644 --- a/src/mavedb/lib/uniprot/utils.py +++ b/src/mavedb/lib/uniprot/utils.py @@ -1,11 +1,11 @@ from typing import Literal, Union -from mavedb.lib.validation.identifier import validate_refseq_identifier +from mavedb.lib.validation.identifier import validate_ensembl_identifier, validate_refseq_identifier def infer_db_name_from_sequence_accession( sequence_accession: str, -) -> Union[Literal["RefSeq_Nucleotide", "RefSeq_Protein"]]: +) -> Union[Literal["RefSeq_Nucleotide", "RefSeq_Protein", "Ensembl_Protein"]]: """ Infers the database name from a sequence accession. @@ -15,6 +15,12 @@ def infer_db_name_from_sequence_accession( Returns: str: The inferred database name. """ + # Ensembl protein accessions must be handled before validate_refseq_identifier, + # which raises on any non-RefSeq accession. + if sequence_accession.startswith("ENSP"): + validate_ensembl_identifier(sequence_accession) + return "Ensembl_Protein" + validate_refseq_identifier(sequence_accession) if sequence_accession.startswith("NM_"): @@ -22,4 +28,6 @@ def infer_db_name_from_sequence_accession( if sequence_accession.startswith("NP_"): return "RefSeq_Protein" - raise NotImplementedError("Only RefSeq NM and NP identifiers are currently supported for inference.") + raise NotImplementedError( + "Only RefSeq NM/NP and Ensembl protein (ENSP) identifiers are currently supported for inference." + ) diff --git a/src/mavedb/lib/validation/identifier.py b/src/mavedb/lib/validation/identifier.py index e6e2dcde7..077b13fc0 100644 --- a/src/mavedb/lib/validation/identifier.py +++ b/src/mavedb/lib/validation/identifier.py @@ -1,3 +1,5 @@ +import re + import idutils from mavedb.lib.validation.constants.identifier import valid_dbnames @@ -52,6 +54,9 @@ def validate_ensembl_identifier(identifier: str): """ Validates whether the identifier is a valid Ensembl identifier. + Ensembl stable IDs may carry a version suffix (e.g. ``ENSP00000369497.3``), which + ``idutils.is_ensembl`` does not accept; strip it before validating the stable ID. + Parameters __________ identifier : str @@ -62,7 +67,8 @@ def validate_ensembl_identifier(identifier: str): ValidationError If the identifier is not a valid Ensembl identifier. """ - if not idutils.is_ensembl(identifier): + base_identifier = re.sub(r"\.\d+$", "", identifier) + if not idutils.is_ensembl(base_identifier): raise ValidationError(f"'{identifier}' is not a valid Ensembl accession.") diff --git a/tests/lib/uniprot/test_utils.py b/tests/lib/uniprot/test_utils.py index 7afa278c2..5ac773e38 100644 --- a/tests/lib/uniprot/test_utils.py +++ b/tests/lib/uniprot/test_utils.py @@ -21,6 +21,14 @@ def test_infer_db_name_from_sequence_accession_np(): assert result == "RefSeq_Protein" +# Both versioned and unversioned ENSP accessions must route to Ensembl_Protein; the versioned case +# guards that validate_ensembl_identifier's version handling continues to accept it. +@pytest.mark.parametrize("ensembl_protein_accession", ["ENSP00000418960", "ENSP00000418960.3"]) +def test_infer_db_name_from_sequence_accession_ensp(ensembl_protein_accession): + result = infer_db_name_from_sequence_accession(ensembl_protein_accession) + assert result == "Ensembl_Protein" + + @pytest.mark.parametrize("invalid_accession", ["XP_000000", VALID_CHR_ACCESSION]) def test_infer_db_name_from_sequence_accession_invalid(invalid_accession): with pytest.raises(NotImplementedError): diff --git a/tests/validation/test_identifier.py b/tests/validation/test_identifier.py index 005278307..9cbd16b0d 100644 --- a/tests/validation/test_identifier.py +++ b/tests/validation/test_identifier.py @@ -63,6 +63,10 @@ def test_ve_invalid_list(self): def test_passes_valid_id(self): validate_ensembl_identifier("ENSG00000143384") + def test_passes_valid_versioned_id(self): + validate_ensembl_identifier("ENSP00000369497.3") + validate_ensembl_list(["ENSG00000139618.15", "ENST00000380152.8"]) + class TestRefSeqValidators(TestCase): """ From f44a9a131bd98a599bb01be773f8e374a1df05e5 Mon Sep 17 00:00:00 2001 From: David Reinhart Date: Wed, 8 Jul 2026 11:49:19 -0700 Subject: [PATCH 2/2] Add Ensembl transcript (ENST) support to sequence accession inference Extend infer_db_name_from_sequence_accession to recognize ENST identifiers and return "Ensembl_Transcript" as the database name. Restructure control flow to validate each accession type only within its matching branch rather than calling validate_refseq_identifier unconditionally for non-Ensembl accessions. --- src/mavedb/lib/uniprot/utils.py | 18 +++++++++--------- tests/lib/uniprot/test_utils.py | 18 +++++++++++------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/mavedb/lib/uniprot/utils.py b/src/mavedb/lib/uniprot/utils.py index 11c13a8f4..ec5a8a185 100644 --- a/src/mavedb/lib/uniprot/utils.py +++ b/src/mavedb/lib/uniprot/utils.py @@ -5,7 +5,7 @@ def infer_db_name_from_sequence_accession( sequence_accession: str, -) -> Union[Literal["RefSeq_Nucleotide", "RefSeq_Protein", "Ensembl_Protein"]]: +) -> Union[Literal["RefSeq_Nucleotide", "RefSeq_Protein", "Ensembl_Protein", "Ensembl_Transcript"]]: """ Infers the database name from a sequence accession. @@ -15,19 +15,19 @@ def infer_db_name_from_sequence_accession( Returns: str: The inferred database name. """ - # Ensembl protein accessions must be handled before validate_refseq_identifier, - # which raises on any non-RefSeq accession. if sequence_accession.startswith("ENSP"): validate_ensembl_identifier(sequence_accession) return "Ensembl_Protein" - - validate_refseq_identifier(sequence_accession) - - if sequence_accession.startswith("NM_"): + elif sequence_accession.startswith("ENST"): + validate_ensembl_identifier(sequence_accession) + return "Ensembl_Transcript" + elif sequence_accession.startswith("NM_"): + validate_refseq_identifier(sequence_accession) return "RefSeq_Nucleotide" - if sequence_accession.startswith("NP_"): + elif sequence_accession.startswith("NP_"): + validate_refseq_identifier(sequence_accession) return "RefSeq_Protein" raise NotImplementedError( - "Only RefSeq NM/NP and Ensembl protein (ENSP) identifiers are currently supported for inference." + "Only RefSeq (NM_/NP_) and Ensembl (ENSP/ENST) identifiers are currently supported for inference." ) diff --git a/tests/lib/uniprot/test_utils.py b/tests/lib/uniprot/test_utils.py index 5ac773e38..5d51c2afe 100644 --- a/tests/lib/uniprot/test_utils.py +++ b/tests/lib/uniprot/test_utils.py @@ -1,12 +1,10 @@ import pytest -from mavedb.lib.validation.exceptions import ValidationError from mavedb.lib.uniprot.utils import infer_db_name_from_sequence_accession - from tests.helpers.constants import ( + VALID_CHR_ACCESSION, VALID_NT_ACCESSION, VALID_PRO_ACCESSION, - VALID_CHR_ACCESSION, VALID_UNIPROT_ACCESSION, ) @@ -21,14 +19,20 @@ def test_infer_db_name_from_sequence_accession_np(): assert result == "RefSeq_Protein" -# Both versioned and unversioned ENSP accessions must route to Ensembl_Protein; the versioned case -# guards that validate_ensembl_identifier's version handling continues to accept it. +# Both versioned and unversioned ENSP and ENST accessions must route to Ensembl_Protein and Ensembl_Transcript, respectively; +# the versioned case guards that validate_ensembl_identifier's version handling continues to accept it. @pytest.mark.parametrize("ensembl_protein_accession", ["ENSP00000418960", "ENSP00000418960.3"]) def test_infer_db_name_from_sequence_accession_ensp(ensembl_protein_accession): result = infer_db_name_from_sequence_accession(ensembl_protein_accession) assert result == "Ensembl_Protein" +@pytest.mark.parametrize("ensembl_transcript_accession", ["ENST00000418960", "ENST00000418960.3"]) +def test_infer_db_name_from_sequence_accession_enst(ensembl_transcript_accession): + result = infer_db_name_from_sequence_accession(ensembl_transcript_accession) + assert result == "Ensembl_Transcript" + + @pytest.mark.parametrize("invalid_accession", ["XP_000000", VALID_CHR_ACCESSION]) def test_infer_db_name_from_sequence_accession_invalid(invalid_accession): with pytest.raises(NotImplementedError): @@ -36,10 +40,10 @@ def test_infer_db_name_from_sequence_accession_invalid(invalid_accession): def test_infer_db_name_from_non_refseq_accession(): - with pytest.raises(ValidationError): + with pytest.raises(NotImplementedError): infer_db_name_from_sequence_accession(VALID_UNIPROT_ACCESSION) def test_infer_db_name_from_sequence_accession_empty_string(): - with pytest.raises(ValidationError): + with pytest.raises(NotImplementedError): infer_db_name_from_sequence_accession("")