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.
Files changed (83) hide show
  1. {allelix-2.0.2 → allelix-2.1.1}/PKG-INFO +106 -8
  2. {allelix-2.0.2 → allelix-2.1.1}/README.md +105 -7
  3. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/base.py +28 -19
  4. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/clinvar.py +14 -4
  5. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/gwas.py +44 -6
  6. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/pharmgkb.py +21 -5
  7. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/snpedia.py +3 -0
  8. {allelix-2.0.2 → allelix-2.1.1}/allelix/models.py +63 -2
  9. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/vcf.py +1 -0
  10. {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/_pipeline.py +39 -58
  11. {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/diff.py +5 -3
  12. {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/json_report.py +9 -7
  13. allelix-2.1.1/allelix/utils/allele.py +211 -0
  14. {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/PKG-INFO +106 -8
  15. {allelix-2.0.2 → allelix-2.1.1}/pyproject.toml +1 -1
  16. {allelix-2.0.2 → allelix-2.1.1}/tests/test_models.py +73 -0
  17. allelix-2.0.2/allelix/utils/allele.py +0 -89
  18. {allelix-2.0.2 → allelix-2.1.1}/LICENSE +0 -0
  19. {allelix-2.0.2 → allelix-2.1.1}/allelix/__init__.py +0 -0
  20. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/__init__.py +0 -0
  21. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/alphamissense.py +0 -0
  22. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/cadd.py +0 -0
  23. {allelix-2.0.2 → allelix-2.1.1}/allelix/annotators/gnomad.py +0 -0
  24. {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/__init__.py +0 -0
  25. {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/_helpers.py +0 -0
  26. {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/_options.py +0 -0
  27. {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/analyze.py +0 -0
  28. {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/config.py +0 -0
  29. {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/db.py +0 -0
  30. {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/focused.py +0 -0
  31. {allelix-2.0.2 → allelix-2.1.1}/allelix/cli/utility.py +0 -0
  32. {allelix-2.0.2 → allelix-2.1.1}/allelix/compare.py +0 -0
  33. {allelix-2.0.2 → allelix-2.1.1}/allelix/config.py +0 -0
  34. {allelix-2.0.2 → allelix-2.1.1}/allelix/data/__init__.py +0 -0
  35. {allelix-2.0.2 → allelix-2.1.1}/allelix/data/clinvar_clnsig_snapshot.yaml +0 -0
  36. {allelix-2.0.2 → allelix-2.1.1}/allelix/data/high_value_snps.yaml +0 -0
  37. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/__init__.py +0 -0
  38. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/_versions.py +0 -0
  39. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/alphamissense_loader.py +0 -0
  40. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/cadd_loader.py +0 -0
  41. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/cpic_loader.py +0 -0
  42. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/gnomad_loader.py +0 -0
  43. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/gwas_loader.py +0 -0
  44. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/loader_utils.py +0 -0
  45. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/manager.py +0 -0
  46. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/pharmgkb_loader.py +0 -0
  47. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/schema.py +0 -0
  48. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/snpedia_loader.py +0 -0
  49. {allelix-2.0.2 → allelix-2.1.1}/allelix/databases/snpedia_parser.py +0 -0
  50. {allelix-2.0.2 → allelix-2.1.1}/allelix/exporters/__init__.py +0 -0
  51. {allelix-2.0.2 → allelix-2.1.1}/allelix/exporters/plink.py +0 -0
  52. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/__init__.py +0 -0
  53. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/_helpers.py +0 -0
  54. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/ancestrydna.py +0 -0
  55. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/base.py +0 -0
  56. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/ftdna.py +0 -0
  57. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/ftdna_illumina.py +0 -0
  58. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/livingdna.py +0 -0
  59. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/myhappygenes.py +0 -0
  60. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/myheritage.py +0 -0
  61. {allelix-2.0.2 → allelix-2.1.1}/allelix/parsers/twentythreeandme.py +0 -0
  62. {allelix-2.0.2 → allelix-2.1.1}/allelix/py.typed +0 -0
  63. {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/__init__.py +0 -0
  64. {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/high_value.py +0 -0
  65. {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/html.py +0 -0
  66. {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/methylation.py +0 -0
  67. {allelix-2.0.2 → allelix-2.1.1}/allelix/reports/terminal.py +0 -0
  68. {allelix-2.0.2 → allelix-2.1.1}/allelix/utils/__init__.py +0 -0
  69. {allelix-2.0.2 → allelix-2.1.1}/allelix/utils/build_detect.py +0 -0
  70. {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/SOURCES.txt +0 -0
  71. {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/dependency_links.txt +0 -0
  72. {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/entry_points.txt +0 -0
  73. {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/requires.txt +0 -0
  74. {allelix-2.0.2 → allelix-2.1.1}/allelix.egg-info/top_level.txt +0 -0
  75. {allelix-2.0.2 → allelix-2.1.1}/setup.cfg +0 -0
  76. {allelix-2.0.2 → allelix-2.1.1}/tests/test_cli.py +0 -0
  77. {allelix-2.0.2 → allelix-2.1.1}/tests/test_cli_helpers.py +0 -0
  78. {allelix-2.0.2 → allelix-2.1.1}/tests/test_compare.py +0 -0
  79. {allelix-2.0.2 → allelix-2.1.1}/tests/test_config.py +0 -0
  80. {allelix-2.0.2 → allelix-2.1.1}/tests/test_end_to_end.py +0 -0
  81. {allelix-2.0.2 → allelix-2.1.1}/tests/test_mock_data_invariants.py +0 -0
  82. {allelix-2.0.2 → allelix-2.1.1}/tests/test_registry.py +0 -0
  83. {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.0.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.0.0** — VCF +
51
- > gVCF parser with multi-sample handling, batched annotation pipeline
52
- > for WGS scale, FTDNA Illumina raw parser, R-4 ClinVar CLNSIG drift CI
53
- > test, CLI package restructure.
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 (~15GB with all sources).
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
- Requires Python 3.11+. See [Development](#development) for source installs and running tests.
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. 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%. **Opt-in** — disabled by default (`sources.cadd = false`). Enable via `allelix db update --cadd` or `allelix config set sources.cadd true`. Use `--no-cadd` to skip enrichment for a single run. Pre-built cache (~5 GB on disk, ~120M variant keys). Full mode available via pysam for GRCh38 data (`options.cadd_full = true`). Cache mode covers the large majority of variants present in gnomAD, AlphaMissense, and ClinVar — nearly every position allelix can annotate from its other databases. For genotyping chip data (23andMe, AncestryDNA, MyHappyGenes, etc.), cache and full mode produce effectively identical results because chip probes overwhelmingly target known, cataloged variants. Full mode adds coverage for novel or private variants that appear only in whole-genome or whole-exome sequencing data and are not in any pre-computed database. If your input is a genotyping chip file, cache mode is all you need. |
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.0.0** — VCF +
14
- > gVCF parser with multi-sample handling, batched annotation pipeline
15
- > for WGS scale, FTDNA Illumina raw parser, R-4 ClinVar CLNSIG drift CI
16
- > test, CLI package restructure.
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 (~15GB with all sources).
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
- Requires Python 3.11+. See [Development](#development) for source installs and running tests.
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. 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%. **Opt-in** — disabled by default (`sources.cadd = false`). Enable via `allelix db update --cadd` or `allelix config set sources.cadd true`. Use `--no-cadd` to skip enrichment for a single run. Pre-built cache (~5 GB on disk, ~120M variant keys). Full mode available via pysam for GRCh38 data (`options.cadd_full = true`). Cache mode covers the large majority of variants present in gnomAD, AlphaMissense, and ClinVar — nearly every position allelix can annotate from its other databases. For genotyping chip data (23andMe, AncestryDNA, MyHappyGenes, etc.), cache and full mode produce effectively identical results because chip probes overwhelmingly target known, cataloged variants. Full mode adds coverage for novel or private variants that appear only in whole-genome or whole-exome sequencing data and are not in any pre-computed database. If your input is a genotyping chip file, cache mode is all you need. |
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
- GH #36 (audit second pass) flagged ``__del__`` as a Python
148
- antipattern — GC timing is nondeterministic and raised exceptions
149
- are silently swallowed. The correct usage pattern is the
150
- ``__enter__`` / ``__exit__`` context manager pair below, wired
151
- through ``contextlib.ExitStack`` in ``reports/_pipeline.py``.
152
-
153
- However: removing ``__del__`` exposes residual SQLite connection
154
- leaks in code paths that construct an annotator outside a
155
- context manager. ``ResourceWarning`` is elevated to error by
156
- ``pytest`` config, so leaks fail the suite as
157
- ``PytestUnraisableExceptionWarning`` — caught in the v2.0.2
158
- ship gate when ``__del__`` was first removed. Until every call
159
- site is verified to use ``with`` / ``ExitStack`` / explicit
160
- ``close()``, this safety net stays. v2.1 task: audit and
161
- remove.
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. The GC timing and shutdown-ordering edges
165
- are explicitly silenced; this is exactly the
166
- "if you must keep ``__del__``, make absolutely sure it can
167
- never raise" mitigation the audit recommended.
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
- if alt not in carrier_alleles:
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
- if alt not in carrier_alleles:
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 = f"GWAS Catalog: {trait} ({p_str}, gene: {gene_str}){risk_note}"
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=trait,
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 = f"GWAS Catalog: {trait} ({p_str}, gene: {gene_str}){risk_note}"
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=trait,
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 genotype = ? AND is_nonfinding = 0"
273
+ "WHERE rsid = ? AND is_nonfinding = 0"
268
274
  )
269
- params = (variant.rsid, user_geno)
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
- _geno,
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, user_geno in candidates:
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
- if geno != user_geno:
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
  )