From 939f182c59af6f8445fa3b04dddb97afc6b0f931 Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Mon, 5 Oct 2026 23:10:39 -0400 Subject: [PATCH 01/10] test: serve hgvs UTA lookups from a recorded cache in tests Add VRS_HGVS_CACHE_MODE/VRS_HGVS_CACHE_FILE to enable the hgvs data provider cache, default tests to run mode with tests/data/hgvs_cache.pkl, and add `make record-hgvs-cache` to re-record it against UTA and a seqrepo-rest-service. --- Makefile | 6 ++++ src/ga4gh/vrs/utils/hgvs_tools.py | 40 ++++++++++++++++++++-- tests/conftest.py | 10 ++++++ tests/data/hgvs_cache.pkl | Bin 0 -> 17169 bytes tests/extras/test_hgvs_tools.py | 53 +++++++++++++++++++++++++++++- 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 tests/data/hgvs_cache.pkl diff --git a/Makefile b/Makefile index fcf3502b..7edef708 100644 --- a/Makefile +++ b/Makefile @@ -128,6 +128,12 @@ cleanest: cleaner clean-cassettes: find ./tests -type f -path '*/cassettes/*.yaml' -print0 | ${XRM} +#=> record-hgvs-cache: re-record tests/data/hgvs_cache.pkl (needs UTA_DB_URL and a running seqrepo-rest-service at SEQREPO_REST_URL; VCR cassettes are not touched) +.PHONY: record-hgvs-cache +record-hgvs-cache: + rm -f tests/data/hgvs_cache.pkl + HGVS_SEQREPO_URL="$${SEQREPO_REST_URL:-http://localhost:5000/seqrepo}" VRS_HGVS_CACHE_MODE=learn pytest --disable-vcr + ## ## Copyright 2016 Source Code Committers diff --git a/src/ga4gh/vrs/utils/hgvs_tools.py b/src/ga4gh/vrs/utils/hgvs_tools.py index 42d81f86..027a9689 100644 --- a/src/ga4gh/vrs/utils/hgvs_tools.py +++ b/src/ga4gh/vrs/utils/hgvs_tools.py @@ -1,6 +1,8 @@ """Provide extra tools for working with hgvs expressions.""" +import functools import logging +import os import re import hgvs @@ -16,6 +18,21 @@ _logger = logging.getLogger(__name__) +HGVS_CACHE_MODES = ("learn", "run", "verify") + + +@functools.cache +def _connect_with_cache( + mode: str, cache_file: str +) -> hgvs.dataproviders.uta.UTA_postgresql: + """Return the data provider shared by every `HgvsTools` using this cache. + + Each data provider keeps its own in-memory copy of the cache file and, in learn + mode, rewrites the whole file after every miss, so separate providers writing the + same file would drop each other's entries. + """ + return hgvs.dataproviders.uta.connect(mode=mode, cache=cache_file) + class HgvsTools: """A convenience class that exposes only the tools needed by vrs-python for working with HGVS (Human Genome Variation Society) notation. @@ -34,10 +51,28 @@ class HgvsTools: def __init__(self, data_proxy: _DataProxy | None = None) -> None: """Initialize object. + The hgvs data provider (UTA) cache can be enabled with environment variables: + ``VRS_HGVS_CACHE_MODE`` (``learn``, ``run``, or ``verify``) and + ``VRS_HGVS_CACHE_FILE`` (path to the cache file). In ``run`` mode, lookups are + served only from the cache file and no UTA connection is made. + :param data_proxy: GA4GH data proxy instance """ self.parser = hgvs.parser.Parser() - self.uta_conn = hgvs.dataproviders.uta.connect() + cache_mode = os.environ.get("VRS_HGVS_CACHE_MODE") or None + cache_file = os.environ.get("VRS_HGVS_CACHE_FILE") or None + if cache_mode: + if cache_mode not in HGVS_CACHE_MODES: + msg = f"VRS_HGVS_CACHE_MODE must be one of {HGVS_CACHE_MODES}, got {cache_mode!r}" + raise ValueError(msg) + if not cache_file: + msg = "VRS_HGVS_CACHE_FILE must be set when VRS_HGVS_CACHE_MODE is set" + raise ValueError(msg) + self.uta_conn = _connect_with_cache(cache_mode, cache_file) + self._owns_uta_conn = False + else: + self.uta_conn = hgvs.dataproviders.uta.connect() + self._owns_uta_conn = True self.normalizer = hgvs.normalizer.Normalizer(self.uta_conn, validate=True) self.variant_mapper = hgvs.variantmapper.VariantMapper(self.uta_conn) self.data_proxy = data_proxy @@ -50,7 +85,8 @@ def close(self) -> None: self.normalizer = None self.variant_mapper = None self.data_proxy = None - if self.uta_conn is not None: + # A cache-backed data provider is shared with other instances; leave it open + if self.uta_conn is not None and self._owns_uta_conn: self.uta_conn.close() # convenience methods for hgvs parsing, normalization, and some mappings diff --git a/tests/conftest.py b/tests/conftest.py index 4c843539..8f407de1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,20 @@ import os +from pathlib import Path import pytest from biocommons.seqrepo import SeqRepo from ga4gh.vrs.dataproxy import SeqRepoDataProxy, SeqRepoRESTDataProxy +# Serve hgvs data provider lookups (UTA queries and hgvs sequence fetches) from a +# recorded cache by default, so tests don't need a UTA database or network access. To +# re-record, run `make record-hgvs-cache` with UTA_DB_URL pointing at a UTA instance and +# a seqrepo-rest-service running at SEQREPO_REST_URL. +os.environ.setdefault("VRS_HGVS_CACHE_MODE", "run") +os.environ.setdefault( + "VRS_HGVS_CACHE_FILE", str(Path(__file__).parent / "data" / "hgvs_cache.pkl") +) + def remove_request_headers(request): """Remove all headers from VCR request before recording.""" diff --git a/tests/data/hgvs_cache.pkl b/tests/data/hgvs_cache.pkl new file mode 100644 index 0000000000000000000000000000000000000000..5ee7e7eb3d3cdc0ea65efd766dfa8a6cf0e229ff GIT binary patch literal 17169 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 EUq4F6 Date: Mon, 5 Oct 2026 23:54:54 -0400 Subject: [PATCH 02/10] docs: describe recording VCR cassettes and the hgvs test cache --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 3e5686de..ecebeac8 100644 --- a/README.md +++ b/README.md @@ -315,6 +315,40 @@ To run tests: make test ``` +#### Recorded test data + +Tests run offline by replaying recorded data, so they don't need a SeqRepo REST +service, a UTA database, or network access. Set `SEQREPO_ROOT_DIR=tests/data/seqrepo/latest` +(as CI does) when running or recording tests. + +- **VCR cassettes** (`tests/**/cassettes/*.yaml`) replay vrs-python's HTTP requests to + the [SeqRepo REST service](https://github.com/biocommons/seqrepo-rest-service). CI + runs `pytest --vcr-record=none`, so a test making an unrecorded request fails. +- **The hgvs cache** (`tests/data/hgvs_cache.pkl`) replays the hgvs library's UTA + queries and sequence fetches. `tests/conftest.py` enables it by default + (`VRS_HGVS_CACHE_MODE=run`), and a lookup missing from the cache raises + `HGVSDataNotAvailableError`. + +To re-record, start the external services (`docker compose up` starts +seqrepo-rest-service on port 5000 and UTA on port 5432). With an existing local SeqRepo +snapshot, you can instead run just the REST service with it mounted: +`docker run -p 5000:5000 -v /path/to/seqrepo:/usr/local/share/seqrepo:ro biocommons/seqrepo-rest-service:0.2.2 seqrepo-rest-service /usr/local/share/seqrepo/2024-12-20`. +When a change needs new data in both, re-record the hgvs cache first, since cassette +recording reads hgvs data from it: + +```shell +# hgvs cache: needs UTA and seqrepo-rest-service; does not touch cassettes +export UTA_DB_URL=postgresql://anonymous@localhost:5432/uta/uta_20241220 +make record-hgvs-cache + +# VCR cassettes: needs seqrepo-rest-service +make clean-cassettes +pytest --vcr-record=once +``` + +`SEQREPO_REST_URL` (default `http://localhost:5000/seqrepo`) sets the REST service used +by both. + ## Running the Notebooks The notebooks **do not** require you to setup SeqRepo or UTA from From 32064ff9b137071cfd7f7191a6795fd82186cf71 Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Mon, 5 Oct 2026 23:54:55 -0400 Subject: [PATCH 03/10] test: re-record hgvs-related cassettes without NCBI sequence requests hgvs sequence fetches are now served from tests/data/hgvs_cache.pkl, so the NCBI E-utilities interactions recorded in these cassettes are no longer used. --- ...NC_000007.14:g.55181220del-expected2].yaml | 36 ---------- ...g.55181230_55181231insGGCT-expected3].yaml | 54 -------------- ...NC_000007.14:g.55181320A>T-expected1].yaml | 36 ---------- ...NC_000013.11:g.32316467dup-expected5].yaml | 36 ---------- ....11:g.32331093_32331094dup-expected4].yaml | 72 ------------------- ...s[NC_000013.11:g.32936732=-expected0].yaml | 18 ----- ....10:g.289464_289465insCACA-expected8].yaml | 72 ------------------- ...0019.10:g.289485_289500del-expected9].yaml | 54 -------------- ...vs[NM_001331029.1:c.722A>G-expected6].yaml | 36 ---------- ...hgvs[NM_181798.1:c.1007G>T-expected7].yaml | 54 -------------- .../extras/cassettes/test_rle_seq_limit.yaml | 38 ---------- 11 files changed, 506 deletions(-) diff --git a/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181220del-expected2].yaml b/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181220del-expected2].yaml index 01a253d4..9515082d 100644 --- a/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181220del-expected2].yaml +++ b/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181220del-expected2].yaml @@ -59,40 +59,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000007.14&rettype=fasta&seq_start=55181220&seq_stop=55181220&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000007.14:55181220-55181220 Homo sapiens chromosome 7, GRCh38.p14 - Primary Assembly - - T - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000007.14&rettype=fasta&seq_start=55181220&seq_stop=55181240&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000007.14:55181220-55181240 Homo sapiens chromosome 7, GRCh38.p14 - Primary Assembly - - TGGAAGGGGTCCATGTGCCCC - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181230_55181231insGGCT-expected3].yaml b/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181230_55181231insGGCT-expected3].yaml index dbf9fef6..8e01ba90 100644 --- a/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181230_55181231insGGCT-expected3].yaml +++ b/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181230_55181231insGGCT-expected3].yaml @@ -35,58 +35,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000007.14&rettype=fasta&seq_start=55181231&seq_stop=55181231&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000007.14:55181231-55181231 Homo sapiens chromosome 7, GRCh38.p14 - Primary Assembly - - C - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000007.14&rettype=fasta&seq_start=55181230&seq_stop=55181250&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000007.14:55181230-55181250 Homo sapiens chromosome 7, GRCh38.p14 - Primary Assembly - - CCATGTGCCCCTCCTTCTGGC - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000007.14&rettype=fasta&seq_start=55181227&seq_stop=55181230&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000007.14:55181227-55181230 Homo sapiens chromosome 7, GRCh38.p14 - Primary Assembly - - GGTC - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181320A>T-expected1].yaml b/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181320A>T-expected1].yaml index 3c14c730..22b2d38f 100644 --- a/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181320A>T-expected1].yaml +++ b/tests/extras/cassettes/test_hgvs[NC_000007.14:g.55181320A>T-expected1].yaml @@ -34,40 +34,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000007.14&rettype=fasta&seq_start=55181320&seq_stop=55181320&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000007.14:55181320-55181320 Homo sapiens chromosome 7, GRCh38.p14 - Primary Assembly - - A - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000007.14&rettype=fasta&seq_start=55181320&seq_stop=55181340&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000007.14:55181320-55181340 Homo sapiens chromosome 7, GRCh38.p14 - Primary Assembly - - AACCCCCACGTGTGCCGCCTG - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32316467dup-expected5].yaml b/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32316467dup-expected5].yaml index 7a529525..dbc79ed2 100644 --- a/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32316467dup-expected5].yaml +++ b/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32316467dup-expected5].yaml @@ -59,40 +59,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32316467&seq_stop=32316467&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32316467-32316467 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - A - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32316467&seq_stop=32316487&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32316467-32316487 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - ATTGGATCCAAAGAGAGGCCA - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32331093_32331094dup-expected4].yaml b/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32331093_32331094dup-expected4].yaml index e61ad59e..2a9d5445 100644 --- a/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32331093_32331094dup-expected4].yaml +++ b/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32331093_32331094dup-expected4].yaml @@ -215,76 +215,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32331083&seq_stop=32331094&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32331083-32331094 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - TTTTTTTTTTTT - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32331094&seq_stop=32331094&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32331094-32331094 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - T - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32331083&seq_stop=32331114&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32331083-32331114 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - TTTTTTTTTTTTGAGGTGGAGTCTTGCTCTGT - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32331093&seq_stop=32331094&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32331093-32331094 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - TT - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32936732=-expected0].yaml b/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32936732=-expected0].yaml index bded6051..ab55ae01 100644 --- a/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32936732=-expected0].yaml +++ b/tests/extras/cassettes/test_hgvs[NC_000013.11:g.32936732=-expected0].yaml @@ -11,22 +11,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32936732&seq_stop=32936732&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32936732-32936732 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - C - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NC_000019.10:g.289464_289465insCACA-expected8].yaml b/tests/extras/cassettes/test_hgvs[NC_000019.10:g.289464_289465insCACA-expected8].yaml index 3cf20e71..ff25ea33 100644 --- a/tests/extras/cassettes/test_hgvs[NC_000019.10:g.289464_289465insCACA-expected8].yaml +++ b/tests/extras/cassettes/test_hgvs[NC_000019.10:g.289464_289465insCACA-expected8].yaml @@ -71,76 +71,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000019.10&rettype=fasta&seq_start=289465&seq_stop=289466&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000019.10:289465-289466 Homo sapiens chromosome 19, GRCh38.p14 - Primary Assembly - - CA - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000019.10&rettype=fasta&seq_start=289466&seq_stop=289466&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000019.10:289466-289466 Homo sapiens chromosome 19, GRCh38.p14 - Primary Assembly - - A - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000019.10&rettype=fasta&seq_start=289465&seq_stop=289486&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000019.10:289465-289486 Homo sapiens chromosome 19, GRCh38.p14 - Primary Assembly - - CAGCACTTTGGGAGGCCGAGGC - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000019.10&rettype=fasta&seq_start=289463&seq_stop=289466&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000019.10:289463-289466 Homo sapiens chromosome 19, GRCh38.p14 - Primary Assembly - - CTCA - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NC_000019.10:g.289485_289500del-expected9].yaml b/tests/extras/cassettes/test_hgvs[NC_000019.10:g.289485_289500del-expected9].yaml index 96e65209..2a55e187 100644 --- a/tests/extras/cassettes/test_hgvs[NC_000019.10:g.289485_289500del-expected9].yaml +++ b/tests/extras/cassettes/test_hgvs[NC_000019.10:g.289485_289500del-expected9].yaml @@ -119,58 +119,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000019.10&rettype=fasta&seq_start=289481&seq_stop=289501&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000019.10:289481-289501 Homo sapiens chromosome 19, GRCh38.p14 - Primary Assembly - - CGAGGCGGGCAGATCACGAGG - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000019.10&rettype=fasta&seq_start=289501&seq_stop=289501&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000019.10:289501-289501 Homo sapiens chromosome 19, GRCh38.p14 - Primary Assembly - - G - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000019.10&rettype=fasta&seq_start=289481&seq_stop=289521&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000019.10:289481-289521 Homo sapiens chromosome 19, GRCh38.p14 - Primary Assembly - - CGAGGCGGGCAGATCACGAGGTCAGGAGATCGAGACCATCC - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NM_001331029.1:c.722A>G-expected6].yaml b/tests/extras/cassettes/test_hgvs[NM_001331029.1:c.722A>G-expected6].yaml index 44352fa1..d86ad12f 100644 --- a/tests/extras/cassettes/test_hgvs[NM_001331029.1:c.722A>G-expected6].yaml +++ b/tests/extras/cassettes/test_hgvs[NM_001331029.1:c.722A>G-expected6].yaml @@ -16,24 +16,6 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NM_001331029.1&rettype=fasta&seq_start=872&seq_stop=872&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NM_001331029.1:872-872 Homo sapiens protein phosphatase 1 regulatory - subunit 12B (PPP1R12B), transcript variant 8, mRNA - - A - - - ' - headers: {} - status: - code: 200 - message: OK - request: body: null headers: {} @@ -63,22 +45,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NM_001331029.1&rettype=fasta&seq_start=872&seq_stop=892&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NM_001331029.1:872-892 Homo sapiens protein phosphatase 1 regulatory - subunit 12B (PPP1R12B), transcript variant 8, mRNA - - ATGAACTCAATGTTCAGGATT - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_hgvs[NM_181798.1:c.1007G>T-expected7].yaml b/tests/extras/cassettes/test_hgvs[NM_181798.1:c.1007G>T-expected7].yaml index 20c3a261..34775aab 100644 --- a/tests/extras/cassettes/test_hgvs[NM_181798.1:c.1007G>T-expected7].yaml +++ b/tests/extras/cassettes/test_hgvs[NM_181798.1:c.1007G>T-expected7].yaml @@ -16,24 +16,6 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NM_181798.1&rettype=fasta&seq_start=1263&seq_stop=1263&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NM_181798.1:1263-1263 Homo sapiens potassium voltage-gated channel - subfamily Q member 1 (KCNQ1), transcript variant 2, mRNA - - G - - - ' - headers: {} - status: - code: 200 - message: OK - request: body: null headers: {} @@ -63,40 +45,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NM_181798.1&rettype=fasta&seq_start=1263&seq_stop=1268&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NM_181798.1:1263-1268 Homo sapiens potassium voltage-gated channel - subfamily Q member 1 (KCNQ1), transcript variant 2, mRNA - - GTTCTG - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NR_040711.1&rettype=fasta&seq_start=1263&seq_stop=1263&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NR_040711.1:1263-1263 Homo sapiens potassium voltage-gated channel, - KQT-like subfamily, member 1 (KCNQ1), transcript variant 2, non-coding RNA - - G - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 diff --git a/tests/extras/cassettes/test_rle_seq_limit.yaml b/tests/extras/cassettes/test_rle_seq_limit.yaml index d7046ab8..96521dda 100644 --- a/tests/extras/cassettes/test_rle_seq_limit.yaml +++ b/tests/extras/cassettes/test_rle_seq_limit.yaml @@ -503,42 +503,4 @@ interactions: status: code: 200 message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32331043&seq_stop=32331094&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32331043-32331094 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - TTTAGTTGAACTACAGGTTTTTTTGTTGTTGTTGTTTTGATTTTTTTTTTTT - - - ' - headers: {} - status: - code: 200 - message: OK -- request: - body: null - headers: {} - method: GET - uri: https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nucleotide&id=NC_000013.11&rettype=fasta&seq_start=32331043&seq_stop=32331114&tool=bioutils&email=biocommons-dev@googlegroups.com - response: - body: - string: '>NC_000013.11:32331043-32331114 Homo sapiens chromosome 13, GRCh38.p14 - Primary Assembly - - TTTAGTTGAACTACAGGTTTTTTTGTTGTTGTTGTTTTGATTTTTTTTTTTTGAGGTGGAGTCTTGCTCT - - GT - - - ' - headers: {} - status: - code: 200 - message: OK version: 1 From 01c02084f04e7199e8f00ff88d52b1b12e60d980 Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Tue, 6 Oct 2026 00:01:08 -0400 Subject: [PATCH 04/10] test: explain how the hgvs cache tests work in their docstrings --- tests/extras/test_hgvs_tools.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/extras/test_hgvs_tools.py b/tests/extras/test_hgvs_tools.py index 70603c9b..7473927a 100644 --- a/tests/extras/test_hgvs_tools.py +++ b/tests/extras/test_hgvs_tools.py @@ -47,7 +47,12 @@ def test_is_intronic(hgvs_tools, hgvs_expr, expected): def test_hgvs_cache_run_mode_does_not_connect(monkeypatch): """With the tests' default hgvs cache (run mode), lookups are served from the cache - file without connecting to UTA + file without connecting to UTA. + + Replaces hgvs's `UTA_postgresql._connect` with a function that fails, clears the + memoized shared data provider so a new one is built under the patch, then creates + an `HgvsTools` and does a cached lookup. Skipped outside run mode (e.g. when + re-recording the cache), where connecting is expected. """ if os.environ.get("VRS_HGVS_CACHE_MODE") != "run": pytest.skip("only applies when the hgvs cache is in run mode") @@ -86,7 +91,9 @@ def test_hgvs_cache_data_provider_is_shared(): @pytest.mark.parametrize("case", hgvs_cache_env_invalid_cases, ids=lambda c: c["id"]) def test_hgvs_cache_env_invalid(monkeypatch, case): - """Invalid hgvs cache settings raise rather than silently connecting to UTA""" + """Invalid hgvs cache settings raise rather than silently connecting to UTA: an + unknown `VRS_HGVS_CACHE_MODE`, or a mode set without `VRS_HGVS_CACHE_FILE` + """ for name, value in case["env"].items(): monkeypatch.setenv(name, value) with pytest.raises(ValueError, match=case["match"]): From b1691fc986a9b1be18656138dbeb5e087dcd578f Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Tue, 6 Oct 2026 14:30:17 -0400 Subject: [PATCH 05/10] test: check hgvs run mode stays offline through a full translation --- .../test_hgvs_cache_run_mode_is_offline.yaml | 48 +++++++++++++++++++ tests/extras/test_allele_translator.py | 31 ++++++++++++ tests/extras/test_hgvs_tools.py | 28 +---------- 3 files changed, 80 insertions(+), 27 deletions(-) create mode 100644 tests/extras/cassettes/test_hgvs_cache_run_mode_is_offline.yaml diff --git a/tests/extras/cassettes/test_hgvs_cache_run_mode_is_offline.yaml b/tests/extras/cassettes/test_hgvs_cache_run_mode_is_offline.yaml new file mode 100644 index 00000000..34775aab --- /dev/null +++ b/tests/extras/cassettes/test_hgvs_cache_run_mode_is_offline.yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: {} + method: GET + uri: http://localhost:5000/seqrepo/1/metadata/refseq:NM_181798.1 + response: + body: + string: "{\n \"added\": \"2016-08-24T05:03:05Z\",\n \"aliases\": [\n \"MD5:93e2dc533ba755782b3affcf53470434\",\n + \ \"NCBI:NM_181798.1\",\n \"refseq:NM_181798.1\",\n \"NCBI:NR_040711.1\",\n + \ \"refseq:NR_040711.1\",\n \"SEGUID:0nrqUDibhGAQBZ4pXR+soOiCOf0\",\n + \ \"SHA1:d27aea50389b846010059e295d1faca0e88239fd\",\n \"VMC:GS_KN07u-RFqd1dTyOWOG98HnOq87Nq-ZIg\",\n + \ \"sha512t24u:KN07u-RFqd1dTyOWOG98HnOq87Nq-ZIg\",\n \"ga4gh:SQ.KN07u-RFqd1dTyOWOG98HnOq87Nq-ZIg\"\n + \ ],\n \"alphabet\": \"ACGT\",\n \"length\": 3029\n}\n" + headers: {} + status: + code: 200 + message: OK +- request: + body: null + headers: {} + method: GET + uri: http://localhost:5000/seqrepo/1/metadata/ga4gh:SQ.KN07u-RFqd1dTyOWOG98HnOq87Nq-ZIg + response: + body: + string: "{\n \"added\": \"2016-08-24T05:03:05Z\",\n \"aliases\": [\n \"MD5:93e2dc533ba755782b3affcf53470434\",\n + \ \"NCBI:NM_181798.1\",\n \"refseq:NM_181798.1\",\n \"NCBI:NR_040711.1\",\n + \ \"refseq:NR_040711.1\",\n \"SEGUID:0nrqUDibhGAQBZ4pXR+soOiCOf0\",\n + \ \"SHA1:d27aea50389b846010059e295d1faca0e88239fd\",\n \"VMC:GS_KN07u-RFqd1dTyOWOG98HnOq87Nq-ZIg\",\n + \ \"sha512t24u:KN07u-RFqd1dTyOWOG98HnOq87Nq-ZIg\",\n \"ga4gh:SQ.KN07u-RFqd1dTyOWOG98HnOq87Nq-ZIg\"\n + \ ],\n \"alphabet\": \"ACGT\",\n \"length\": 3029\n}\n" + headers: {} + status: + code: 200 + message: OK +- request: + body: null + headers: {} + method: GET + uri: http://localhost:5000/seqrepo/1/sequence/ga4gh:SQ.KN07u-RFqd1dTyOWOG98HnOq87Nq-ZIg?start=1262&end=1263 + response: + body: + string: G + headers: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/extras/test_allele_translator.py b/tests/extras/test_allele_translator.py index 15f87a89..6c7f5dfc 100644 --- a/tests/extras/test_allele_translator.py +++ b/tests/extras/test_allele_translator.py @@ -1,3 +1,8 @@ +import os +from typing import NoReturn + +import hgvs.dataproviders.seqfetcher +import hgvs.dataproviders.uta import pytest from ga4gh.vrs import models @@ -751,6 +756,32 @@ def test_hgvs(tlr, hgvsexpr, expected): ) +@pytest.mark.vcr +def test_hgvs_cache_run_mode_is_offline( + monkeypatch: pytest.MonkeyPatch, tlr: AlleleTranslator +) -> None: + """With the tests' default hgvs cache (run mode), translating to and from hgvs + makes no UTA queries or hgvs sequence fetches. + + Replaces the two places hgvs reaches outside the process, `UTA_postgresql._get_cursor` + (every UTA query) and `SeqFetcher.fetch_seq` (every sequence fetch), with a function + that fails, then translates a transcript variant both ways. Skipped outside run mode + (e.g. when re-recording the cache), where both are expected. + """ + if os.environ.get("VRS_HGVS_CACHE_MODE") != "run": + pytest.skip("only applies when the hgvs cache is in run mode") + + def fail(*args: object, **kwargs: object) -> NoReturn: # noqa: ARG001 + msg = "hgvs queried UTA or fetched a sequence in hgvs cache run mode" + raise AssertionError(msg) + + monkeypatch.setattr(hgvs.dataproviders.uta.UTA_postgresql, "_get_cursor", fail) + monkeypatch.setattr(hgvs.dataproviders.seqfetcher.SeqFetcher, "fetch_seq", fail) + + allele = tlr.translate_from("NM_181798.1:c.1007G>T", "hgvs") + assert "NM_181798.1:c.1007G>T" in tlr.translate_to(allele, "hgvs") + + @pytest.mark.vcr def test_rle_seq_limit(tlr): """Test that for an ReferenceLengthExpression over 50bp, the sequence is not diff --git a/tests/extras/test_hgvs_tools.py b/tests/extras/test_hgvs_tools.py index 7473927a..0b3dea66 100644 --- a/tests/extras/test_hgvs_tools.py +++ b/tests/extras/test_hgvs_tools.py @@ -1,12 +1,9 @@ """Tests for ga4gh.vrs.utils.hgvs_tools.""" -import os - -import hgvs.dataproviders.uta import hgvs.parser import pytest -from ga4gh.vrs.utils.hgvs_tools import HgvsTools, _connect_with_cache +from ga4gh.vrs.utils.hgvs_tools import HgvsTools @pytest.fixture(scope="module") @@ -45,29 +42,6 @@ def test_is_intronic(hgvs_tools, hgvs_expr, expected): assert hgvs_tools.is_intronic(sv) is expected -def test_hgvs_cache_run_mode_does_not_connect(monkeypatch): - """With the tests' default hgvs cache (run mode), lookups are served from the cache - file without connecting to UTA. - - Replaces hgvs's `UTA_postgresql._connect` with a function that fails, clears the - memoized shared data provider so a new one is built under the patch, then creates - an `HgvsTools` and does a cached lookup. Skipped outside run mode (e.g. when - re-recording the cache), where connecting is expected. - """ - if os.environ.get("VRS_HGVS_CACHE_MODE") != "run": - pytest.skip("only applies when the hgvs cache is in run mode") - - def fail_connect(self): # noqa: ARG001 - msg = "UTA connection attempted in hgvs cache run mode" - raise AssertionError(msg) - - monkeypatch.setattr(hgvs.dataproviders.uta.UTA_postgresql, "_connect", fail_connect) - _connect_with_cache.cache_clear() - - tools = HgvsTools() - assert tools.uta_conn.get_tx_identity_info("NM_181798.1")["tx_ac"] == "NM_181798.1" - - def test_hgvs_cache_data_provider_is_shared(): """HgvsTools instances using the same cache share one data provider, so in learn mode they cannot overwrite each other's cache entries From 0bac5e9670b7d58bc4136ed1ac29da9b6e76970a Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Tue, 6 Oct 2026 14:31:29 -0400 Subject: [PATCH 06/10] test: provide the hgvs cache from a conftest fixture --- src/ga4gh/vrs/utils/hgvs_tools.py | 40 ++--------------------- tests/conftest.py | 45 +++++++++++++++++++++----- tests/extras/test_allele_translator.py | 8 +++-- tests/extras/test_hgvs_tools.py | 37 ++++++--------------- 4 files changed, 53 insertions(+), 77 deletions(-) diff --git a/src/ga4gh/vrs/utils/hgvs_tools.py b/src/ga4gh/vrs/utils/hgvs_tools.py index 027a9689..42d81f86 100644 --- a/src/ga4gh/vrs/utils/hgvs_tools.py +++ b/src/ga4gh/vrs/utils/hgvs_tools.py @@ -1,8 +1,6 @@ """Provide extra tools for working with hgvs expressions.""" -import functools import logging -import os import re import hgvs @@ -18,21 +16,6 @@ _logger = logging.getLogger(__name__) -HGVS_CACHE_MODES = ("learn", "run", "verify") - - -@functools.cache -def _connect_with_cache( - mode: str, cache_file: str -) -> hgvs.dataproviders.uta.UTA_postgresql: - """Return the data provider shared by every `HgvsTools` using this cache. - - Each data provider keeps its own in-memory copy of the cache file and, in learn - mode, rewrites the whole file after every miss, so separate providers writing the - same file would drop each other's entries. - """ - return hgvs.dataproviders.uta.connect(mode=mode, cache=cache_file) - class HgvsTools: """A convenience class that exposes only the tools needed by vrs-python for working with HGVS (Human Genome Variation Society) notation. @@ -51,28 +34,10 @@ class HgvsTools: def __init__(self, data_proxy: _DataProxy | None = None) -> None: """Initialize object. - The hgvs data provider (UTA) cache can be enabled with environment variables: - ``VRS_HGVS_CACHE_MODE`` (``learn``, ``run``, or ``verify``) and - ``VRS_HGVS_CACHE_FILE`` (path to the cache file). In ``run`` mode, lookups are - served only from the cache file and no UTA connection is made. - :param data_proxy: GA4GH data proxy instance """ self.parser = hgvs.parser.Parser() - cache_mode = os.environ.get("VRS_HGVS_CACHE_MODE") or None - cache_file = os.environ.get("VRS_HGVS_CACHE_FILE") or None - if cache_mode: - if cache_mode not in HGVS_CACHE_MODES: - msg = f"VRS_HGVS_CACHE_MODE must be one of {HGVS_CACHE_MODES}, got {cache_mode!r}" - raise ValueError(msg) - if not cache_file: - msg = "VRS_HGVS_CACHE_FILE must be set when VRS_HGVS_CACHE_MODE is set" - raise ValueError(msg) - self.uta_conn = _connect_with_cache(cache_mode, cache_file) - self._owns_uta_conn = False - else: - self.uta_conn = hgvs.dataproviders.uta.connect() - self._owns_uta_conn = True + self.uta_conn = hgvs.dataproviders.uta.connect() self.normalizer = hgvs.normalizer.Normalizer(self.uta_conn, validate=True) self.variant_mapper = hgvs.variantmapper.VariantMapper(self.uta_conn) self.data_proxy = data_proxy @@ -85,8 +50,7 @@ def close(self) -> None: self.normalizer = None self.variant_mapper = None self.data_proxy = None - # A cache-backed data provider is shared with other instances; leave it open - if self.uta_conn is not None and self._owns_uta_conn: + if self.uta_conn is not None: self.uta_conn.close() # convenience methods for hgvs parsing, normalization, and some mappings diff --git a/tests/conftest.py b/tests/conftest.py index 8f407de1..800d2042 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,48 @@ import os +from collections.abc import Iterator from pathlib import Path +import hgvs.dataproviders.uta import pytest from biocommons.seqrepo import SeqRepo from ga4gh.vrs.dataproxy import SeqRepoDataProxy, SeqRepoRESTDataProxy -# Serve hgvs data provider lookups (UTA queries and hgvs sequence fetches) from a -# recorded cache by default, so tests don't need a UTA database or network access. To -# re-record, run `make record-hgvs-cache` with UTA_DB_URL pointing at a UTA instance and -# a seqrepo-rest-service running at SEQREPO_REST_URL. -os.environ.setdefault("VRS_HGVS_CACHE_MODE", "run") -os.environ.setdefault( - "VRS_HGVS_CACHE_FILE", str(Path(__file__).parent / "data" / "hgvs_cache.pkl") -) +HGVS_CACHE_MODES = ("learn", "run", "verify") +HGVS_CACHE_FILE = Path(__file__).parent / "data" / "hgvs_cache.pkl" + + +@pytest.fixture(scope="session", autouse=True) +def hgvs_cached_data_provider() -> Iterator[hgvs.dataproviders.uta.UTABase | None]: + """Serve hgvs data provider lookups (UTA queries and hgvs sequence fetches) from a + recorded cache, so tests don't need a UTA database or network access. + + Every `HgvsTools` gets this one data provider, so in learn mode they cannot + overwrite each other's cache entries. `VRS_HGVS_CACHE_MODE` is `run` (default), + `learn`, or `verify`, or empty to query UTA directly without the cache, and + `VRS_HGVS_CACHE_FILE` overrides the cache file. In run mode, a lookup missing from + the cache raises `HGVSDataNotAvailableError`. To re-record, run + `make record-hgvs-cache` with UTA_DB_URL pointing at a UTA instance and a + seqrepo-rest-service running at SEQREPO_REST_URL. + """ + mode = os.environ.get("VRS_HGVS_CACHE_MODE", "run") + if not mode: + yield None + return + if mode not in HGVS_CACHE_MODES: + msg = f"VRS_HGVS_CACHE_MODE must be one of {HGVS_CACHE_MODES} or empty, got {mode!r}" + raise pytest.UsageError(msg) + cache_file = os.environ.get("VRS_HGVS_CACHE_FILE") or str(HGVS_CACHE_FILE) + + provider = hgvs.dataproviders.uta.connect(mode=mode, cache=cache_file) + with pytest.MonkeyPatch.context() as mp: + mp.setattr( + hgvs.dataproviders.uta, "connect", lambda *_args, **_kwargs: provider + ) + # HgvsTools.close() closes its data provider, but this one is shared + mp.setattr(provider, "close", lambda: None) + yield provider + provider.close() def remove_request_headers(request): diff --git a/tests/extras/test_allele_translator.py b/tests/extras/test_allele_translator.py index 6c7f5dfc..ce161082 100644 --- a/tests/extras/test_allele_translator.py +++ b/tests/extras/test_allele_translator.py @@ -1,9 +1,9 @@ -import os from typing import NoReturn import hgvs.dataproviders.seqfetcher import hgvs.dataproviders.uta import pytest +from hgvs.decorators.lru_cache import RUN from ga4gh.vrs import models from ga4gh.vrs.dataproxy import DataProxyValidationError @@ -758,7 +758,9 @@ def test_hgvs(tlr, hgvsexpr, expected): @pytest.mark.vcr def test_hgvs_cache_run_mode_is_offline( - monkeypatch: pytest.MonkeyPatch, tlr: AlleleTranslator + monkeypatch: pytest.MonkeyPatch, + tlr: AlleleTranslator, + hgvs_cached_data_provider: hgvs.dataproviders.uta.UTABase | None, ) -> None: """With the tests' default hgvs cache (run mode), translating to and from hgvs makes no UTA queries or hgvs sequence fetches. @@ -768,7 +770,7 @@ def test_hgvs_cache_run_mode_is_offline( that fails, then translates a transcript variant both ways. Skipped outside run mode (e.g. when re-recording the cache), where both are expected. """ - if os.environ.get("VRS_HGVS_CACHE_MODE") != "run": + if hgvs_cached_data_provider is None or hgvs_cached_data_provider.mode != RUN: pytest.skip("only applies when the hgvs cache is in run mode") def fail(*args: object, **kwargs: object) -> NoReturn: # noqa: ARG001 diff --git a/tests/extras/test_hgvs_tools.py b/tests/extras/test_hgvs_tools.py index 0b3dea66..5d48dbeb 100644 --- a/tests/extras/test_hgvs_tools.py +++ b/tests/extras/test_hgvs_tools.py @@ -1,5 +1,6 @@ """Tests for ga4gh.vrs.utils.hgvs_tools.""" +import hgvs.dataproviders.uta import hgvs.parser import pytest @@ -42,33 +43,13 @@ def test_is_intronic(hgvs_tools, hgvs_expr, expected): assert hgvs_tools.is_intronic(sv) is expected -def test_hgvs_cache_data_provider_is_shared(): - """HgvsTools instances using the same cache share one data provider, so in learn +def test_hgvs_cache_data_provider_is_shared( + hgvs_cached_data_provider: hgvs.dataproviders.uta.UTABase | None, +) -> None: + """Every HgvsTools gets the tests' shared hgvs cache data provider, so in learn mode they cannot overwrite each other's cache entries """ - assert HgvsTools().uta_conn is HgvsTools().uta_conn - - -hgvs_cache_env_invalid_cases = [ - { - "id": "unknown-mode", - "env": {"VRS_HGVS_CACHE_MODE": "lean"}, - "match": "must be one of", - }, - { - "id": "mode-without-file", - "env": {"VRS_HGVS_CACHE_FILE": ""}, - "match": "VRS_HGVS_CACHE_FILE must be set", - }, -] - - -@pytest.mark.parametrize("case", hgvs_cache_env_invalid_cases, ids=lambda c: c["id"]) -def test_hgvs_cache_env_invalid(monkeypatch, case): - """Invalid hgvs cache settings raise rather than silently connecting to UTA: an - unknown `VRS_HGVS_CACHE_MODE`, or a mode set without `VRS_HGVS_CACHE_FILE` - """ - for name, value in case["env"].items(): - monkeypatch.setenv(name, value) - with pytest.raises(ValueError, match=case["match"]): - HgvsTools() + if hgvs_cached_data_provider is None: + pytest.skip("the hgvs cache is disabled") + assert HgvsTools().uta_conn is hgvs_cached_data_provider + assert HgvsTools().uta_conn is hgvs_cached_data_provider From 4c1b65a15a688db180aa7b7bea1091c35c67ce74 Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Tue, 6 Oct 2026 14:32:21 -0400 Subject: [PATCH 07/10] test: validate the hgvs cache file before loading it --- tests/conftest.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 800d2042..8e62e9cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,54 @@ import os +import pickle from collections.abc import Iterator from pathlib import Path +from typing import Any import hgvs.dataproviders.uta import pytest from biocommons.seqrepo import SeqRepo +from hgvs.decorators.lru_cache import _make_key from ga4gh.vrs.dataproxy import SeqRepoDataProxy, SeqRepoRESTDataProxy HGVS_CACHE_MODES = ("learn", "run", "verify") HGVS_CACHE_FILE = Path(__file__).parent / "data" / "hgvs_cache.pkl" +# The only classes a recorded hgvs cache file is made of: its keys (_HashedSeq), UTA +# rows (DictRow, with an OrderedDict column index), and what pickle rebuilds them with +HGVS_CACHE_ALLOWED_CLASSES = { + ("builtins", "list"), + ("collections", "OrderedDict"), + ("copyreg", "_reconstructor"), + ("hgvs.decorators.lru_cache", "_HashedSeq"), + ("psycopg2.extras", "DictRow"), +} + + +class HgvsCacheUnpickler(pickle.Unpickler): + """Unpickler that refuses any class not in `HGVS_CACHE_ALLOWED_CLASSES`""" + + def find_class(self, module: str, name: str) -> Any: # noqa: ANN401 + if (module, name) not in HGVS_CACHE_ALLOWED_CLASSES: + msg = f"hgvs cache file references a disallowed class: {module}.{name}" + raise pickle.UnpicklingError(msg) + return super().find_class(module, name) + + +def check_hgvs_cache_file(cache_file: str) -> None: + """Check an hgvs cache file before hgvs loads it with an unrestricted `pickle.load` + + The file may only reference the classes in `HGVS_CACHE_ALLOWED_CLASSES`, and its + keys must be in the format this hgvs version builds. Every data provider looks up + `schema_version` when it's created, so a recorded cache always has that key. + """ + with Path(cache_file).open("rb") as f: + cache: dict = HgvsCacheUnpickler(f).load() # cache keys -> recorded results + if _make_key("schema_version", (), {}, False, ()) not in cache: + msg = ( + f"{cache_file} has no schema_version entry in this hgvs version's cache key " + "format; re-record it with `make record-hgvs-cache`" + ) + raise pytest.UsageError(msg) @pytest.fixture(scope="session", autouse=True) @@ -33,6 +72,8 @@ def hgvs_cached_data_provider() -> Iterator[hgvs.dataproviders.uta.UTABase | Non msg = f"VRS_HGVS_CACHE_MODE must be one of {HGVS_CACHE_MODES} or empty, got {mode!r}" raise pytest.UsageError(msg) cache_file = os.environ.get("VRS_HGVS_CACHE_FILE") or str(HGVS_CACHE_FILE) + if Path(cache_file).exists(): + check_hgvs_cache_file(cache_file) provider = hgvs.dataproviders.uta.connect(mode=mode, cache=cache_file) with pytest.MonkeyPatch.context() as mp: From 5385c8ceec1b834dbca333b202ce24408f1c316a Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Tue, 6 Oct 2026 14:32:48 -0400 Subject: [PATCH 08/10] build: write the recorded hgvs cache only when recording succeeds --- .gitignore | 3 +++ Makefile | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5f1c1695..dd645d63 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ notebooks/scratch/ notes lint uta_*.pgd.gz + +# partial hgvs cache from an interrupted `make record-hgvs-cache` +tests/data/hgvs_cache.pkl.new diff --git a/Makefile b/Makefile index 7edef708..c3a5db9c 100644 --- a/Makefile +++ b/Makefile @@ -131,8 +131,9 @@ clean-cassettes: #=> record-hgvs-cache: re-record tests/data/hgvs_cache.pkl (needs UTA_DB_URL and a running seqrepo-rest-service at SEQREPO_REST_URL; VCR cassettes are not touched) .PHONY: record-hgvs-cache record-hgvs-cache: - rm -f tests/data/hgvs_cache.pkl - HGVS_SEQREPO_URL="$${SEQREPO_REST_URL:-http://localhost:5000/seqrepo}" VRS_HGVS_CACHE_MODE=learn pytest --disable-vcr + rm -f tests/data/hgvs_cache.pkl.new + HGVS_SEQREPO_URL="$${SEQREPO_REST_URL:-http://localhost:5000/seqrepo}" VRS_HGVS_CACHE_MODE=learn VRS_HGVS_CACHE_FILE=$(CURDIR)/tests/data/hgvs_cache.pkl.new pytest --disable-vcr + mv tests/data/hgvs_cache.pkl.new tests/data/hgvs_cache.pkl ## From 03a14e22af6ccea6577b7f045a3888394d86706f Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Tue, 6 Oct 2026 14:41:07 -0400 Subject: [PATCH 09/10] test: note that live hgvs cache modes need --disable-vcr --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 8e62e9cf..f1326659 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,6 +63,10 @@ def hgvs_cached_data_provider() -> Iterator[hgvs.dataproviders.uta.UTABase | Non the cache raises `HGVSDataNotAvailableError`. To re-record, run `make record-hgvs-cache` with UTA_DB_URL pointing at a UTA instance and a seqrepo-rest-service running at SEQREPO_REST_URL. + + Learn and verify modes, and an empty mode, query UTA and fetch sequences live. The + VCR cassettes don't contain those hgvs sequence requests (to HGVS_SEQREPO_URL), so + run these modes with `pytest --disable-vcr`. """ mode = os.environ.get("VRS_HGVS_CACHE_MODE", "run") if not mode: From 94bb78a0596abce138428ba1a532704a77c89b65 Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Tue, 6 Oct 2026 16:16:34 -0400 Subject: [PATCH 10/10] test: store the hgvs test cache as JSON --- Makefile | 5 +- README.md | 5 +- tests/conftest.py | 70 +- tests/data/hgvs_cache.json | 1457 ++++++++++++++++++++++++++++++++++++ tests/data/hgvs_cache.pkl | Bin 17169 -> 0 bytes tests/hgvs_cache_json.py | 93 +++ 6 files changed, 1578 insertions(+), 52 deletions(-) create mode 100644 tests/data/hgvs_cache.json delete mode 100644 tests/data/hgvs_cache.pkl create mode 100644 tests/hgvs_cache_json.py diff --git a/Makefile b/Makefile index c3a5db9c..a751e490 100644 --- a/Makefile +++ b/Makefile @@ -128,12 +128,13 @@ cleanest: cleaner clean-cassettes: find ./tests -type f -path '*/cassettes/*.yaml' -print0 | ${XRM} -#=> record-hgvs-cache: re-record tests/data/hgvs_cache.pkl (needs UTA_DB_URL and a running seqrepo-rest-service at SEQREPO_REST_URL; VCR cassettes are not touched) +#=> record-hgvs-cache: re-record tests/data/hgvs_cache.json (needs UTA_DB_URL and a running seqrepo-rest-service at SEQREPO_REST_URL; VCR cassettes are not touched) .PHONY: record-hgvs-cache record-hgvs-cache: rm -f tests/data/hgvs_cache.pkl.new HGVS_SEQREPO_URL="$${SEQREPO_REST_URL:-http://localhost:5000/seqrepo}" VRS_HGVS_CACHE_MODE=learn VRS_HGVS_CACHE_FILE=$(CURDIR)/tests/data/hgvs_cache.pkl.new pytest --disable-vcr - mv tests/data/hgvs_cache.pkl.new tests/data/hgvs_cache.pkl + python tests/hgvs_cache_json.py tests/data/hgvs_cache.pkl.new tests/data/hgvs_cache.json + rm tests/data/hgvs_cache.pkl.new ## diff --git a/README.md b/README.md index ecebeac8..ab6d4287 100644 --- a/README.md +++ b/README.md @@ -324,10 +324,11 @@ service, a UTA database, or network access. Set `SEQREPO_ROOT_DIR=tests/data/seq - **VCR cassettes** (`tests/**/cassettes/*.yaml`) replay vrs-python's HTTP requests to the [SeqRepo REST service](https://github.com/biocommons/seqrepo-rest-service). CI runs `pytest --vcr-record=none`, so a test making an unrecorded request fails. -- **The hgvs cache** (`tests/data/hgvs_cache.pkl`) replays the hgvs library's UTA +- **The hgvs cache** (`tests/data/hgvs_cache.json`) replays the hgvs library's UTA queries and sequence fetches. `tests/conftest.py` enables it by default (`VRS_HGVS_CACHE_MODE=run`), and a lookup missing from the cache raises - `HGVSDataNotAvailableError`. + `HGVSDataNotAvailableError`. hgvs itself reads and writes its cache as a pickle; + `tests/hgvs_cache_json.py` converts between the two. To re-record, start the external services (`docker compose up` starts seqrepo-rest-service on port 5000 and UTA on port 5432). With an existing local SeqRepo diff --git a/tests/conftest.py b/tests/conftest.py index f1326659..fd3d5d9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,68 +1,37 @@ import os -import pickle from collections.abc import Iterator from pathlib import Path -from typing import Any import hgvs.dataproviders.uta import pytest from biocommons.seqrepo import SeqRepo -from hgvs.decorators.lru_cache import _make_key +from hgvs_cache_json import json_to_pickle from ga4gh.vrs.dataproxy import SeqRepoDataProxy, SeqRepoRESTDataProxy HGVS_CACHE_MODES = ("learn", "run", "verify") -HGVS_CACHE_FILE = Path(__file__).parent / "data" / "hgvs_cache.pkl" -# The only classes a recorded hgvs cache file is made of: its keys (_HashedSeq), UTA -# rows (DictRow, with an OrderedDict column index), and what pickle rebuilds them with -HGVS_CACHE_ALLOWED_CLASSES = { - ("builtins", "list"), - ("collections", "OrderedDict"), - ("copyreg", "_reconstructor"), - ("hgvs.decorators.lru_cache", "_HashedSeq"), - ("psycopg2.extras", "DictRow"), -} - - -class HgvsCacheUnpickler(pickle.Unpickler): - """Unpickler that refuses any class not in `HGVS_CACHE_ALLOWED_CLASSES`""" - - def find_class(self, module: str, name: str) -> Any: # noqa: ANN401 - if (module, name) not in HGVS_CACHE_ALLOWED_CLASSES: - msg = f"hgvs cache file references a disallowed class: {module}.{name}" - raise pickle.UnpicklingError(msg) - return super().find_class(module, name) - - -def check_hgvs_cache_file(cache_file: str) -> None: - """Check an hgvs cache file before hgvs loads it with an unrestricted `pickle.load` - - The file may only reference the classes in `HGVS_CACHE_ALLOWED_CLASSES`, and its - keys must be in the format this hgvs version builds. Every data provider looks up - `schema_version` when it's created, so a recorded cache always has that key. - """ - with Path(cache_file).open("rb") as f: - cache: dict = HgvsCacheUnpickler(f).load() # cache keys -> recorded results - if _make_key("schema_version", (), {}, False, ()) not in cache: - msg = ( - f"{cache_file} has no schema_version entry in this hgvs version's cache key " - "format; re-record it with `make record-hgvs-cache`" - ) - raise pytest.UsageError(msg) +HGVS_CACHE_JSON = Path(__file__).parent / "data" / "hgvs_cache.json" @pytest.fixture(scope="session", autouse=True) -def hgvs_cached_data_provider() -> Iterator[hgvs.dataproviders.uta.UTABase | None]: +def hgvs_cached_data_provider( + tmp_path_factory: pytest.TempPathFactory, +) -> Iterator[hgvs.dataproviders.uta.UTABase | None]: """Serve hgvs data provider lookups (UTA queries and hgvs sequence fetches) from a recorded cache, so tests don't need a UTA database or network access. Every `HgvsTools` gets this one data provider, so in learn mode they cannot overwrite each other's cache entries. `VRS_HGVS_CACHE_MODE` is `run` (default), - `learn`, or `verify`, or empty to query UTA directly without the cache, and - `VRS_HGVS_CACHE_FILE` overrides the cache file. In run mode, a lookup missing from - the cache raises `HGVSDataNotAvailableError`. To re-record, run + `learn`, or `verify`, or empty to query UTA directly without the cache. In run + mode, a lookup missing from the cache raises `HGVSDataNotAvailableError`. + + hgvs keeps its cache as a pickle, so in run and verify modes the committed + `tests/data/hgvs_cache.json` is converted to a temporary pickle for hgvs to load + (see `hgvs_cache_json.py`). Learn mode records a new cache, starting empty, into + the pickle file named by `VRS_HGVS_CACHE_FILE`. To re-record, run `make record-hgvs-cache` with UTA_DB_URL pointing at a UTA instance and a - seqrepo-rest-service running at SEQREPO_REST_URL. + seqrepo-rest-service running at SEQREPO_REST_URL; it converts the recorded pickle + to `tests/data/hgvs_cache.json`. Learn and verify modes, and an empty mode, query UTA and fetch sequences live. The VCR cassettes don't contain those hgvs sequence requests (to HGVS_SEQREPO_URL), so @@ -75,9 +44,14 @@ def hgvs_cached_data_provider() -> Iterator[hgvs.dataproviders.uta.UTABase | Non if mode not in HGVS_CACHE_MODES: msg = f"VRS_HGVS_CACHE_MODE must be one of {HGVS_CACHE_MODES} or empty, got {mode!r}" raise pytest.UsageError(msg) - cache_file = os.environ.get("VRS_HGVS_CACHE_FILE") or str(HGVS_CACHE_FILE) - if Path(cache_file).exists(): - check_hgvs_cache_file(cache_file) + if mode == "learn": + cache_file = os.environ.get("VRS_HGVS_CACHE_FILE") + if not cache_file: + msg = "VRS_HGVS_CACHE_FILE must name the pickle file to record into in learn mode" + raise pytest.UsageError(msg) + else: + cache_file = str(tmp_path_factory.mktemp("hgvs_cache") / "hgvs_cache.pkl") + json_to_pickle(HGVS_CACHE_JSON, Path(cache_file)) provider = hgvs.dataproviders.uta.connect(mode=mode, cache=cache_file) with pytest.MonkeyPatch.context() as mp: diff --git a/tests/data/hgvs_cache.json b/tests/data/hgvs_cache.json new file mode 100644 index 00000000..878d4239 --- /dev/null +++ b/tests/data/hgvs_cache.json @@ -0,0 +1,1457 @@ +[ + { + "func": "get_seq", + "args": [ + "NC_000007.14", + 55181219, + 55181220 + ], + "value": "T" + }, + { + "func": "get_seq", + "args": [ + "NC_000007.14", + 55181219, + 55181240 + ], + "value": "TGGAAGGGGTCCATGTGCCCC" + }, + { + "func": "get_seq", + "args": [ + "NC_000007.14", + 55181226, + 55181230 + ], + "value": "GGTC" + }, + { + "func": "get_seq", + "args": [ + "NC_000007.14", + 55181229, + 55181250 + ], + "value": "CCATGTGCCCCTCCTTCTGGC" + }, + { + "func": "get_seq", + "args": [ + "NC_000007.14", + 55181230, + 55181231 + ], + "value": "C" + }, + { + "func": "get_seq", + "args": [ + "NC_000007.14", + 55181319, + 55181320 + ], + "value": "A" + }, + { + "func": "get_seq", + "args": [ + "NC_000007.14", + 55181319, + 55181340 + ], + "value": "AACCCCCACGTGTGCCGCCTG" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32316466, + 32316467 + ], + "value": "A" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32316466, + 32316487 + ], + "value": "ATTGGATCCAAAGAGAGGCCA" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32331042, + 32331094 + ], + "value": "TTTAGTTGAACTACAGGTTTTTTTGTTGTTGTTGTTTTGATTTTTTTTTTTT" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32331042, + 32331114 + ], + "value": "TTTAGTTGAACTACAGGTTTTTTTGTTGTTGTTGTTTTGATTTTTTTTTTTTGAGGTGGAGTCTTGCTCTGT" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32331082, + 32331094 + ], + "value": "TTTTTTTTTTTT" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32331082, + 32331114 + ], + "value": "TTTTTTTTTTTTGAGGTGGAGTCTTGCTCTGT" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32331092, + 32331094 + ], + "value": "TT" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32331093, + 32331094 + ], + "value": "T" + }, + { + "func": "get_seq", + "args": [ + "NC_000013.11", + 32936731, + 32936732 + ], + "value": "C" + }, + { + "func": "get_seq", + "args": [ + "NC_000019.10", + 289462, + 289466 + ], + "value": "CTCA" + }, + { + "func": "get_seq", + "args": [ + "NC_000019.10", + 289464, + 289466 + ], + "value": "CA" + }, + { + "func": "get_seq", + "args": [ + "NC_000019.10", + 289464, + 289486 + ], + "value": "CAGCACTTTGGGAGGCCGAGGC" + }, + { + "func": "get_seq", + "args": [ + "NC_000019.10", + 289465, + 289466 + ], + "value": "A" + }, + { + "func": "get_seq", + "args": [ + "NC_000019.10", + 289480, + 289501 + ], + "value": "CGAGGCGGGCAGATCACGAGG" + }, + { + "func": "get_seq", + "args": [ + "NC_000019.10", + 289480, + 289521 + ], + "value": "CGAGGCGGGCAGATCACGAGGTCAGGAGATCGAGACCATCC" + }, + { + "func": "get_seq", + "args": [ + "NC_000019.10", + 289500, + 289501 + ], + "value": "G" + }, + { + "func": "get_seq", + "args": [ + "NM_001331029.1", + 871, + 872 + ], + "value": "A" + }, + { + "func": "get_seq", + "args": [ + "NM_001331029.1", + 871, + 892 + ], + "value": "ATGAACTCAATGTTCAGGATT" + }, + { + "func": "get_seq", + "args": [ + "NM_181798.1", + 1262, + 1263 + ], + "value": "G" + }, + { + "func": "get_seq", + "args": [ + "NM_181798.1", + 1262, + 1268 + ], + "value": "GTTCTG" + }, + { + "func": "get_seq", + "args": [ + "NR_040711.1", + 1262, + 1263 + ], + "value": "G" + }, + { + "func": "get_seq", + "args": [ + "XM_017001344.1", + 871, + 872 + ], + "value": "A" + }, + { + "func": "get_tx_exons", + "args": [ + "NM_001331029.1", + "NC_000001.10", + "splign" + ], + "value": [ + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 0, + "tx_start_i": 0, + "tx_end_i": 441, + "alt_start_i": 202317829, + "alt_end_i": 202318270, + "cigar": "441=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895004, + "alt_exon_id": 6911773, + "exon_aln_id": 4375364 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 1, + "tx_start_i": 441, + "tx_end_i": 572, + "alt_start_i": 202385914, + "alt_end_i": 202386045, + "cigar": "131=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895005, + "alt_exon_id": 6911774, + "exon_aln_id": 4375329 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 2, + "tx_start_i": 572, + "tx_end_i": 691, + "alt_start_i": 202391747, + "alt_end_i": 202391866, + "cigar": "119=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895006, + "alt_exon_id": 6911775, + "exon_aln_id": 4375326 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 3, + "tx_start_i": 691, + "tx_end_i": 851, + "alt_start_i": 202394693, + "alt_end_i": 202394853, + "cigar": "160=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895007, + "alt_exon_id": 6911776, + "exon_aln_id": 4375339 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 4, + "tx_start_i": 851, + "tx_end_i": 996, + "alt_start_i": 202396167, + "alt_end_i": 202396312, + "cigar": "145=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895008, + "alt_exon_id": 6911777, + "exon_aln_id": 4375320 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 5, + "tx_start_i": 996, + "tx_end_i": 1071, + "alt_start_i": 202397982, + "alt_end_i": 202398057, + "cigar": "75=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895009, + "alt_exon_id": 6911778, + "exon_aln_id": 4375342 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 6, + "tx_start_i": 1071, + "tx_end_i": 1151, + "alt_start_i": 202399858, + "alt_end_i": 202399938, + "cigar": "80=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895010, + "alt_exon_id": 6911779, + "exon_aln_id": 4375348 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 7, + "tx_start_i": 1151, + "tx_end_i": 1291, + "alt_start_i": 202400607, + "alt_end_i": 202400747, + "cigar": "140=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895011, + "alt_exon_id": 6911780, + "exon_aln_id": 4375321 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 8, + "tx_start_i": 1291, + "tx_end_i": 1404, + "alt_start_i": 202403783, + "alt_end_i": 202403896, + "cigar": "113=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895012, + "alt_exon_id": 6911781, + "exon_aln_id": 4375354 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 9, + "tx_start_i": 1404, + "tx_end_i": 1608, + "alt_start_i": 202406948, + "alt_end_i": 202407152, + "cigar": "204=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895013, + "alt_exon_id": 6911782, + "exon_aln_id": 4375356 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 10, + "tx_start_i": 1608, + "tx_end_i": 1691, + "alt_start_i": 202409833, + "alt_end_i": 202409916, + "cigar": "83=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895014, + "alt_exon_id": 6911783, + "exon_aln_id": 4375355 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 11, + "tx_start_i": 1691, + "tx_end_i": 1817, + "alt_start_i": 202411574, + "alt_end_i": 202411700, + "cigar": "126=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895015, + "alt_exon_id": 6911784, + "exon_aln_id": 4375357 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 12, + "tx_start_i": 1817, + "tx_end_i": 2000, + "alt_start_i": 202414173, + "alt_end_i": 202414356, + "cigar": "183=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895016, + "alt_exon_id": 6911785, + "exon_aln_id": 4375347 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 13, + "tx_start_i": 2000, + "tx_end_i": 2183, + "alt_start_i": 202418116, + "alt_end_i": 202418299, + "cigar": "183=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895017, + "alt_exon_id": 6911786, + "exon_aln_id": 4375333 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 14, + "tx_start_i": 2183, + "tx_end_i": 2274, + "alt_start_i": 202457660, + "alt_end_i": 202457751, + "cigar": "91=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895018, + "alt_exon_id": 6911787, + "exon_aln_id": 4375343 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 15, + "tx_start_i": 2274, + "tx_end_i": 2478, + "alt_start_i": 202462241, + "alt_end_i": 202462445, + "cigar": "204=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895019, + "alt_exon_id": 6911788, + "exon_aln_id": 4375336 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 16, + "tx_start_i": 2478, + "tx_end_i": 2668, + "alt_start_i": 202464420, + "alt_end_i": 202464610, + "cigar": "190=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895020, + "alt_exon_id": 6911789, + "exon_aln_id": 4375334 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 17, + "tx_start_i": 2668, + "tx_end_i": 2781, + "alt_start_i": 202464697, + "alt_end_i": 202464810, + "cigar": "113=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895021, + "alt_exon_id": 6911790, + "exon_aln_id": 4375322 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 18, + "tx_start_i": 2781, + "tx_end_i": 2823, + "alt_start_i": 202465908, + "alt_end_i": 202465950, + "cigar": "42=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895022, + "alt_exon_id": 6911791, + "exon_aln_id": 4375330 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 19, + "tx_start_i": 2823, + "tx_end_i": 2840, + "alt_start_i": 202528004, + "alt_end_i": 202528021, + "cigar": "17=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895023, + "alt_exon_id": 6911792, + "exon_aln_id": 4375360 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 20, + "tx_start_i": 2840, + "tx_end_i": 2985, + "alt_start_i": 202531905, + "alt_end_i": 202532050, + "cigar": "145=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895024, + "alt_exon_id": 6911793, + "exon_aln_id": 4375335 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 21, + "tx_start_i": 2985, + "tx_end_i": 3090, + "alt_start_i": 202533570, + "alt_end_i": 202533675, + "cigar": "105=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895025, + "alt_exon_id": 6911794, + "exon_aln_id": 4375323 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 22, + "tx_start_i": 3090, + "tx_end_i": 3144, + "alt_start_i": 202536905, + "alt_end_i": 202536959, + "cigar": "54=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895026, + "alt_exon_id": 6911795, + "exon_aln_id": 4375325 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 23, + "tx_start_i": 3144, + "tx_end_i": 3195, + "alt_start_i": 202538274, + "alt_end_i": 202538325, + "cigar": "51=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895027, + "alt_exon_id": 6911796, + "exon_aln_id": 4375346 + } + }, + { + "psycopg2_dictrow": { + "symbol": "PPP1R12B", + "hgnc": "PPP1R12B", + "gene_id": "4660", + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 24, + "tx_start_i": 3195, + "tx_end_i": 11291, + "alt_start_i": 202549601, + "alt_end_i": 202557697, + "cigar": "8096=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 808849, + "alt_exon_set_id": 810341, + "tx_exon_id": 6895028, + "alt_exon_id": 6911797, + "exon_aln_id": 4375324 + } + } + ] + }, + { + "func": "get_tx_exons", + "args": [ + "NM_181798.1", + "NC_000011.10", + "splign" + ], + "value": [ + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 0, + "tx_start_i": 0, + "tx_end_i": 261, + "alt_start_i": 2461453, + "alt_end_i": 2461714, + "cigar": "261=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773599, + "alt_exon_id": 6791722, + "exon_aln_id": 3977791 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 1, + "tx_start_i": 261, + "tx_end_i": 352, + "alt_start_i": 2527927, + "alt_end_i": 2528018, + "cigar": "91=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773600, + "alt_exon_id": 6791723, + "exon_aln_id": 3977803 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 2, + "tx_start_i": 352, + "tx_end_i": 479, + "alt_start_i": 2570627, + "alt_end_i": 2570754, + "cigar": "127=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773601, + "alt_exon_id": 6791724, + "exon_aln_id": 3977799 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 3, + "tx_start_i": 479, + "tx_end_i": 558, + "alt_start_i": 2571324, + "alt_end_i": 2571403, + "cigar": "79=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773602, + "alt_exon_id": 6791725, + "exon_aln_id": 3977778 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 4, + "tx_start_i": 558, + "tx_end_i": 655, + "alt_start_i": 2572012, + "alt_end_i": 2572109, + "cigar": "97=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773603, + "alt_exon_id": 6791726, + "exon_aln_id": 3977805 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 5, + "tx_start_i": 655, + "tx_end_i": 796, + "alt_start_i": 2572845, + "alt_end_i": 2572986, + "cigar": "141=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773604, + "alt_exon_id": 6791727, + "exon_aln_id": 3977802 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 6, + "tx_start_i": 796, + "tx_end_i": 907, + "alt_start_i": 2583434, + "alt_end_i": 2583545, + "cigar": "111=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773605, + "alt_exon_id": 6791728, + "exon_aln_id": 3977793 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 7, + "tx_start_i": 907, + "tx_end_i": 1003, + "alt_start_i": 2585211, + "alt_end_i": 2585307, + "cigar": "96=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773606, + "alt_exon_id": 6791729, + "exon_aln_id": 3977779 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 8, + "tx_start_i": 1003, + "tx_end_i": 1126, + "alt_start_i": 2587569, + "alt_end_i": 2587692, + "cigar": "123=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773607, + "alt_exon_id": 6791730, + "exon_aln_id": 3977809 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 9, + "tx_start_i": 1126, + "tx_end_i": 1268, + "alt_start_i": 2588712, + "alt_end_i": 2588854, + "cigar": "142=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773608, + "alt_exon_id": 6791731, + "exon_aln_id": 3977801 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 10, + "tx_start_i": 1268, + "tx_end_i": 1389, + "alt_start_i": 2661960, + "alt_end_i": 2662081, + "cigar": "121=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773609, + "alt_exon_id": 6791732, + "exon_aln_id": 3977804 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 11, + "tx_start_i": 1389, + "tx_end_i": 1465, + "alt_start_i": 2768843, + "alt_end_i": 2768919, + "cigar": "76=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773610, + "alt_exon_id": 6791733, + "exon_aln_id": 3977796 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 12, + "tx_start_i": 1465, + "tx_end_i": 1560, + "alt_start_i": 2775959, + "alt_end_i": 2776054, + "cigar": "95=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773611, + "alt_exon_id": 6791734, + "exon_aln_id": 3977784 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 13, + "tx_start_i": 1560, + "tx_end_i": 1607, + "alt_start_i": 2776985, + "alt_end_i": 2777032, + "cigar": "47=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773612, + "alt_exon_id": 6791735, + "exon_aln_id": 3977780 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 14, + "tx_start_i": 1607, + "tx_end_i": 1669, + "alt_start_i": 2777975, + "alt_end_i": 2778037, + "cigar": "62=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773613, + "alt_exon_id": 6791736, + "exon_aln_id": 3977783 + } + }, + { + "psycopg2_dictrow": { + "symbol": "KCNQ1", + "hgnc": "KCNQ1", + "gene_id": "3784", + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign", + "alt_strand": 1, + "ord": 15, + "tx_start_i": 1669, + "tx_end_i": 3013, + "alt_start_i": 2847766, + "alt_end_i": 2849110, + "cigar": "1344=", + "tx_aseq": null, + "alt_aseq": null, + "tx_exon_set_id": 83331, + "alt_exon_set_id": 792010, + "tx_exon_id": 773614, + "alt_exon_id": 6791737, + "exon_aln_id": 3977787 + } + } + ] + }, + { + "func": "get_tx_identity_info", + "args": [ + "NM_001331029.1" + ], + "value": { + "psycopg2_dictrow": { + "tx_ac": "NM_001331029.1", + "alt_ac": "NM_001331029.1", + "alt_aln_method": "transcript", + "cds_start_i": 150, + "cds_end_i": 3282, + "lengths": [ + 441, + 131, + 119, + 160, + 145, + 75, + 80, + 140, + 113, + 204, + 83, + 126, + 183, + 183, + 91, + 204, + 190, + 113, + 42, + 17, + 145, + 105, + 54, + 51, + 8096 + ], + "hgnc": "PPP1R12B" + } + } + }, + { + "func": "get_tx_identity_info", + "args": [ + "NM_181798.1" + ], + "value": { + "psycopg2_dictrow": { + "tx_ac": "NM_181798.1", + "alt_ac": "NM_181798.1", + "alt_aln_method": "transcript", + "cds_start_i": 256, + "cds_end_i": 1906, + "lengths": [ + 261, + 91, + 127, + 79, + 97, + 141, + 111, + 96, + 123, + 142, + 121, + 76, + 95, + 47, + 62, + 1344 + ], + "hgnc": "KCNQ1" + } + } + }, + { + "func": "get_tx_info", + "args": [ + "NM_001331029.1", + "NC_000001.10", + "splign" + ], + "value": { + "psycopg2_dictrow": { + "hgnc": "PPP1R12B", + "cds_start_i": 150, + "cds_end_i": 3282, + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign" + } + } + }, + { + "func": "get_tx_info", + "args": [ + "NM_181798.1", + "NC_000011.10", + "splign" + ], + "value": { + "psycopg2_dictrow": { + "hgnc": "KCNQ1", + "cds_start_i": 256, + "cds_end_i": 1906, + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign" + } + } + }, + { + "func": "get_tx_mapping_options", + "args": [ + "NM_001331029.1" + ], + "value": [ + { + "psycopg2_dictrow": { + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.10", + "alt_aln_method": "splign" + } + }, + { + "psycopg2_dictrow": { + "tx_ac": "NM_001331029.1", + "alt_ac": "NC_000001.11", + "alt_aln_method": "splign" + } + } + ] + }, + { + "func": "get_tx_mapping_options", + "args": [ + "NM_181798.1" + ], + "value": [ + { + "psycopg2_dictrow": { + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.10", + "alt_aln_method": "splign" + } + }, + { + "psycopg2_dictrow": { + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.9", + "alt_aln_method": "blat" + } + }, + { + "psycopg2_dictrow": { + "tx_ac": "NM_181798.1", + "alt_ac": "NC_000011.9", + "alt_aln_method": "splign" + } + }, + { + "psycopg2_dictrow": { + "tx_ac": "NM_181798.1", + "alt_ac": "NG_008935.1", + "alt_aln_method": "splign" + } + } + ] + }, + { + "func": "get_tx_mapping_options", + "args": [ + "NR_040711.1" + ], + "value": [] + }, + { + "func": "get_tx_mapping_options", + "args": [ + "XM_017001344.1" + ], + "value": [] + }, + { + "func": "schema_version", + "args": [], + "value": "1.2" + } +] diff --git a/tests/data/hgvs_cache.pkl b/tests/data/hgvs_cache.pkl deleted file mode 100644 index 5ee7e7eb3d3cdc0ea65efd766dfa8a6cf0e229ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17169 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 EUq4F6 `` +""" + +import json +import pickle +import sys +from collections import OrderedDict +from pathlib import Path +from typing import Any + +from hgvs.decorators.lru_cache import _make_key +from psycopg2.extras import DictRow + +# hgvs builds each cache key as (*args, "__func__", function name) +_FUNC_MARKER = "__func__" +_DICTROW_TAG = "psycopg2_dictrow" +_JSON_SCALARS = (str, int, float, bool, type(None)) + + +def _encode_value(value: Any) -> Any: # noqa: ANN401 + """Convert a cached hgvs result to a JSON value""" + if isinstance(value, DictRow): + return {_DICTROW_TAG: dict(value.items())} + if isinstance(value, list): + return [_encode_value(v) for v in value] + if isinstance(value, _JSON_SCALARS): + return value + msg = f"can't store a {type(value).__name__} in the hgvs JSON cache: {value!r}" + raise TypeError(msg) + + +def _decode_value(value: Any) -> Any: # noqa: ANN401 + """Convert a JSON value back to the cached hgvs result, rebuilding UTA rows""" + if isinstance(value, dict): + if value.keys() != {_DICTROW_TAG}: + msg = f'expected {{"{_DICTROW_TAG}": {{...}}}}, got {value!r}' + raise ValueError(msg) + columns: dict[str, Any] = value[_DICTROW_TAG] + row = DictRow.__new__(DictRow) + index = OrderedDict((column, i) for i, column in enumerate(columns)) + row.__setstate__((list(columns.values()), index)) + return row + if isinstance(value, list): + return [_decode_value(v) for v in value] + return value + + +def pickle_to_json(pickle_file: Path, json_file: Path) -> None: + """Write the hgvs cache pickle `pickle_file` as JSON to `json_file` + + Only use this on a cache hgvs just recorded: loading a pickle can run code. + """ + with pickle_file.open("rb") as f: + cache: dict = pickle.load(f) # noqa: S301 # hgvs cache key -> cached result + entries = [] + for key in cache: + *args, marker, func = key + if marker != _FUNC_MARKER: + msg = f"unexpected hgvs cache key layout: {key!r}" + raise ValueError(msg) + entries.append({"func": func, "args": args, "value": _encode_value(cache[key])}) + entries.sort(key=lambda e: (e["func"], json.dumps(e["args"]))) + json_file.write_text(json.dumps(entries, indent=2) + "\n") + + +def json_to_pickle(json_file: Path, pickle_file: Path) -> None: + """Write the JSON hgvs cache `json_file` as the pickle hgvs loads to `pickle_file`""" + entries: list[dict[str, Any]] = json.loads(json_file.read_text()) + cache = {} + for e in entries: + try: + value = _decode_value(e["value"]) + except ValueError as err: + msg = f"{json_file}: {e['func']} {e['args']}: {err}" + raise ValueError(msg) from err + cache[_make_key(e["func"], tuple(e["args"]), {}, False, ())] = value + with pickle_file.open("wb") as f: + pickle.dump(cache, f) + + +if __name__ == "__main__": + pickle_to_json(Path(sys.argv[1]), Path(sys.argv[2]))