gnomad-api-cache 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gnomad_api_cache/__init__.py +35 -0
- gnomad_api_cache/__main__.py +8 -0
- gnomad_api_cache/_utils.py +14 -0
- gnomad_api_cache/adapters/__init__.py +0 -0
- gnomad_api_cache/adapters/vcf_adapter.py +147 -0
- gnomad_api_cache/cache.py +328 -0
- gnomad_api_cache/cli.py +284 -0
- gnomad_api_cache/client.py +20 -0
- gnomad_api_cache/export.py +319 -0
- gnomad_api_cache/fetch.py +149 -0
- gnomad_api_cache/keys.py +78 -0
- gnomad_api_cache/query.py +305 -0
- gnomad_api_cache-0.1.0.dist-info/METADATA +115 -0
- gnomad_api_cache-0.1.0.dist-info/RECORD +17 -0
- gnomad_api_cache-0.1.0.dist-info/WHEEL +4 -0
- gnomad_api_cache-0.1.0.dist-info/entry_points.txt +2 -0
- gnomad_api_cache-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Fetch and cache gnomAD annotations for VCF variants.
|
|
2
|
+
|
|
3
|
+
from gnomad_api_cache import VariantCache, read_vcf
|
|
4
|
+
|
|
5
|
+
with VariantCache("gnomad.sqlite") as cache:
|
|
6
|
+
print(cache.fetch_vcf("cohort.vcf.gz"))
|
|
7
|
+
cache.to_parquet("annotations.parquet")
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from gnomad_api_cache.adapters.vcf_adapter import (
|
|
13
|
+
BuildMismatchError,
|
|
14
|
+
iter_variant_keys,
|
|
15
|
+
read_vcf,
|
|
16
|
+
)
|
|
17
|
+
from gnomad_api_cache.cache import VariantCache
|
|
18
|
+
from gnomad_api_cache.fetch import FetchSummary, fetch_gnomad
|
|
19
|
+
from gnomad_api_cache.keys import InvalidVariantError, VariantKey
|
|
20
|
+
from gnomad_api_cache.query import DEFAULT_DATASET, QUERY_VERSION
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DEFAULT_DATASET",
|
|
26
|
+
"QUERY_VERSION",
|
|
27
|
+
"BuildMismatchError",
|
|
28
|
+
"FetchSummary",
|
|
29
|
+
"InvalidVariantError",
|
|
30
|
+
"VariantCache",
|
|
31
|
+
"VariantKey",
|
|
32
|
+
"fetch_gnomad",
|
|
33
|
+
"iter_variant_keys",
|
|
34
|
+
"read_vcf",
|
|
35
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator, Sequence
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _now() -> str:
|
|
9
|
+
return datetime.now(UTC).isoformat(timespec="seconds")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _chunked(items: Sequence[Any], size: int) -> Iterator[Sequence[Any]]:
|
|
13
|
+
for i in range(0, len(items), size):
|
|
14
|
+
yield items[i : i + size]
|
|
File without changes
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Read a VCF into VariantKey objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from collections.abc import Callable, Iterator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from cyvcf2 import VCF, Variant
|
|
11
|
+
|
|
12
|
+
from gnomad_api_cache.keys import (
|
|
13
|
+
CANONICAL_CHROMS,
|
|
14
|
+
InvalidVariantError,
|
|
15
|
+
VariantKey,
|
|
16
|
+
normalize_chrom,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Use chr1 to detect build
|
|
22
|
+
CHR1_LENGTH_BY_BUILD = {248956422: "GRCh38", 249250621: "GRCh37"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BuildMismatchError(RuntimeError):
|
|
26
|
+
"""Raised when a VCF is not on the build the target gnomAD dataset uses."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def detect_build(vcf: VCF) -> str | None:
|
|
30
|
+
"""Return "GRCh38", "GRCh37", or None if the header doesn't say."""
|
|
31
|
+
lengths: dict[str, int] = dict(zip(vcf.seqnames, vcf.seqlens))
|
|
32
|
+
chr1 = lengths.get("chr1") or lengths.get("1")
|
|
33
|
+
if chr1 is None:
|
|
34
|
+
return None
|
|
35
|
+
return CHR1_LENGTH_BY_BUILD.get(chr1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_normalized(vcf: VCF) -> bool:
|
|
39
|
+
"""True if the header shows bcftools norm was run."""
|
|
40
|
+
return any(
|
|
41
|
+
line.startswith("##bcftools_norm") for line in vcf.raw_header.split("\n")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _skip_reason(alt: str, chrom: str) -> str | None:
|
|
46
|
+
"""Return why this allele can't become a gnomAD ID, or None if it can."""
|
|
47
|
+
if alt.startswith("<"):
|
|
48
|
+
return "symbolic" # <DEL>, <NON_REF>, <*>
|
|
49
|
+
if "[" in alt or "]" in alt:
|
|
50
|
+
return "breakend"
|
|
51
|
+
if alt == "*":
|
|
52
|
+
return "spanning_deletion"
|
|
53
|
+
if normalize_chrom(chrom) not in CANONICAL_CHROMS:
|
|
54
|
+
return "non_canonical_contig" # *_random, chrUn_*, HLA-*
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def iter_variant_keys(
|
|
59
|
+
path: str | Path,
|
|
60
|
+
*,
|
|
61
|
+
require_build: str | None = "GRCh38",
|
|
62
|
+
warn_unnormalized: bool = True,
|
|
63
|
+
filter_function: Callable[[Variant], bool] | None = None,
|
|
64
|
+
) -> Iterator[VariantKey]:
|
|
65
|
+
"""Yield one VariantKey per ALT allele of every record in `path`."""
|
|
66
|
+
vcf = VCF(str(path))
|
|
67
|
+
|
|
68
|
+
build = detect_build(vcf)
|
|
69
|
+
if require_build is not None:
|
|
70
|
+
if build is None:
|
|
71
|
+
log.warning(
|
|
72
|
+
"%s: could not determine build from header; assuming %s",
|
|
73
|
+
path,
|
|
74
|
+
require_build,
|
|
75
|
+
)
|
|
76
|
+
elif build != require_build:
|
|
77
|
+
raise BuildMismatchError(
|
|
78
|
+
f"{path}: VCF is {build}, expected {require_build}. "
|
|
79
|
+
+ "Lift over first, or target a matching gnomAD dataset."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if warn_unnormalized and not is_normalized(vcf):
|
|
83
|
+
log.warning(
|
|
84
|
+
"%s: no ##bcftools_norm header. Unnormalized indels will silently "
|
|
85
|
+
+ "miss in gnomAD. Run: bcftools norm -f <ref.fa> -m -any",
|
|
86
|
+
path,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
skipped: Counter[str] = Counter()
|
|
90
|
+
yielded = 0
|
|
91
|
+
|
|
92
|
+
# main loop
|
|
93
|
+
for record in vcf:
|
|
94
|
+
if filter_function is not None and not filter_function(record):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
for alt in record.ALT: # empty list when ALT is "."
|
|
98
|
+
reason = _skip_reason(alt, record.CHROM)
|
|
99
|
+
if reason is not None:
|
|
100
|
+
skipped[reason] += 1
|
|
101
|
+
continue
|
|
102
|
+
try:
|
|
103
|
+
key = VariantKey.from_parts(record.CHROM, record.POS, record.REF, alt)
|
|
104
|
+
except InvalidVariantError as e:
|
|
105
|
+
skipped["invalid"] += 1
|
|
106
|
+
log.debug(
|
|
107
|
+
"skipping %s:%s %s>%s -- %s",
|
|
108
|
+
record.CHROM,
|
|
109
|
+
record.POS,
|
|
110
|
+
record.REF,
|
|
111
|
+
alt,
|
|
112
|
+
e,
|
|
113
|
+
)
|
|
114
|
+
continue
|
|
115
|
+
yielded += 1
|
|
116
|
+
yield key
|
|
117
|
+
|
|
118
|
+
vcf.close()
|
|
119
|
+
|
|
120
|
+
if skipped:
|
|
121
|
+
log.info(
|
|
122
|
+
"%s: %d keys, %d alleles skipped (%s)",
|
|
123
|
+
path,
|
|
124
|
+
yielded,
|
|
125
|
+
sum(skipped.values()),
|
|
126
|
+
", ".join(f"{k}={v}" for k, v in sorted(skipped.items())),
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
log.info("%s: %d keys", path, yielded)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def read_vcf(
|
|
133
|
+
path: str | Path,
|
|
134
|
+
*,
|
|
135
|
+
require_build: str | None = "GRCh38",
|
|
136
|
+
warn_unnormalized: bool = True,
|
|
137
|
+
filter_function: Callable[[Variant], bool] | None = None,
|
|
138
|
+
) -> list[VariantKey]:
|
|
139
|
+
"""Eager convenience wrapper around iter_variant_keys."""
|
|
140
|
+
return list(
|
|
141
|
+
iter_variant_keys(
|
|
142
|
+
path,
|
|
143
|
+
require_build=require_build,
|
|
144
|
+
warn_unnormalized=warn_unnormalized,
|
|
145
|
+
filter_function=filter_function
|
|
146
|
+
)
|
|
147
|
+
)
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""SQLite-backed cache of gnomAD records.
|
|
2
|
+
|
|
3
|
+
Stores each API response verbatim as compressed JSON, keyed by variant id and
|
|
4
|
+
dataset.
|
|
5
|
+
|
|
6
|
+
Reads behave like a dict:
|
|
7
|
+
|
|
8
|
+
with VariantCache("gnomad.sqlite") as cache:
|
|
9
|
+
record = cache["1-55039974-G-T"] # -> dict, or None if not in gnomAD
|
|
10
|
+
"1-55039974-G-T" in cache # -> have we ever asked?
|
|
11
|
+
len(cache)
|
|
12
|
+
|
|
13
|
+
Three columns carry the bookkeeping the fetch loop depends on:
|
|
14
|
+
|
|
15
|
+
status 'found' | 'not_found' | 'error'. Caching a miss is what stops
|
|
16
|
+
absent variants from being re-queried on every run; 'error' is
|
|
17
|
+
kept distinct so transient failures can be retried without
|
|
18
|
+
being mistaken for real absences.
|
|
19
|
+
query_version which version of the GraphQL selection produced this row, so a
|
|
20
|
+
widened query re-fetches only the rows that predate it.
|
|
21
|
+
fetched_at provenance, and a basis for staleness policies later.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import sqlite3
|
|
28
|
+
import zlib
|
|
29
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
32
|
+
from cyvcf2 import Variant
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from gnomad_api_cache.fetch import FetchSummary
|
|
36
|
+
|
|
37
|
+
from gnomad_api_cache._utils import _chunked, _now
|
|
38
|
+
from gnomad_api_cache.keys import VariantKey
|
|
39
|
+
from gnomad_api_cache.query import DEFAULT_DATASET, QUERY_VERSION
|
|
40
|
+
|
|
41
|
+
# zlib level 6 measured ~8.1x on real gnomAD records (21.4 KB -> 2.6 KB mean)
|
|
42
|
+
# at ~0.3 ms/record. Level 9 buys 2% more for 2x the time.
|
|
43
|
+
COMPRESSION_LEVEL = 6
|
|
44
|
+
|
|
45
|
+
# Stay under SQLite's bound-parameter limit when building "IN (?, ?, ...)".
|
|
46
|
+
_SQL_CHUNK = 900
|
|
47
|
+
|
|
48
|
+
STATUS_FOUND = "found"
|
|
49
|
+
STATUS_NOT_FOUND = "not_found"
|
|
50
|
+
STATUS_ERROR = "error"
|
|
51
|
+
|
|
52
|
+
_SCHEMA = """
|
|
53
|
+
CREATE TABLE IF NOT EXISTS variants (
|
|
54
|
+
variant_id TEXT NOT NULL,
|
|
55
|
+
dataset TEXT NOT NULL,
|
|
56
|
+
status TEXT NOT NULL,
|
|
57
|
+
data BLOB,
|
|
58
|
+
fetched_at TEXT NOT NULL,
|
|
59
|
+
query_version INTEGER NOT NULL,
|
|
60
|
+
PRIMARY KEY (variant_id, dataset)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
64
|
+
key TEXT PRIMARY KEY,
|
|
65
|
+
value TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class VariantCache(Mapping[str, "dict[str, Any] | None"]):
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
path: str | Path,
|
|
74
|
+
dataset: str = DEFAULT_DATASET,
|
|
75
|
+
query_version: int = QUERY_VERSION,
|
|
76
|
+
) -> None:
|
|
77
|
+
self.path = str(path)
|
|
78
|
+
self.dataset = dataset
|
|
79
|
+
self.query_version = query_version
|
|
80
|
+
self.db = sqlite3.connect(self.path)
|
|
81
|
+
# WAL lets readers work while a fetch is writing. synchronous=NORMAL is
|
|
82
|
+
# safe under WAL for this use: a crash can lose the last commit, which
|
|
83
|
+
# costs at most one batch that the next run simply re-fetches.
|
|
84
|
+
self.db.execute("PRAGMA journal_mode=WAL")
|
|
85
|
+
self.db.execute("PRAGMA synchronous=NORMAL")
|
|
86
|
+
self.db.executescript(_SCHEMA)
|
|
87
|
+
self.db.commit()
|
|
88
|
+
|
|
89
|
+
# --- lifecycle --------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def close(self) -> None:
|
|
92
|
+
self.db.commit()
|
|
93
|
+
self.db.close()
|
|
94
|
+
|
|
95
|
+
def __enter__(self) -> Self:
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def __exit__(self, *exc: object) -> None:
|
|
99
|
+
self.close()
|
|
100
|
+
|
|
101
|
+
def commit(self) -> None:
|
|
102
|
+
self.db.commit()
|
|
103
|
+
|
|
104
|
+
# --- what the fetch loop calls ---------------------------------------
|
|
105
|
+
|
|
106
|
+
def needs_query(
|
|
107
|
+
self,
|
|
108
|
+
keys: Sequence[VariantKey],
|
|
109
|
+
retry_errors: bool = True,
|
|
110
|
+
retry_not_found: bool = False,
|
|
111
|
+
) -> list[VariantKey]:
|
|
112
|
+
"""Return the subset of `keys` that must be fetched from gnomAD.
|
|
113
|
+
|
|
114
|
+
A key is skipped only when a row exists AND it was written by the
|
|
115
|
+
current query_version AND its status is a real answer. Anything older
|
|
116
|
+
or missing comes back for re-fetch.
|
|
117
|
+
|
|
118
|
+
The two retry flags cover the two ways a key can be in the cache
|
|
119
|
+
without usable data, which want opposite defaults:
|
|
120
|
+
|
|
121
|
+
retry_errors we never got an answer (network, bad response).
|
|
122
|
+
Usually transient, so retried by default -- leaving
|
|
123
|
+
these out would let a run report itself complete
|
|
124
|
+
while carrying silent holes.
|
|
125
|
+
retry_not_found gnomAD answered "no such variant". A real answer, so
|
|
126
|
+
skipped by default; set True to re-check after a
|
|
127
|
+
gnomAD release adds variants.
|
|
128
|
+
|
|
129
|
+
Input order is preserved and duplicates are dropped, so the caller can
|
|
130
|
+
pass a raw VCF-derived list straight in.
|
|
131
|
+
"""
|
|
132
|
+
# Deduplicate up front
|
|
133
|
+
unique: dict[str, VariantKey] = {}
|
|
134
|
+
for key in keys:
|
|
135
|
+
unique.setdefault(key.id, key)
|
|
136
|
+
|
|
137
|
+
fresh: set[str] = set()
|
|
138
|
+
ids = list(unique)
|
|
139
|
+
for chunk in _chunked(ids, _SQL_CHUNK):
|
|
140
|
+
placeholders = ",".join("?" * len(chunk))
|
|
141
|
+
rows = self.db.execute(
|
|
142
|
+
f"SELECT variant_id, status FROM variants "
|
|
143
|
+
f"WHERE dataset = ? AND query_version >= ? "
|
|
144
|
+
f"AND variant_id IN ({placeholders})",
|
|
145
|
+
(self.dataset, self.query_version, *chunk),
|
|
146
|
+
).fetchall()
|
|
147
|
+
for variant_id, status in rows:
|
|
148
|
+
if retry_errors and status == STATUS_ERROR:
|
|
149
|
+
continue
|
|
150
|
+
if retry_not_found and status == STATUS_NOT_FOUND:
|
|
151
|
+
continue
|
|
152
|
+
fresh.add(variant_id)
|
|
153
|
+
|
|
154
|
+
return [key for vid, key in unique.items() if vid not in fresh]
|
|
155
|
+
|
|
156
|
+
def put(
|
|
157
|
+
self,
|
|
158
|
+
key: VariantKey | str,
|
|
159
|
+
record: dict[str, Any] | None,
|
|
160
|
+
status: str | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Stage one record. Call commit() (or put_many) to persist.
|
|
163
|
+
|
|
164
|
+
`status` defaults to 'found' when a record is given and 'not_found'
|
|
165
|
+
when it is None.
|
|
166
|
+
"""
|
|
167
|
+
variant_id = key.id if isinstance(key, VariantKey) else key
|
|
168
|
+
if status is None:
|
|
169
|
+
status = STATUS_FOUND if record is not None else STATUS_NOT_FOUND
|
|
170
|
+
blob = (
|
|
171
|
+
zlib.compress(json.dumps(record).encode(), COMPRESSION_LEVEL)
|
|
172
|
+
if record is not None
|
|
173
|
+
else None
|
|
174
|
+
)
|
|
175
|
+
self.db.execute(
|
|
176
|
+
"INSERT OR REPLACE INTO variants "
|
|
177
|
+
"(variant_id, dataset, status, data, fetched_at, query_version) "
|
|
178
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
179
|
+
(variant_id, self.dataset, status, blob, _now(), self.query_version),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def put_many(
|
|
183
|
+
self,
|
|
184
|
+
records: Mapping[str, dict[str, Any] | None],
|
|
185
|
+
status: str | None = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Stage and commit a whole batch."""
|
|
188
|
+
for variant_id, record in records.items():
|
|
189
|
+
self.put(variant_id, record, status)
|
|
190
|
+
self.db.commit()
|
|
191
|
+
|
|
192
|
+
def mark_errors(self, keys: Sequence[VariantKey]) -> None:
|
|
193
|
+
"""Record a failed batch so a later run can retry just those keys."""
|
|
194
|
+
for key in keys:
|
|
195
|
+
self.put(key, None, STATUS_ERROR)
|
|
196
|
+
self.db.commit()
|
|
197
|
+
|
|
198
|
+
# --- dict-style reads -------------------------------------------------
|
|
199
|
+
|
|
200
|
+
def __getitem__(self, variant_id: str) -> dict[str, Any] | None:
|
|
201
|
+
row = self.db.execute(
|
|
202
|
+
"SELECT status, data FROM variants WHERE variant_id = ? AND dataset = ?",
|
|
203
|
+
(variant_id, self.dataset),
|
|
204
|
+
).fetchone()
|
|
205
|
+
if row is None:
|
|
206
|
+
raise KeyError(variant_id)
|
|
207
|
+
status, blob = row
|
|
208
|
+
if status != STATUS_FOUND or blob is None:
|
|
209
|
+
return None
|
|
210
|
+
return json.loads(zlib.decompress(blob))
|
|
211
|
+
|
|
212
|
+
def __iter__(self) -> Iterator[str]:
|
|
213
|
+
for (variant_id,) in self.db.execute(
|
|
214
|
+
"SELECT variant_id FROM variants WHERE dataset = ?", (self.dataset,)
|
|
215
|
+
):
|
|
216
|
+
yield variant_id
|
|
217
|
+
|
|
218
|
+
def __len__(self) -> int:
|
|
219
|
+
row = self.db.execute(
|
|
220
|
+
"SELECT count(*) FROM variants WHERE dataset = ?", (self.dataset,)
|
|
221
|
+
).fetchone()
|
|
222
|
+
return int(row[0])
|
|
223
|
+
|
|
224
|
+
def many(self, variant_ids: Sequence[str]) -> dict[str, dict[str, Any] | None]:
|
|
225
|
+
"""Fetch many records in one round trip. Absent ids are simply omitted."""
|
|
226
|
+
out: dict[str, dict[str, Any] | None] = {}
|
|
227
|
+
for chunk in _chunked(list(variant_ids), _SQL_CHUNK):
|
|
228
|
+
placeholders = ",".join("?" * len(chunk))
|
|
229
|
+
for variant_id, status, blob in self.db.execute(
|
|
230
|
+
f"SELECT variant_id, status, data FROM variants "
|
|
231
|
+
f"WHERE dataset = ? AND variant_id IN ({placeholders})",
|
|
232
|
+
(self.dataset, *chunk),
|
|
233
|
+
):
|
|
234
|
+
out[variant_id] = (
|
|
235
|
+
json.loads(zlib.decompress(blob))
|
|
236
|
+
if status == STATUS_FOUND and blob is not None
|
|
237
|
+
else None
|
|
238
|
+
)
|
|
239
|
+
return out
|
|
240
|
+
|
|
241
|
+
# --- export ----------------------------------------------------------
|
|
242
|
+
#
|
|
243
|
+
# Thin delegations to gnomad_api_cache.export, which holds the flattening
|
|
244
|
+
# logic. Imported lazily so cache.py has no import-time dependency on the
|
|
245
|
+
# export module (or, through it, on pyarrow).
|
|
246
|
+
|
|
247
|
+
def to_json(
|
|
248
|
+
self,
|
|
249
|
+
path: str | Path,
|
|
250
|
+
variant_ids: Sequence[str] | None = None,
|
|
251
|
+
lines: bool = False,
|
|
252
|
+
indent: int | None = None,
|
|
253
|
+
) -> int:
|
|
254
|
+
"""Write records verbatim as JSON. See export.to_json."""
|
|
255
|
+
from gnomad_api_cache import export
|
|
256
|
+
|
|
257
|
+
return export.to_json(self, path, variant_ids, lines, indent)
|
|
258
|
+
|
|
259
|
+
def to_csv(
|
|
260
|
+
self,
|
|
261
|
+
path: str | Path,
|
|
262
|
+
variant_ids: Sequence[str] | None = None,
|
|
263
|
+
include_populations: bool = False,
|
|
264
|
+
) -> int:
|
|
265
|
+
"""Write flattened rows as CSV. See export.to_csv."""
|
|
266
|
+
from gnomad_api_cache import export
|
|
267
|
+
|
|
268
|
+
return export.to_csv(self, path, variant_ids, include_populations)
|
|
269
|
+
|
|
270
|
+
def to_tsv(
|
|
271
|
+
self,
|
|
272
|
+
path: str | Path,
|
|
273
|
+
variant_ids: Sequence[str] | None = None,
|
|
274
|
+
include_populations: bool = False,
|
|
275
|
+
) -> int:
|
|
276
|
+
"""Write flattened rows as TSV. See export.to_tsv."""
|
|
277
|
+
from gnomad_api_cache import export
|
|
278
|
+
|
|
279
|
+
return export.to_tsv(self, path, variant_ids, include_populations)
|
|
280
|
+
|
|
281
|
+
def to_parquet(
|
|
282
|
+
self,
|
|
283
|
+
path: str | Path,
|
|
284
|
+
variant_ids: Sequence[str] | None = None,
|
|
285
|
+
include_populations: bool = False,
|
|
286
|
+
) -> int:
|
|
287
|
+
"""Write flattened rows as Parquet. See export.to_parquet."""
|
|
288
|
+
from gnomad_api_cache import export
|
|
289
|
+
|
|
290
|
+
return export.to_parquet(self, path, variant_ids, include_populations)
|
|
291
|
+
|
|
292
|
+
def status_counts(self) -> dict[str, int]:
|
|
293
|
+
"""Row counts by status -- a one-line health check on the cache."""
|
|
294
|
+
return {
|
|
295
|
+
status: count
|
|
296
|
+
for status, count in self.db.execute(
|
|
297
|
+
"SELECT status, count(*) FROM variants WHERE dataset = ? "
|
|
298
|
+
"GROUP BY status",
|
|
299
|
+
(self.dataset,),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
def fetch(self, variants: list[VariantKey], **kwargs) -> FetchSummary:
|
|
304
|
+
"""Fetch missing records into this cache. See fetch.fetch_into."""
|
|
305
|
+
from gnomad_api_cache import fetch as _fetch
|
|
306
|
+
|
|
307
|
+
return _fetch.fetch_into(self, variants, **kwargs)
|
|
308
|
+
|
|
309
|
+
def fetch_vcf(
|
|
310
|
+
self,
|
|
311
|
+
vcf_path: str | Path,
|
|
312
|
+
retry_errors: bool = True,
|
|
313
|
+
retry_not_found: bool = False,
|
|
314
|
+
filter_function: Callable[[Variant], bool] | None = None,
|
|
315
|
+
) -> FetchSummary:
|
|
316
|
+
"""Read a VCF and fetch missing records into this cache."""
|
|
317
|
+
from gnomad_api_cache import fetch as _fetch
|
|
318
|
+
from gnomad_api_cache.adapters import vcf_adapter
|
|
319
|
+
|
|
320
|
+
return _fetch.fetch_into(
|
|
321
|
+
self,
|
|
322
|
+
vcf_adapter.read_vcf(
|
|
323
|
+
vcf_path,
|
|
324
|
+
filter_function=filter_function
|
|
325
|
+
),
|
|
326
|
+
retry_errors=retry_errors,
|
|
327
|
+
retry_not_found=retry_not_found,
|
|
328
|
+
)
|