allelix 2.0.2__tar.gz → 2.1.1__tar.gz
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.
- {allelix-2.0.2 → allelix-2.1.1}/PKG-INFO +106 -8
- {allelix-2.0.2 → allelix-2.1.1}/README.md +105 -7
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/base.py +28 -19
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/clinvar.py +14 -4
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/gwas.py +44 -6
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/pharmgkb.py +21 -5
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/snpedia.py +3 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/models.py +63 -2
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/vcf.py +1 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/_pipeline.py +39 -58
- {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/diff.py +5 -3
- {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/json_report.py +9 -7
- allelix-2.1.1/allelix/utils/allele.py +211 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/PKG-INFO +106 -8
- {allelix-2.0.2 → allelix-2.1.1}/pyproject.toml +1 -1
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_models.py +73 -0
- allelix-2.0.2/allelix/utils/allele.py +0 -89
- {allelix-2.0.2 → allelix-2.1.1}/LICENSE +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/alphamissense.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/cadd.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/gnomad.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/_helpers.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/_options.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/analyze.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/config.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/db.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/focused.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/utility.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/compare.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/config.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/data/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/data/clinvar_clnsig_snapshot.yaml +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/data/high_value_snps.yaml +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/_versions.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/alphamissense_loader.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/cadd_loader.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/cpic_loader.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/gnomad_loader.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/gwas_loader.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/loader_utils.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/manager.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/pharmgkb_loader.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/schema.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/snpedia_loader.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/snpedia_parser.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/exporters/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/exporters/plink.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/_helpers.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/ancestrydna.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/base.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/ftdna.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/ftdna_illumina.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/livingdna.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/myhappygenes.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/myheritage.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/twentythreeandme.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/py.typed +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/high_value.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/html.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/methylation.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/terminal.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/utils/__init__.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix/utils/build_detect.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/SOURCES.txt +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/dependency_links.txt +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/entry_points.txt +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/requires.txt +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/top_level.txt +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/setup.cfg +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_cli.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_cli_helpers.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_compare.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_config.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_end_to_end.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_mock_data_invariants.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_registry.py +0 -0
- {allelix-2.0.2 → allelix-2.1.1}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: allelix
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: Open-source genotype analysis toolkit. Format-agnostic ingestion, database-agnostic annotation, offline-first.
|
|
5
5
|
Author: Allelix
|
|
6
6
|
Maintainer-email: dial481 <dial481@users.noreply.github.com>
|
|
@@ -47,19 +47,26 @@ Open-source command-line toolkit for analyzing raw genotype files from consumer
|
|
|
47
47
|
> HTML/JSON/terminal reports, methylation + pharmacogenomics focused
|
|
48
48
|
> commands, report diffing, persistent config with commercial-mode
|
|
49
49
|
> safety switch. Build auto-detection from position data (ADR-0021).
|
|
50
|
-
> No regex on prose anywhere in production. **Latest: v2.
|
|
51
|
-
>
|
|
52
|
-
>
|
|
53
|
-
>
|
|
50
|
+
> No regex on prose anywhere in production. **Latest: v2.1.0** —
|
|
51
|
+
> ADR-0035 'Cluster B' coordinated model change: `Variant.ref` carries
|
|
52
|
+
> the forward-strand reference allele, per-Annotation `alt` threading
|
|
53
|
+
> on GWAS / SNPedia / ClinPGx enables allele-specific gnomAD /
|
|
54
|
+
> AlphaMissense / CADD enrichment on array inputs, structured GWAS
|
|
55
|
+
> fields (`trait` / `p_value` / `phecode`) replace prose re-parsing in
|
|
56
|
+
> the rollup, and ClinVar / ClinPGx carrier matching is strand-aware
|
|
57
|
+
> with multi-allelic safety guard. JSON `schema_version` bumped 4 → 5
|
|
58
|
+
> (additive, v4 readers unaffected).
|
|
54
59
|
> Release notes:
|
|
55
60
|
> [`CHANGELOG.md`](https://github.com/allelix/allelix/blob/main/CHANGELOG.md).
|
|
56
61
|
|
|
57
62
|
## Quickstart
|
|
58
63
|
|
|
64
|
+
**Requires Python 3.11 or newer.** On Python 3.10 or older, `pip install allelix` succeeds but the first command fails with `ImportError: cannot import name 'UTC' from 'datetime'` — that's the symptom; the fix is to install on 3.11+. Check with `python --version`.
|
|
65
|
+
|
|
59
66
|
```bash
|
|
60
67
|
pip install allelix
|
|
61
68
|
|
|
62
|
-
# Download reference databases (~
|
|
69
|
+
# Download reference databases (~16 GB default install; ~22 GB if you opt into CADD).
|
|
63
70
|
# Use --no-gnomad / --no-alphamissense to skip the large ones.
|
|
64
71
|
# CADD is opt-in: allelix db update --cadd
|
|
65
72
|
allelix db update
|
|
@@ -77,7 +84,7 @@ allelix analyze trio.vcf.gz --sample HG002 --output report.html
|
|
|
77
84
|
allelix analyze your_genotype_file.txt --filter-file my_panel.txt --output report.html
|
|
78
85
|
```
|
|
79
86
|
|
|
80
|
-
|
|
87
|
+
See [Development](#development) for source installs and running tests, [Managing your data](#managing-your-data) for cache locations and cleanup, and [Troubleshooting](#troubleshooting) for common failure states.
|
|
81
88
|
|
|
82
89
|
## Supported Formats
|
|
83
90
|
|
|
@@ -116,7 +123,20 @@ Adding a new format means adding one file to `allelix/parsers/` and registering
|
|
|
116
123
|
| GWAS Catalog | ✓ | Public domain (EBI/NHGRI). Trait–SNP associations with p-values and effect sizes. Carrier rule (ADR-0007) requires the user to carry the risk allele. P-value magnitude scoring (ADR-0024) maps continuous p-values to the 0–10 scale; unknown-risk-allele entries fire on rsID match alone but are capped at 3.0. |
|
|
117
124
|
| gnomAD | ✓ | ODbL v1.0. **Enrichment annotator** — adds population allele frequency context to existing annotations. Shows how common each variant is in the general population (~16M exome variants from 730K individuals). A pathogenic variant that 35% of people carry reads very differently from one seen in 0.001%. Pre-built cache downloaded via `db update` (~6GB on disk). Use `--no-gnomad` to skip. |
|
|
118
125
|
| AlphaMissense | ✓ | CC BY 4.0. **Enrichment annotator** — adds DeepMind's protein-structure-based pathogenicity predictions to existing annotations. Scores 71M missense variants on a 0–1 scale: <0.34 = likely benign, >0.564 = likely pathogenic. Complements ClinVar's expert classifications with computational predictions — especially valuable for variants ClinVar hasn't reviewed yet. Pre-built cache downloaded via `db update` (~8GB on disk). Use `--no-alphamissense` to skip. |
|
|
119
|
-
| CADD | ✓ | LicenseRef-CADD (non-commercial). **Enrichment annotator** — adds PHRED-scaled deleteriousness scores from CADD v1.7.
|
|
126
|
+
| CADD | ✓ | LicenseRef-CADD (non-commercial). **Enrichment annotator** — adds PHRED-scaled deleteriousness scores from CADD v1.7. **Opt-in**, disabled by default; see [CADD modes](#cadd-modes) below for details. |
|
|
127
|
+
|
|
128
|
+
### CADD modes
|
|
129
|
+
|
|
130
|
+
CADD ranks how deleterious any single-nucleotide variant is using 100+ annotation tracks (coding, non-coding, regulatory). PHRED 10 = top 10% most deleterious, 20 = top 1%, 30 = top 0.1%.
|
|
131
|
+
|
|
132
|
+
**Opt-in**: enable via `allelix db update --cadd` or `allelix config set sources.cadd true`. Use `--no-cadd` to skip enrichment for a single run.
|
|
133
|
+
|
|
134
|
+
Two modes:
|
|
135
|
+
|
|
136
|
+
- **Cache mode** (default): pre-built ~5.8 GB SQLite cache (~120M variant keys). Covers nearly every position allelix can annotate from its other databases (gnomAD, AlphaMissense, ClinVar). For genotyping chip data (23andMe, AncestryDNA, MyHappyGenes, etc.), this mode is functionally complete — chip probes overwhelmingly target known, cataloged variants.
|
|
137
|
+
- **Full mode** (`options.cadd_full = true`): tabix queries against the complete CADD v1.7 file via pysam (GRCh38 only). Adds coverage for novel or private variants that appear only in WGS/WES data and are not in any pre-computed database. Requires `pip install allelix[cadd]` for the pysam dependency.
|
|
138
|
+
|
|
139
|
+
If your input is a genotyping chip file, cache mode is all you need.
|
|
120
140
|
|
|
121
141
|
### Build coverage asymmetry (GRCh37 vs GRCh38)
|
|
122
142
|
|
|
@@ -214,6 +234,48 @@ gnomAD and AlphaMissense are the largest but add the most interpretive context.
|
|
|
214
234
|
|
|
215
235
|
To skip either during download: `allelix db update --no-gnomad --no-alphamissense`. To disable permanently: `allelix config set sources.gnomad false`.
|
|
216
236
|
|
|
237
|
+
### Managing your data
|
|
238
|
+
|
|
239
|
+
Allelix stores reference database caches at `~/.local/share/allelix/` (Linux/macOS) or the XDG-equivalent on other platforms. Default install (no CADD) is ~16 GB on disk; with the CADD opt-in it's ~22 GB.
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
~/.local/share/allelix/
|
|
243
|
+
├── alphamissense.sqlite ~7.8 GB
|
|
244
|
+
├── gnomad.sqlite ~6.1 GB
|
|
245
|
+
├── cadd.sqlite ~5.8 GB (only if you opted into --cadd)
|
|
246
|
+
├── clinvar.GRCh37.sqlite ~470 MB
|
|
247
|
+
├── clinvar.GRCh38.sqlite ~480 MB
|
|
248
|
+
├── gwas_catalog_associations.tsv ~700 MB
|
|
249
|
+
├── snpedia.sqlite ~390 MB
|
|
250
|
+
├── gwas.sqlite ~190 MB
|
|
251
|
+
└── pharmgkb.sqlite ~14 MB
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**The caches are disposable and re-downloadable.** Everything here was fetched by `allelix db update` and can be re-fetched at any time. There's no per-user state in the cache — your genotype files never enter this directory. Safe to delete to reclaim space (or to force a full refresh if you suspect a cache went stale):
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Reclaim all ~16-22 GB
|
|
258
|
+
rm -rf ~/.local/share/allelix/
|
|
259
|
+
|
|
260
|
+
# Reclaim everything except CADD (avoids re-downloading the slowest one)
|
|
261
|
+
find ~/.local/share/allelix/ -mindepth 1 ! -name 'cadd*.sqlite*' -delete
|
|
262
|
+
|
|
263
|
+
# Re-populate from scratch next time you analyze a file
|
|
264
|
+
allelix db update
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**The one thing worth backing up: `config.toml`.** It lives separately (XDG config dir, typically `~/.config/allelix/config.toml`) and stores your license assertions (commercial-mode toggle, CADD opt-in confirmation) and per-source enable/disable settings. It's the only non-reproducible state in the install. The caches will rebuild on the next `db update`; `config.toml` won't.
|
|
268
|
+
|
|
269
|
+
To fully uninstall allelix and reclaim everything:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
pip uninstall allelix
|
|
273
|
+
rm -rf ~/.local/share/allelix/ # reference database caches
|
|
274
|
+
rm -rf ~/.config/allelix/ # license + source-toggle config
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
A `allelix db clean` subcommand to automate the cache-clear step (with confirmation prompt and size report) is planned for v2.2 alongside `allelix db path` (prints the resolved data directory for scripting / backup integration).
|
|
278
|
+
|
|
217
279
|
## Data Sources & Licensing
|
|
218
280
|
|
|
219
281
|
Allelix source code is licensed under the **GNU Affero General Public License v3.0 or later** (AGPL-3.0-or-later). Allelix ships with **zero third-party data**. All reference databases are downloaded by the user at runtime via `allelix db update`. Each database retains its original license on the user's machine:
|
|
@@ -256,6 +318,42 @@ Of the 104,806 genotype pages in the archive:
|
|
|
256
318
|
|
|
257
319
|
None of these are scraping errors. They are editorial inconsistencies on the source wiki. The annotator handles all of them correctly: incomplete entries are skipped, variant title formats are matched, and no false annotations are produced.
|
|
258
320
|
|
|
321
|
+
## Troubleshooting
|
|
322
|
+
|
|
323
|
+
### `ImportError: cannot import name 'UTC' from 'datetime'`
|
|
324
|
+
|
|
325
|
+
You're on Python 3.10 or older. Allelix requires 3.11+. `python --version` to check; upgrade and reinstall.
|
|
326
|
+
|
|
327
|
+
### `allelix db update` failed partway through
|
|
328
|
+
|
|
329
|
+
Run it again. The update is idempotent — completed downloads are skipped (signal-matched against the upstream freshness probe), so re-running picks up where it left off. If a specific source keeps failing, you can isolate it: `allelix db update --no-gnomad` (or `--no-alphamissense` etc.) to skip the failing source for now, then retry that source standalone later.
|
|
330
|
+
|
|
331
|
+
If the failure is a network timeout on the same source repeatedly, the issue is likely on the source's end (HuggingFace / NCBI / EBI / etc. transient outage). Wait an hour and retry.
|
|
332
|
+
|
|
333
|
+
### `db status` shows an annotator as "not ready" but the cache files exist on disk
|
|
334
|
+
|
|
335
|
+
Almost always a schema or interpreter version bump in a recent allelix release that invalidates the pre-bump cache. The fix is `allelix db update` — it'll re-ingest only the affected annotators (everything else signal-skips). See the relevant `CHANGELOG.md` entry for "Cache invalidation" notes per release.
|
|
336
|
+
|
|
337
|
+
### Build mismatch warning when analyzing a known-good file
|
|
338
|
+
|
|
339
|
+
Either: (a) the file's header claims one build and the position data is actually a different build (real-world MHG / Tempus exports do this — header says 37, positions are 38; ADR-0021 documents the auto-detection), or (b) the file is a GRCh36-era export (pre-2012), which allelix detects accurately but doesn't ship a ClinVar cache for (see [`docs/grch36-liftover.md`](https://github.com/allelix/allelix/blob/main/docs/grch36-liftover.md) for the external-liftover path).
|
|
340
|
+
|
|
341
|
+
If you trust the file's header more than allelix's detection, force the build: `--build grch37` or `--build grch38`. The build banner in the analyze output will reflect what was actually used.
|
|
342
|
+
|
|
343
|
+
### Is my database stale? How fresh is the data?
|
|
344
|
+
|
|
345
|
+
`allelix db status` shows the ingestion date of each cache. The freshness model is signal-driven: `allelix db update` queries an upstream freshness signal (md5 / etag) per source and re-ingests only when the signal changed. There's no time-based staleness — a 6-month-old cache against an unchanged source IS current.
|
|
346
|
+
|
|
347
|
+
If you want to force a refresh: `allelix db update --force` re-downloads every cache regardless of freshness signal.
|
|
348
|
+
|
|
349
|
+
### Output reports contain real genome annotations
|
|
350
|
+
|
|
351
|
+
Anything `allelix analyze` writes to disk contains real annotations of your genome. Treat the output files (HTML, JSON, terminal redirects) as sensitive — they're as private as the input genotype file. Allelix does not transmit them; the lifecycle is local.
|
|
352
|
+
|
|
353
|
+
### Where does allelix store its data?
|
|
354
|
+
|
|
355
|
+
See [Managing your data](#managing-your-data) above for cache locations, the disposability rule, what to back up, and the uninstall procedure.
|
|
356
|
+
|
|
259
357
|
## Architecture & Design Decisions
|
|
260
358
|
|
|
261
359
|
The "why" behind major design choices lives in [`docs/adr/`](https://github.com/allelix/allelix/blob/main/docs/adr/README.md) as Architecture Decision Records. Read these before proposing changes that touch the parser/annotator interfaces, the regulatory posture, or the data-handling model.
|
|
@@ -10,19 +10,26 @@ Open-source command-line toolkit for analyzing raw genotype files from consumer
|
|
|
10
10
|
> HTML/JSON/terminal reports, methylation + pharmacogenomics focused
|
|
11
11
|
> commands, report diffing, persistent config with commercial-mode
|
|
12
12
|
> safety switch. Build auto-detection from position data (ADR-0021).
|
|
13
|
-
> No regex on prose anywhere in production. **Latest: v2.
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
13
|
+
> No regex on prose anywhere in production. **Latest: v2.1.0** —
|
|
14
|
+
> ADR-0035 'Cluster B' coordinated model change: `Variant.ref` carries
|
|
15
|
+
> the forward-strand reference allele, per-Annotation `alt` threading
|
|
16
|
+
> on GWAS / SNPedia / ClinPGx enables allele-specific gnomAD /
|
|
17
|
+
> AlphaMissense / CADD enrichment on array inputs, structured GWAS
|
|
18
|
+
> fields (`trait` / `p_value` / `phecode`) replace prose re-parsing in
|
|
19
|
+
> the rollup, and ClinVar / ClinPGx carrier matching is strand-aware
|
|
20
|
+
> with multi-allelic safety guard. JSON `schema_version` bumped 4 → 5
|
|
21
|
+
> (additive, v4 readers unaffected).
|
|
17
22
|
> Release notes:
|
|
18
23
|
> [`CHANGELOG.md`](https://github.com/allelix/allelix/blob/main/CHANGELOG.md).
|
|
19
24
|
|
|
20
25
|
## Quickstart
|
|
21
26
|
|
|
27
|
+
**Requires Python 3.11 or newer.** On Python 3.10 or older, `pip install allelix` succeeds but the first command fails with `ImportError: cannot import name 'UTC' from 'datetime'` — that's the symptom; the fix is to install on 3.11+. Check with `python --version`.
|
|
28
|
+
|
|
22
29
|
```bash
|
|
23
30
|
pip install allelix
|
|
24
31
|
|
|
25
|
-
# Download reference databases (~
|
|
32
|
+
# Download reference databases (~16 GB default install; ~22 GB if you opt into CADD).
|
|
26
33
|
# Use --no-gnomad / --no-alphamissense to skip the large ones.
|
|
27
34
|
# CADD is opt-in: allelix db update --cadd
|
|
28
35
|
allelix db update
|
|
@@ -40,7 +47,7 @@ allelix analyze trio.vcf.gz --sample HG002 --output report.html
|
|
|
40
47
|
allelix analyze your_genotype_file.txt --filter-file my_panel.txt --output report.html
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
|
|
50
|
+
See [Development](#development) for source installs and running tests, [Managing your data](#managing-your-data) for cache locations and cleanup, and [Troubleshooting](#troubleshooting) for common failure states.
|
|
44
51
|
|
|
45
52
|
## Supported Formats
|
|
46
53
|
|
|
@@ -79,7 +86,20 @@ Adding a new format means adding one file to `allelix/parsers/` and registering
|
|
|
79
86
|
| GWAS Catalog | ✓ | Public domain (EBI/NHGRI). Trait–SNP associations with p-values and effect sizes. Carrier rule (ADR-0007) requires the user to carry the risk allele. P-value magnitude scoring (ADR-0024) maps continuous p-values to the 0–10 scale; unknown-risk-allele entries fire on rsID match alone but are capped at 3.0. |
|
|
80
87
|
| gnomAD | ✓ | ODbL v1.0. **Enrichment annotator** — adds population allele frequency context to existing annotations. Shows how common each variant is in the general population (~16M exome variants from 730K individuals). A pathogenic variant that 35% of people carry reads very differently from one seen in 0.001%. Pre-built cache downloaded via `db update` (~6GB on disk). Use `--no-gnomad` to skip. |
|
|
81
88
|
| AlphaMissense | ✓ | CC BY 4.0. **Enrichment annotator** — adds DeepMind's protein-structure-based pathogenicity predictions to existing annotations. Scores 71M missense variants on a 0–1 scale: <0.34 = likely benign, >0.564 = likely pathogenic. Complements ClinVar's expert classifications with computational predictions — especially valuable for variants ClinVar hasn't reviewed yet. Pre-built cache downloaded via `db update` (~8GB on disk). Use `--no-alphamissense` to skip. |
|
|
82
|
-
| CADD | ✓ | LicenseRef-CADD (non-commercial). **Enrichment annotator** — adds PHRED-scaled deleteriousness scores from CADD v1.7.
|
|
89
|
+
| CADD | ✓ | LicenseRef-CADD (non-commercial). **Enrichment annotator** — adds PHRED-scaled deleteriousness scores from CADD v1.7. **Opt-in**, disabled by default; see [CADD modes](#cadd-modes) below for details. |
|
|
90
|
+
|
|
91
|
+
### CADD modes
|
|
92
|
+
|
|
93
|
+
CADD ranks how deleterious any single-nucleotide variant is using 100+ annotation tracks (coding, non-coding, regulatory). PHRED 10 = top 10% most deleterious, 20 = top 1%, 30 = top 0.1%.
|
|
94
|
+
|
|
95
|
+
**Opt-in**: enable via `allelix db update --cadd` or `allelix config set sources.cadd true`. Use `--no-cadd` to skip enrichment for a single run.
|
|
96
|
+
|
|
97
|
+
Two modes:
|
|
98
|
+
|
|
99
|
+
- **Cache mode** (default): pre-built ~5.8 GB SQLite cache (~120M variant keys). Covers nearly every position allelix can annotate from its other databases (gnomAD, AlphaMissense, ClinVar). For genotyping chip data (23andMe, AncestryDNA, MyHappyGenes, etc.), this mode is functionally complete — chip probes overwhelmingly target known, cataloged variants.
|
|
100
|
+
- **Full mode** (`options.cadd_full = true`): tabix queries against the complete CADD v1.7 file via pysam (GRCh38 only). Adds coverage for novel or private variants that appear only in WGS/WES data and are not in any pre-computed database. Requires `pip install allelix[cadd]` for the pysam dependency.
|
|
101
|
+
|
|
102
|
+
If your input is a genotyping chip file, cache mode is all you need.
|
|
83
103
|
|
|
84
104
|
### Build coverage asymmetry (GRCh37 vs GRCh38)
|
|
85
105
|
|
|
@@ -177,6 +197,48 @@ gnomAD and AlphaMissense are the largest but add the most interpretive context.
|
|
|
177
197
|
|
|
178
198
|
To skip either during download: `allelix db update --no-gnomad --no-alphamissense`. To disable permanently: `allelix config set sources.gnomad false`.
|
|
179
199
|
|
|
200
|
+
### Managing your data
|
|
201
|
+
|
|
202
|
+
Allelix stores reference database caches at `~/.local/share/allelix/` (Linux/macOS) or the XDG-equivalent on other platforms. Default install (no CADD) is ~16 GB on disk; with the CADD opt-in it's ~22 GB.
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
~/.local/share/allelix/
|
|
206
|
+
├── alphamissense.sqlite ~7.8 GB
|
|
207
|
+
├── gnomad.sqlite ~6.1 GB
|
|
208
|
+
├── cadd.sqlite ~5.8 GB (only if you opted into --cadd)
|
|
209
|
+
├── clinvar.GRCh37.sqlite ~470 MB
|
|
210
|
+
├── clinvar.GRCh38.sqlite ~480 MB
|
|
211
|
+
├── gwas_catalog_associations.tsv ~700 MB
|
|
212
|
+
├── snpedia.sqlite ~390 MB
|
|
213
|
+
├── gwas.sqlite ~190 MB
|
|
214
|
+
└── pharmgkb.sqlite ~14 MB
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**The caches are disposable and re-downloadable.** Everything here was fetched by `allelix db update` and can be re-fetched at any time. There's no per-user state in the cache — your genotype files never enter this directory. Safe to delete to reclaim space (or to force a full refresh if you suspect a cache went stale):
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Reclaim all ~16-22 GB
|
|
221
|
+
rm -rf ~/.local/share/allelix/
|
|
222
|
+
|
|
223
|
+
# Reclaim everything except CADD (avoids re-downloading the slowest one)
|
|
224
|
+
find ~/.local/share/allelix/ -mindepth 1 ! -name 'cadd*.sqlite*' -delete
|
|
225
|
+
|
|
226
|
+
# Re-populate from scratch next time you analyze a file
|
|
227
|
+
allelix db update
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**The one thing worth backing up: `config.toml`.** It lives separately (XDG config dir, typically `~/.config/allelix/config.toml`) and stores your license assertions (commercial-mode toggle, CADD opt-in confirmation) and per-source enable/disable settings. It's the only non-reproducible state in the install. The caches will rebuild on the next `db update`; `config.toml` won't.
|
|
231
|
+
|
|
232
|
+
To fully uninstall allelix and reclaim everything:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
pip uninstall allelix
|
|
236
|
+
rm -rf ~/.local/share/allelix/ # reference database caches
|
|
237
|
+
rm -rf ~/.config/allelix/ # license + source-toggle config
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
A `allelix db clean` subcommand to automate the cache-clear step (with confirmation prompt and size report) is planned for v2.2 alongside `allelix db path` (prints the resolved data directory for scripting / backup integration).
|
|
241
|
+
|
|
180
242
|
## Data Sources & Licensing
|
|
181
243
|
|
|
182
244
|
Allelix source code is licensed under the **GNU Affero General Public License v3.0 or later** (AGPL-3.0-or-later). Allelix ships with **zero third-party data**. All reference databases are downloaded by the user at runtime via `allelix db update`. Each database retains its original license on the user's machine:
|
|
@@ -219,6 +281,42 @@ Of the 104,806 genotype pages in the archive:
|
|
|
219
281
|
|
|
220
282
|
None of these are scraping errors. They are editorial inconsistencies on the source wiki. The annotator handles all of them correctly: incomplete entries are skipped, variant title formats are matched, and no false annotations are produced.
|
|
221
283
|
|
|
284
|
+
## Troubleshooting
|
|
285
|
+
|
|
286
|
+
### `ImportError: cannot import name 'UTC' from 'datetime'`
|
|
287
|
+
|
|
288
|
+
You're on Python 3.10 or older. Allelix requires 3.11+. `python --version` to check; upgrade and reinstall.
|
|
289
|
+
|
|
290
|
+
### `allelix db update` failed partway through
|
|
291
|
+
|
|
292
|
+
Run it again. The update is idempotent — completed downloads are skipped (signal-matched against the upstream freshness probe), so re-running picks up where it left off. If a specific source keeps failing, you can isolate it: `allelix db update --no-gnomad` (or `--no-alphamissense` etc.) to skip the failing source for now, then retry that source standalone later.
|
|
293
|
+
|
|
294
|
+
If the failure is a network timeout on the same source repeatedly, the issue is likely on the source's end (HuggingFace / NCBI / EBI / etc. transient outage). Wait an hour and retry.
|
|
295
|
+
|
|
296
|
+
### `db status` shows an annotator as "not ready" but the cache files exist on disk
|
|
297
|
+
|
|
298
|
+
Almost always a schema or interpreter version bump in a recent allelix release that invalidates the pre-bump cache. The fix is `allelix db update` — it'll re-ingest only the affected annotators (everything else signal-skips). See the relevant `CHANGELOG.md` entry for "Cache invalidation" notes per release.
|
|
299
|
+
|
|
300
|
+
### Build mismatch warning when analyzing a known-good file
|
|
301
|
+
|
|
302
|
+
Either: (a) the file's header claims one build and the position data is actually a different build (real-world MHG / Tempus exports do this — header says 37, positions are 38; ADR-0021 documents the auto-detection), or (b) the file is a GRCh36-era export (pre-2012), which allelix detects accurately but doesn't ship a ClinVar cache for (see [`docs/grch36-liftover.md`](https://github.com/allelix/allelix/blob/main/docs/grch36-liftover.md) for the external-liftover path).
|
|
303
|
+
|
|
304
|
+
If you trust the file's header more than allelix's detection, force the build: `--build grch37` or `--build grch38`. The build banner in the analyze output will reflect what was actually used.
|
|
305
|
+
|
|
306
|
+
### Is my database stale? How fresh is the data?
|
|
307
|
+
|
|
308
|
+
`allelix db status` shows the ingestion date of each cache. The freshness model is signal-driven: `allelix db update` queries an upstream freshness signal (md5 / etag) per source and re-ingests only when the signal changed. There's no time-based staleness — a 6-month-old cache against an unchanged source IS current.
|
|
309
|
+
|
|
310
|
+
If you want to force a refresh: `allelix db update --force` re-downloads every cache regardless of freshness signal.
|
|
311
|
+
|
|
312
|
+
### Output reports contain real genome annotations
|
|
313
|
+
|
|
314
|
+
Anything `allelix analyze` writes to disk contains real annotations of your genome. Treat the output files (HTML, JSON, terminal redirects) as sensitive — they're as private as the input genotype file. Allelix does not transmit them; the lifecycle is local.
|
|
315
|
+
|
|
316
|
+
### Where does allelix store its data?
|
|
317
|
+
|
|
318
|
+
See [Managing your data](#managing-your-data) above for cache locations, the disposability rule, what to back up, and the uninstall procedure.
|
|
319
|
+
|
|
222
320
|
## Architecture & Design Decisions
|
|
223
321
|
|
|
224
322
|
The "why" behind major design choices lives in [`docs/adr/`](https://github.com/allelix/allelix/blob/main/docs/adr/README.md) as Architecture Decision Records. Read these before proposing changes that touch the parser/annotator interfaces, the regulatory posture, or the data-handling model.
|
|
@@ -144,27 +144,36 @@ class Annotator(ABC):
|
|
|
144
144
|
def __del__(self) -> None:
|
|
145
145
|
"""Safety-net resource release on GC. Deliberately retained.
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
147
|
+
Status (v2.1.1 audit, GH #36):
|
|
148
|
+
- **Production callsites are clean.** ``run_analysis`` wires every
|
|
149
|
+
primary + enrichment annotator into ``contextlib.ExitStack``
|
|
150
|
+
(v2.0.2 fix). ``allelix/cli/utility.py`` lines 221 and 476 each
|
|
151
|
+
construct a ``GnomadAnnotator`` inside ``try / finally
|
|
152
|
+
gnomad.close()``. ``allelix/annotators/__init__.py`` returns a
|
|
153
|
+
list that the pipeline immediately wraps in ``ExitStack``.
|
|
154
|
+
- **Test callsites NOT yet clean.** ~27 sites in
|
|
155
|
+
``tests/reports/test_pipeline.py`` instantiate annotators
|
|
156
|
+
directly without a context manager or explicit close. The
|
|
157
|
+
pipeline closes them via its own ``ExitStack`` during
|
|
158
|
+
``run_analysis``, but the test still holds the closed-instance
|
|
159
|
+
reference, and the underlying sqlite3 ``Connection``'s GC
|
|
160
|
+
finalizer surfaces a ``ResourceWarning`` during the next test's
|
|
161
|
+
startup. pytest's ``error::PytestUnraisableExceptionWarning``
|
|
162
|
+
filter then converts it to a hard failure on the gate.
|
|
163
|
+
|
|
164
|
+
Removing ``__del__`` without first refactoring the test sites
|
|
165
|
+
therefore fails the gate (verified — 3 failures, all in
|
|
166
|
+
``tests/reports/test_pipeline.py``, triggered by leaks from
|
|
167
|
+
earlier tests in the same file). The test-site refactor is
|
|
168
|
+
tracked separately; ``__del__`` stays as the safety net until
|
|
169
|
+
that lands. v2.1.x task — see the follow-up issue filed at
|
|
170
|
+
``#36``-closure-time.
|
|
162
171
|
|
|
163
172
|
``contextlib.suppress(Exception)`` is deliberate — ``__del__``
|
|
164
|
-
must never raise.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
must never raise. GC timing and shutdown-ordering edges are
|
|
174
|
+
explicitly silenced; this is exactly the "if you must keep
|
|
175
|
+
``__del__``, make absolutely sure it can never raise"
|
|
176
|
+
mitigation the audit recommended.
|
|
168
177
|
"""
|
|
169
178
|
with contextlib.suppress(Exception):
|
|
170
179
|
self.close()
|
|
@@ -30,6 +30,7 @@ from allelix.databases.manager import (
|
|
|
30
30
|
verify_file_hash,
|
|
31
31
|
)
|
|
32
32
|
from allelix.models import Annotation
|
|
33
|
+
from allelix.utils.allele import strand_aware_carrier_match
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from collections.abc import Iterable, Iterator
|
|
@@ -384,7 +385,6 @@ class ClinVarAnnotator(Annotator):
|
|
|
384
385
|
(variant.rsid,),
|
|
385
386
|
).fetchall()
|
|
386
387
|
annotations: list[Annotation] = []
|
|
387
|
-
carrier_alleles = {variant.allele1, variant.allele2}
|
|
388
388
|
user_is_multibase = len(variant.allele1) > 1 or len(variant.allele2) > 1
|
|
389
389
|
# ADR-0023: report the user's actual diploid call consistently
|
|
390
390
|
# across annotators, not the matched ALT base alone.
|
|
@@ -404,7 +404,14 @@ class ClinVarAnnotator(Annotator):
|
|
|
404
404
|
clinvar_is_indel = len(ref) > 1 or len(alt) > 1
|
|
405
405
|
if clinvar_is_indel and not user_is_multibase:
|
|
406
406
|
continue
|
|
407
|
-
|
|
407
|
+
# ADR-0035 PR 4: strand-aware carrier match. Direct {ref, alt}
|
|
408
|
+
# membership still covers forward-stranded inputs; the
|
|
409
|
+
# complement-fallback path fires only when Variant.ref confirms
|
|
410
|
+
# reverse orientation AND the site isn't palindromic — preserving
|
|
411
|
+
# the v2.0.1 #18 multi-allelic safety invariant.
|
|
412
|
+
if not strand_aware_carrier_match(
|
|
413
|
+
variant.ref, variant.allele1, variant.allele2, ref, alt
|
|
414
|
+
):
|
|
408
415
|
continue
|
|
409
416
|
sig_label = _normalize_clnsig(clnsig) if clnsig else "unknown"
|
|
410
417
|
if not self._include_benign and sig_label in _BENIGN_CLNSIGS:
|
|
@@ -562,7 +569,6 @@ class ClinVarAnnotator(Annotator):
|
|
|
562
569
|
rows = rows_by_build_rsid.get((variant.build, variant.rsid))
|
|
563
570
|
if not rows:
|
|
564
571
|
continue
|
|
565
|
-
carrier_alleles = {variant.allele1, variant.allele2}
|
|
566
572
|
user_is_multibase = len(variant.allele1) > 1 or len(variant.allele2) > 1
|
|
567
573
|
user_diploid = _user_diploid(variant)
|
|
568
574
|
for row in rows:
|
|
@@ -580,7 +586,11 @@ class ClinVarAnnotator(Annotator):
|
|
|
580
586
|
clinvar_is_indel = len(ref) > 1 or len(alt) > 1
|
|
581
587
|
if clinvar_is_indel and not user_is_multibase:
|
|
582
588
|
continue
|
|
583
|
-
|
|
589
|
+
# ADR-0035 PR 4: strand-aware carrier match. See annotate() for
|
|
590
|
+
# the safety rationale (#18 multi-allelic invariant preserved).
|
|
591
|
+
if not strand_aware_carrier_match(
|
|
592
|
+
variant.ref, variant.allele1, variant.allele2, ref, alt
|
|
593
|
+
):
|
|
584
594
|
continue
|
|
585
595
|
sig_label = _normalize_clnsig(clnsig) if clnsig else "unknown"
|
|
586
596
|
if not self._include_benign and sig_label in _BENIGN_CLNSIGS:
|
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import contextlib
|
|
8
8
|
import logging
|
|
9
|
+
import re
|
|
9
10
|
import sqlite3
|
|
10
11
|
import zipfile
|
|
11
12
|
from typing import TYPE_CHECKING, ClassVar
|
|
@@ -56,6 +57,27 @@ logger = logging.getLogger(__name__)
|
|
|
56
57
|
|
|
57
58
|
_BATCH_CHUNK = 500 # SQLite default SQLITE_MAX_VARIABLE_NUMBER is 999
|
|
58
59
|
|
|
60
|
+
# ADR-0035 PR 3: PheCode often arrives embedded in the upstream DISEASE/TRAIT
|
|
61
|
+
# field (e.g. ``"Type 2 diabetes (PheCode 250.2)"``). The annotator extracts it
|
|
62
|
+
# at the boundary so downstream code reads ``Annotation.phecode`` as a
|
|
63
|
+
# structured field instead of regex-parsing prose. The trimmed trait flows into
|
|
64
|
+
# ``Annotation.trait``; ``description`` is rebuilt below from the structured
|
|
65
|
+
# parts so display surfaces still show the PheCode label.
|
|
66
|
+
_PHECODE_SUFFIX_RE = re.compile(r"\s*\(PheCode\s+([\d.]+)\)\s*$")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _split_trait_and_phecode(raw_trait: str) -> tuple[str, str]:
|
|
70
|
+
"""Split an upstream trait into (clean_trait, phecode).
|
|
71
|
+
|
|
72
|
+
PheCode (when present) is returned as a bare numeric string like
|
|
73
|
+
``"250.2"``. Returns ``""`` for phecode when no PheCode suffix is found.
|
|
74
|
+
The clean trait is stripped of trailing whitespace.
|
|
75
|
+
"""
|
|
76
|
+
m = _PHECODE_SUFFIX_RE.search(raw_trait)
|
|
77
|
+
if m is None:
|
|
78
|
+
return raw_trait.strip(), ""
|
|
79
|
+
return raw_trait[: m.start()].rstrip(), m.group(1)
|
|
80
|
+
|
|
59
81
|
|
|
60
82
|
def _magnitude(p_value: float | None, or_beta: float | None) -> float:
|
|
61
83
|
"""Derive magnitude from p-value and optional effect size.
|
|
@@ -288,9 +310,14 @@ class GWASCatalogAnnotator(Annotator):
|
|
|
288
310
|
mag = min(_magnitude(p_value, or_beta), _UNKNOWN_RISK_ALLELE_MAG_CAP)
|
|
289
311
|
risk_note = " (risk allele not specified in study)"
|
|
290
312
|
|
|
313
|
+
clean_trait, phecode = _split_trait_and_phecode(trait)
|
|
314
|
+
phecode_suffix = f" (PheCode {phecode})" if phecode else ""
|
|
291
315
|
p_str = f"p={p_value:.1e}" if p_value is not None else "p=N/A"
|
|
292
316
|
gene_str = gene or "—"
|
|
293
|
-
description =
|
|
317
|
+
description = (
|
|
318
|
+
f"GWAS Catalog: {clean_trait}{phecode_suffix} "
|
|
319
|
+
f"({p_str}, gene: {gene_str}){risk_note}"
|
|
320
|
+
)
|
|
294
321
|
|
|
295
322
|
references: list[str] = []
|
|
296
323
|
if pubmed_id:
|
|
@@ -309,10 +336,13 @@ class GWASCatalogAnnotator(Annotator):
|
|
|
309
336
|
attribution=self.attribution,
|
|
310
337
|
genotype_match=user_diploid,
|
|
311
338
|
references=references,
|
|
312
|
-
condition=
|
|
339
|
+
condition=clean_trait,
|
|
313
340
|
gene=gene or "",
|
|
314
|
-
alt="",
|
|
341
|
+
alt=risk_allele or "",
|
|
315
342
|
is_must_include=variant.rsid in _MUST_INCLUDE_RSIDS,
|
|
343
|
+
trait=clean_trait,
|
|
344
|
+
p_value=p_value,
|
|
345
|
+
phecode=phecode,
|
|
316
346
|
)
|
|
317
347
|
)
|
|
318
348
|
return annotations
|
|
@@ -377,9 +407,14 @@ class GWASCatalogAnnotator(Annotator):
|
|
|
377
407
|
mag = min(_magnitude(p_value, or_beta), _UNKNOWN_RISK_ALLELE_MAG_CAP)
|
|
378
408
|
risk_note = " (risk allele not specified in study)"
|
|
379
409
|
|
|
410
|
+
clean_trait, phecode = _split_trait_and_phecode(trait)
|
|
411
|
+
phecode_suffix = f" (PheCode {phecode})" if phecode else ""
|
|
380
412
|
p_str = f"p={p_value:.1e}" if p_value is not None else "p=N/A"
|
|
381
413
|
gene_str = gene or "—"
|
|
382
|
-
description =
|
|
414
|
+
description = (
|
|
415
|
+
f"GWAS Catalog: {clean_trait}{phecode_suffix} "
|
|
416
|
+
f"({p_str}, gene: {gene_str}){risk_note}"
|
|
417
|
+
)
|
|
383
418
|
|
|
384
419
|
references: list[str] = []
|
|
385
420
|
if pubmed_id:
|
|
@@ -397,10 +432,13 @@ class GWASCatalogAnnotator(Annotator):
|
|
|
397
432
|
attribution=self.attribution,
|
|
398
433
|
genotype_match=user_diploid,
|
|
399
434
|
references=references,
|
|
400
|
-
condition=
|
|
435
|
+
condition=clean_trait,
|
|
401
436
|
gene=gene or "",
|
|
402
|
-
alt="",
|
|
437
|
+
alt=risk_allele or "",
|
|
403
438
|
is_must_include=variant.rsid in _MUST_INCLUDE_RSIDS,
|
|
439
|
+
trait=clean_trait,
|
|
440
|
+
p_value=p_value,
|
|
441
|
+
phecode=phecode,
|
|
404
442
|
)
|
|
405
443
|
|
|
406
444
|
|
|
@@ -30,6 +30,7 @@ from allelix.databases.pharmgkb_loader import (
|
|
|
30
30
|
schema_is_current,
|
|
31
31
|
)
|
|
32
32
|
from allelix.models import Annotation
|
|
33
|
+
from allelix.utils.allele import derive_alt_from_diploid, strand_aware_genotype_match
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from collections.abc import Callable, Iterable, Iterator
|
|
@@ -260,20 +261,25 @@ class PharmGKBAnnotator(Annotator):
|
|
|
260
261
|
if is_clinvar_homref(variant, self._clinvar_ref_provider):
|
|
261
262
|
return []
|
|
262
263
|
|
|
264
|
+
# ADR-0035 PR 4: fetch all rows for the rsid (typically <5) and apply
|
|
265
|
+
# strand-aware genotype match in Python. Forward-stranded users hit
|
|
266
|
+
# the same set of rows as the v0.7-era ``WHERE genotype = ?`` SQL;
|
|
267
|
+
# reverse-stranded users now also match when Variant.ref confirms
|
|
268
|
+
# orientation AND the source genotype is non-palindromic.
|
|
263
269
|
sql = (
|
|
264
270
|
"SELECT genotype, gene, drugs, phenotype, phenotype_category, "
|
|
265
271
|
"annotation_text, level_of_evidence, score, pgkb_annotation_id "
|
|
266
272
|
"FROM pharmgkb_annotations "
|
|
267
|
-
"WHERE rsid = ? AND
|
|
273
|
+
"WHERE rsid = ? AND is_nonfinding = 0"
|
|
268
274
|
)
|
|
269
|
-
params = (variant.rsid,
|
|
275
|
+
params = (variant.rsid,)
|
|
270
276
|
|
|
271
277
|
rows = self._connection().execute(sql, params).fetchall()
|
|
272
278
|
annotations: list[Annotation] = []
|
|
273
279
|
user_diploid = _user_diploid(variant)
|
|
274
280
|
for row in rows:
|
|
275
281
|
(
|
|
276
|
-
|
|
282
|
+
geno,
|
|
277
283
|
gene,
|
|
278
284
|
drugs,
|
|
279
285
|
phenotype,
|
|
@@ -283,6 +289,10 @@ class PharmGKBAnnotator(Annotator):
|
|
|
283
289
|
_score,
|
|
284
290
|
pgkb_annotation_id,
|
|
285
291
|
) = row
|
|
292
|
+
if not strand_aware_genotype_match(
|
|
293
|
+
variant.allele1, variant.allele2, geno, variant.ref
|
|
294
|
+
):
|
|
295
|
+
continue
|
|
286
296
|
sig_label = level_of_evidence.strip().lower() or "unknown"
|
|
287
297
|
description_parts = [f"ClinPGx: {drugs}"] if drugs else ["ClinPGx"]
|
|
288
298
|
if phenotype:
|
|
@@ -306,6 +316,7 @@ class PharmGKBAnnotator(Annotator):
|
|
|
306
316
|
references=references,
|
|
307
317
|
condition=phenotype or "",
|
|
308
318
|
gene=gene or "",
|
|
319
|
+
alt=derive_alt_from_diploid(variant.ref, variant.allele1, variant.allele2),
|
|
309
320
|
)
|
|
310
321
|
)
|
|
311
322
|
return annotations
|
|
@@ -352,7 +363,7 @@ class PharmGKBAnnotator(Annotator):
|
|
|
352
363
|
for row in cursor:
|
|
353
364
|
rows_by_rsid.setdefault(row[0], []).append(row[1:])
|
|
354
365
|
|
|
355
|
-
for variant,
|
|
366
|
+
for variant, _user_geno in candidates:
|
|
356
367
|
rows = rows_by_rsid.get(variant.rsid)
|
|
357
368
|
if not rows:
|
|
358
369
|
continue
|
|
@@ -369,7 +380,11 @@ class PharmGKBAnnotator(Annotator):
|
|
|
369
380
|
_score,
|
|
370
381
|
pgkb_annotation_id,
|
|
371
382
|
) = row
|
|
372
|
-
|
|
383
|
+
# ADR-0035 PR 4: strand-aware genotype match. See annotate() for
|
|
384
|
+
# the safety rationale (palindrome guard + variant.ref orientation).
|
|
385
|
+
if not strand_aware_genotype_match(
|
|
386
|
+
variant.allele1, variant.allele2, geno, variant.ref
|
|
387
|
+
):
|
|
373
388
|
continue
|
|
374
389
|
sig_label = level_of_evidence.strip().lower() or "unknown"
|
|
375
390
|
description_parts = [f"ClinPGx: {drugs}"] if drugs else ["ClinPGx"]
|
|
@@ -393,6 +408,7 @@ class PharmGKBAnnotator(Annotator):
|
|
|
393
408
|
references=references,
|
|
394
409
|
condition=phenotype or "",
|
|
395
410
|
gene=gene or "",
|
|
411
|
+
alt=derive_alt_from_diploid(variant.ref, variant.allele1, variant.allele2),
|
|
396
412
|
)
|
|
397
413
|
|
|
398
414
|
|
|
@@ -29,6 +29,7 @@ from allelix.databases.snpedia_loader import (
|
|
|
29
29
|
install_prebuilt_cache,
|
|
30
30
|
)
|
|
31
31
|
from allelix.models import Annotation
|
|
32
|
+
from allelix.utils.allele import derive_alt_from_diploid
|
|
32
33
|
|
|
33
34
|
if TYPE_CHECKING:
|
|
34
35
|
from collections.abc import Callable, Iterable, Iterator
|
|
@@ -272,6 +273,7 @@ class SNPediaAnnotator(Annotator):
|
|
|
272
273
|
genotype_match=genotype_match,
|
|
273
274
|
references=[f"https://www.snpedia.com/index.php/{snp_url_path}"],
|
|
274
275
|
gene=gene or "",
|
|
276
|
+
alt=derive_alt_from_diploid(variant.ref, allele1, allele2),
|
|
275
277
|
)
|
|
276
278
|
)
|
|
277
279
|
|
|
@@ -356,4 +358,5 @@ class SNPediaAnnotator(Annotator):
|
|
|
356
358
|
genotype_match=genotype_match,
|
|
357
359
|
references=[f"https://www.snpedia.com/index.php/{snp_url_path}"],
|
|
358
360
|
gene=gene or "",
|
|
361
|
+
alt=derive_alt_from_diploid(variant.ref, allele1, allele2),
|
|
359
362
|
)
|