allelix 2.1.2__tar.gz → 2.2.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.1.2 → allelix-2.2.1}/PKG-INFO +28 -9
- {allelix-2.1.2 → allelix-2.2.1}/README.md +27 -8
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/clinvar.py +240 -68
- {allelix-2.1.2 → allelix-2.2.1}/allelix/cli/_helpers.py +59 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/cli/_options.py +19 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/cli/config.py +10 -18
- {allelix-2.1.2 → allelix-2.2.1}/allelix/cli/db.py +242 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/cli/focused.py +3 -3
- {allelix-2.1.2 → allelix-2.2.1}/allelix/config.py +31 -13
- {allelix-2.1.2 → allelix-2.2.1}/allelix/data/clinvar_clnsig_snapshot.yaml +13 -1
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/_versions.py +7 -1
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/gwas_loader.py +10 -0
- allelix-2.2.1/allelix/databases/manager.py +774 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/__init__.py +2 -0
- allelix-2.2.1/allelix/parsers/ftdna_famfinder.py +247 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/vcf.py +26 -4
- {allelix-2.1.2 → allelix-2.2.1}/allelix/reports/__init__.py +3 -3
- {allelix-2.1.2 → allelix-2.2.1}/allelix/reports/_pipeline.py +178 -1
- {allelix-2.1.2 → allelix-2.2.1}/allelix/reports/diff.py +1 -1
- {allelix-2.1.2 → allelix-2.2.1}/allelix/reports/html.py +81 -1
- {allelix-2.1.2 → allelix-2.2.1}/allelix/reports/json_report.py +14 -2
- {allelix-2.1.2 → allelix-2.2.1}/allelix/reports/terminal.py +48 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/utils/allele.py +18 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix.egg-info/PKG-INFO +28 -9
- {allelix-2.1.2 → allelix-2.2.1}/allelix.egg-info/SOURCES.txt +2 -0
- {allelix-2.1.2 → allelix-2.2.1}/pyproject.toml +24 -7
- allelix-2.2.1/tests/test_changelog_sanitize.py +83 -0
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_cli.py +521 -42
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_config.py +23 -0
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_mock_data_invariants.py +0 -137
- allelix-2.1.2/allelix/databases/manager.py +0 -532
- {allelix-2.1.2 → allelix-2.2.1}/LICENSE +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/__init__.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/__init__.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/alphamissense.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/base.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/cadd.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/gnomad.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/gwas.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/pharmgkb.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/annotators/snpedia.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/cli/__init__.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/cli/analyze.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/cli/utility.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/compare.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/data/__init__.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/data/high_value_snps.yaml +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/__init__.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/alphamissense_loader.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/cadd_loader.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/cpic_loader.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/gnomad_loader.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/loader_utils.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/pharmgkb_loader.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/schema.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/snpedia_loader.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/databases/snpedia_parser.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/exporters/__init__.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/exporters/plink.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/models.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/_helpers.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/ancestrydna.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/base.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/ftdna.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/ftdna_illumina.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/livingdna.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/myhappygenes.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/myheritage.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/parsers/twentythreeandme.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/py.typed +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/reports/high_value.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/reports/methylation.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/utils/__init__.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix/utils/build_detect.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix.egg-info/dependency_links.txt +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix.egg-info/entry_points.txt +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix.egg-info/requires.txt +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/allelix.egg-info/top_level.txt +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/setup.cfg +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_cli_helpers.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_compare.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_end_to_end.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_models.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_registry.py +0 -0
- {allelix-2.1.2 → allelix-2.2.1}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: allelix
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.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>
|
|
@@ -95,6 +95,7 @@ See [Development](#development) for source installs and running tests, [Managing
|
|
|
95
95
|
| MyHeritage DNA | ✓ | CSV, same structure as FTDNA. Detected by "MyHeritage" in comment header. Handles double-double-quoted field variant. |
|
|
96
96
|
| Living DNA | ✓ | Tab-delimited despite `.csv` extension. Handles AX-, AFFX-prefixed and CHR:POS positional SNP IDs. |
|
|
97
97
|
| FTDNA Illumina raw | ✓ | Tab-delimited variant of the FTDNA export (distinct from the CSV format above). `RSID/CHROMOSOME/POSITION/RESULT` columns. Build 37 default. |
|
|
98
|
+
| FTDNA FamFinder | ✓ | Third FTDNA file shape: tab-delimited with **separate `ALLELE1` / `ALLELE2` columns** instead of a concatenated `RESULT`. Detected by the `famfinder` substring in the file header plus the 5-column canonical header. Build 37 default. |
|
|
98
99
|
| VCF / gVCF | ✓ | REF/ALT encoding, `0/1` genotype notation. Plain VCF: absence at a position means reference. gVCF: explicit reference blocks (lines with `<NON_REF>` ALT and `END=` INFO) are skipped — they match nothing in any annotation database. Multi-sample files require `--sample <ID>`. Streams via stdlib; `.vcf.gz` handled transparently. Optional `pip install allelix[vcf-index]` enables pysam-backed tabix random access for fast `extract --snps` on huge VCFs. |
|
|
99
100
|
|
|
100
101
|
Adding a new format means adding one file to `allelix/parsers/` and registering an instance in the `PARSERS` list in `allelix/parsers/__init__.py`.
|
|
@@ -253,28 +254,44 @@ Allelix stores reference database caches at `~/.local/share/allelix/` (Linux/mac
|
|
|
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):
|
|
254
255
|
|
|
255
256
|
```bash
|
|
256
|
-
# Reclaim all ~16-22 GB
|
|
257
|
-
|
|
257
|
+
# Reclaim all ~16-22 GB, with size report and confirmation prompt
|
|
258
|
+
allelix db clean
|
|
258
259
|
|
|
259
|
-
#
|
|
260
|
-
|
|
260
|
+
# Preserve the CADD cache (avoids re-downloading the slowest one, ~5.8 GB)
|
|
261
|
+
allelix db clean --keep-cadd
|
|
262
|
+
|
|
263
|
+
# Show what would be deleted without acting
|
|
264
|
+
allelix db clean --dry-run
|
|
265
|
+
|
|
266
|
+
# Scripted use — skip the confirmation prompt
|
|
267
|
+
allelix db clean --yes
|
|
261
268
|
|
|
262
269
|
# Re-populate from scratch next time you analyze a file
|
|
263
270
|
allelix db update
|
|
264
271
|
```
|
|
265
272
|
|
|
266
|
-
|
|
273
|
+
`db clean` refuses to act on a directory that doesn't contain any recognized allelix cache files — a typo'd `--data-dir ~/Documents` won't result in `rm -rf` against an unrelated tree, even with `--yes`. Pass `--force` if you've staged caches under a non-standard name and want to delete them anyway.
|
|
274
|
+
|
|
275
|
+
For scripting and backup integration, `allelix db path` prints the resolved data directory:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
ALLELIX_DATA=$(allelix db path)
|
|
279
|
+
du -sh "$ALLELIX_DATA"
|
|
280
|
+
|
|
281
|
+
# --check additionally verifies the path is writable; exit non-zero if not
|
|
282
|
+
allelix db path --check
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**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. `allelix db clean` preserves `config.toml` even when it lives inside the data directory (older installs).
|
|
267
286
|
|
|
268
287
|
To fully uninstall allelix and reclaim everything:
|
|
269
288
|
|
|
270
289
|
```bash
|
|
271
290
|
pip uninstall allelix
|
|
272
|
-
|
|
291
|
+
allelix db clean --yes # reference database caches
|
|
273
292
|
rm -rf ~/.config/allelix/ # license + source-toggle config
|
|
274
293
|
```
|
|
275
294
|
|
|
276
|
-
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).
|
|
277
|
-
|
|
278
295
|
## Data Sources & Licensing
|
|
279
296
|
|
|
280
297
|
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:
|
|
@@ -383,6 +400,8 @@ pytest
|
|
|
383
400
|
|
|
384
401
|
The pre-commit hook enforces `ruff check` + `ruff format --check`. If a commit is blocked, fix the underlying problem rather than skipping the hook.
|
|
385
402
|
|
|
403
|
+
For the full real-data release-validation battery — every parser format, VCF/gVCF, PLINK round-trip, edge cases, the GIAB/1000G corpus — see [`test_data/FULL_TEST_PROTOCOL.md`](test_data/FULL_TEST_PROTOCOL.md). That document is the project's real-data testing story; running `pytest` covers the fast suite that gates every commit.
|
|
404
|
+
|
|
386
405
|
## License
|
|
387
406
|
|
|
388
407
|
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See `LICENSE`.
|
|
@@ -56,6 +56,7 @@ See [Development](#development) for source installs and running tests, [Managing
|
|
|
56
56
|
| MyHeritage DNA | ✓ | CSV, same structure as FTDNA. Detected by "MyHeritage" in comment header. Handles double-double-quoted field variant. |
|
|
57
57
|
| Living DNA | ✓ | Tab-delimited despite `.csv` extension. Handles AX-, AFFX-prefixed and CHR:POS positional SNP IDs. |
|
|
58
58
|
| FTDNA Illumina raw | ✓ | Tab-delimited variant of the FTDNA export (distinct from the CSV format above). `RSID/CHROMOSOME/POSITION/RESULT` columns. Build 37 default. |
|
|
59
|
+
| FTDNA FamFinder | ✓ | Third FTDNA file shape: tab-delimited with **separate `ALLELE1` / `ALLELE2` columns** instead of a concatenated `RESULT`. Detected by the `famfinder` substring in the file header plus the 5-column canonical header. Build 37 default. |
|
|
59
60
|
| VCF / gVCF | ✓ | REF/ALT encoding, `0/1` genotype notation. Plain VCF: absence at a position means reference. gVCF: explicit reference blocks (lines with `<NON_REF>` ALT and `END=` INFO) are skipped — they match nothing in any annotation database. Multi-sample files require `--sample <ID>`. Streams via stdlib; `.vcf.gz` handled transparently. Optional `pip install allelix[vcf-index]` enables pysam-backed tabix random access for fast `extract --snps` on huge VCFs. |
|
|
60
61
|
|
|
61
62
|
Adding a new format means adding one file to `allelix/parsers/` and registering an instance in the `PARSERS` list in `allelix/parsers/__init__.py`.
|
|
@@ -214,28 +215,44 @@ Allelix stores reference database caches at `~/.local/share/allelix/` (Linux/mac
|
|
|
214
215
|
**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):
|
|
215
216
|
|
|
216
217
|
```bash
|
|
217
|
-
# Reclaim all ~16-22 GB
|
|
218
|
-
|
|
218
|
+
# Reclaim all ~16-22 GB, with size report and confirmation prompt
|
|
219
|
+
allelix db clean
|
|
219
220
|
|
|
220
|
-
#
|
|
221
|
-
|
|
221
|
+
# Preserve the CADD cache (avoids re-downloading the slowest one, ~5.8 GB)
|
|
222
|
+
allelix db clean --keep-cadd
|
|
223
|
+
|
|
224
|
+
# Show what would be deleted without acting
|
|
225
|
+
allelix db clean --dry-run
|
|
226
|
+
|
|
227
|
+
# Scripted use — skip the confirmation prompt
|
|
228
|
+
allelix db clean --yes
|
|
222
229
|
|
|
223
230
|
# Re-populate from scratch next time you analyze a file
|
|
224
231
|
allelix db update
|
|
225
232
|
```
|
|
226
233
|
|
|
227
|
-
|
|
234
|
+
`db clean` refuses to act on a directory that doesn't contain any recognized allelix cache files — a typo'd `--data-dir ~/Documents` won't result in `rm -rf` against an unrelated tree, even with `--yes`. Pass `--force` if you've staged caches under a non-standard name and want to delete them anyway.
|
|
235
|
+
|
|
236
|
+
For scripting and backup integration, `allelix db path` prints the resolved data directory:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
ALLELIX_DATA=$(allelix db path)
|
|
240
|
+
du -sh "$ALLELIX_DATA"
|
|
241
|
+
|
|
242
|
+
# --check additionally verifies the path is writable; exit non-zero if not
|
|
243
|
+
allelix db path --check
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**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. `allelix db clean` preserves `config.toml` even when it lives inside the data directory (older installs).
|
|
228
247
|
|
|
229
248
|
To fully uninstall allelix and reclaim everything:
|
|
230
249
|
|
|
231
250
|
```bash
|
|
232
251
|
pip uninstall allelix
|
|
233
|
-
|
|
252
|
+
allelix db clean --yes # reference database caches
|
|
234
253
|
rm -rf ~/.config/allelix/ # license + source-toggle config
|
|
235
254
|
```
|
|
236
255
|
|
|
237
|
-
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).
|
|
238
|
-
|
|
239
256
|
## Data Sources & Licensing
|
|
240
257
|
|
|
241
258
|
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:
|
|
@@ -344,6 +361,8 @@ pytest
|
|
|
344
361
|
|
|
345
362
|
The pre-commit hook enforces `ruff check` + `ruff format --check`. If a commit is blocked, fix the underlying problem rather than skipping the hook.
|
|
346
363
|
|
|
364
|
+
For the full real-data release-validation battery — every parser format, VCF/gVCF, PLINK round-trip, edge cases, the GIAB/1000G corpus — see [`test_data/FULL_TEST_PROTOCOL.md`](test_data/FULL_TEST_PROTOCOL.md). That document is the project's real-data testing story; running `pytest` covers the fast suite that gates every commit.
|
|
365
|
+
|
|
347
366
|
## License
|
|
348
367
|
|
|
349
368
|
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See `LICENSE`.
|
|
@@ -25,7 +25,7 @@ from allelix.databases.manager import (
|
|
|
25
25
|
download,
|
|
26
26
|
fetch_remote_text,
|
|
27
27
|
get_database_info,
|
|
28
|
-
|
|
28
|
+
load_clinvar_tsv,
|
|
29
29
|
stamp_existing_clinvar_cache,
|
|
30
30
|
verify_file_hash,
|
|
31
31
|
)
|
|
@@ -92,9 +92,99 @@ def _magnitude(clnsig: str) -> float:
|
|
|
92
92
|
return _CLNSIG_MAGNITUDE.get(_normalize_clnsig(clnsig), 5.0)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
# GH #109: ClinVar review-status ranking — lower index = stronger
|
|
96
|
+
# evidence per ClinVar's star-rating documentation. Used to pick the
|
|
97
|
+
# winning row when N SCVs with identical (significance, condition,
|
|
98
|
+
# gene) tuples collapse during per-SCV dedup; the surviving Annotation
|
|
99
|
+
# carries the highest-rated review_status across the group instead of
|
|
100
|
+
# silently dropping provenance.
|
|
101
|
+
#
|
|
102
|
+
# Real cache rows use the comma-and-underscore form
|
|
103
|
+
# (`criteria_provided,_multiple_submitters,_no_conflicts`). The rank
|
|
104
|
+
# helper normalizes via _normalize_clnsig so it's robust to spacing
|
|
105
|
+
# variation. Unknown values fall through to "weakest known."
|
|
106
|
+
_REVIEW_STATUS_RANK: tuple[str, ...] = (
|
|
107
|
+
"practice_guideline",
|
|
108
|
+
"reviewed_by_expert_panel",
|
|
109
|
+
"criteria_provided,_multiple_submitters,_no_conflicts",
|
|
110
|
+
"criteria_provided,_conflicting_classifications",
|
|
111
|
+
"criteria_provided,_conflicting_interpretations",
|
|
112
|
+
"criteria_provided,_single_submitter",
|
|
113
|
+
"no_assertion_criteria_provided",
|
|
114
|
+
"no_assertion_provided",
|
|
115
|
+
"no_classification_provided",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _review_status_rank(status: str) -> int:
|
|
120
|
+
"""Return the rank index for a review_status string.
|
|
121
|
+
|
|
122
|
+
Lower index = stronger evidence. Unknown / unrecognized values get
|
|
123
|
+
the max rank (weakest), so a known SCV always beats an unknown one
|
|
124
|
+
when collapsing.
|
|
125
|
+
"""
|
|
126
|
+
if not status:
|
|
127
|
+
return len(_REVIEW_STATUS_RANK)
|
|
128
|
+
normalized = _normalize_clnsig(status)
|
|
129
|
+
for i, candidate in enumerate(_REVIEW_STATUS_RANK):
|
|
130
|
+
if candidate == normalized:
|
|
131
|
+
return i
|
|
132
|
+
return len(_REVIEW_STATUS_RANK)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _collapse_identical_scvs(annotations: list[Annotation]) -> list[Annotation]:
|
|
136
|
+
"""Collapse identical per-SCV rows from the #42 TSV loader (GH #109).
|
|
137
|
+
|
|
138
|
+
Two Annotations are identical for dedup purposes if they share
|
|
139
|
+
(rsid, significance, condition, gene, alt). The surviving row carries:
|
|
140
|
+
|
|
141
|
+
- the strongest ``review_status`` across the collapsed group
|
|
142
|
+
(per ``_REVIEW_STATUS_RANK``)
|
|
143
|
+
- the union of ``references`` across the collapsed group
|
|
144
|
+
(preserving first-seen order — never silently drops provenance)
|
|
145
|
+
|
|
146
|
+
Distinct (significance, condition) pairs on the same variant are
|
|
147
|
+
preserved as separate rows — that's exactly what #42 was built to
|
|
148
|
+
surface, and collapsing them would be the Frankenstein-pair
|
|
149
|
+
regression #42 retired.
|
|
150
|
+
|
|
151
|
+
GH #109 cross-PR review (Finding 1): ``alt`` is in the key for the
|
|
152
|
+
#18 wrong-allele safety invariant. Two SCVs at a multi-allelic site
|
|
153
|
+
can share (significance, condition, gene) but differ in alt — e.g.
|
|
154
|
+
rs1065852 (G→A drug_response/Codeine_response + G→C drug_response/
|
|
155
|
+
Codeine_response would be a hypothetical). Collapsing across alts
|
|
156
|
+
would (a) drop a distinct-allele annotation and (b) mis-attach the
|
|
157
|
+
surviving row's ``alt`` field to the merged result, breaking the
|
|
158
|
+
downstream exact-(rsid, alt) lookup that gnomAD / AlphaMissense /
|
|
159
|
+
CADD use for allele-specific enrichment. The pre-#109 v2.2.0 code
|
|
160
|
+
emitted one Annotation per cache row, so this case was always
|
|
161
|
+
handled correctly; the post-#109 dedup must preserve that property.
|
|
162
|
+
|
|
163
|
+
Input order is preserved for the kept rows: each group is replaced
|
|
164
|
+
by its winner at the position of the group's first occurrence.
|
|
165
|
+
"""
|
|
166
|
+
from dataclasses import replace
|
|
167
|
+
|
|
168
|
+
groups: dict[tuple[str, str, str, str, str], list[Annotation]] = {}
|
|
169
|
+
for a in annotations:
|
|
170
|
+
key = (a.rsid, a.significance, a.condition, a.gene, a.alt)
|
|
171
|
+
groups.setdefault(key, []).append(a)
|
|
172
|
+
|
|
173
|
+
out: list[Annotation] = []
|
|
174
|
+
for group in groups.values():
|
|
175
|
+
if len(group) == 1:
|
|
176
|
+
out.append(group[0])
|
|
177
|
+
continue
|
|
178
|
+
winner = min(group, key=lambda a: _review_status_rank(a.review_status))
|
|
179
|
+
seen_refs: set[str] = set()
|
|
180
|
+
merged_refs: list[str] = []
|
|
181
|
+
for a in group:
|
|
182
|
+
for ref in a.references:
|
|
183
|
+
if ref not in seen_refs:
|
|
184
|
+
seen_refs.add(ref)
|
|
185
|
+
merged_refs.append(ref)
|
|
186
|
+
out.append(replace(winner, references=merged_refs))
|
|
187
|
+
return out
|
|
98
188
|
|
|
99
189
|
|
|
100
190
|
class ClinVarAnnotator(Annotator):
|
|
@@ -186,39 +276,88 @@ class ClinVarAnnotator(Annotator):
|
|
|
186
276
|
return self._conns[build]
|
|
187
277
|
|
|
188
278
|
def setup(self) -> None:
|
|
189
|
-
"""Download
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
279
|
+
"""Download the per-SCV TSV pair once and ingest both builds from it.
|
|
280
|
+
|
|
281
|
+
v2.2 #42 stage B: the VCF-per-build path is replaced by the
|
|
282
|
+
variant_summary + submission_summary TSV pair. variant_summary
|
|
283
|
+
carries per-(VariationID, Assembly) rows so the same downloaded
|
|
284
|
+
file populates both GRCh37 and GRCh38 caches with no second
|
|
285
|
+
download. submission_summary is build-agnostic; one fetch
|
|
286
|
+
applies across builds.
|
|
287
|
+
|
|
288
|
+
Freshness signal is derived from submission_summary's md5
|
|
289
|
+
endpoint — that file drives the meaningful change cadence
|
|
290
|
+
(per-SCV submissions land daily) and variant_summary turns over
|
|
291
|
+
weekly on a slower clock.
|
|
292
|
+
"""
|
|
293
|
+
signal = self._fetch_remote_signal_for_tsv()
|
|
196
294
|
if signal is None:
|
|
197
295
|
msg = (
|
|
198
|
-
|
|
296
|
+
"clinvar: cannot verify remote freshness signal. "
|
|
199
297
|
"Refresh aborted to avoid persisting an incomplete cache stamp. "
|
|
200
298
|
"Retry, or pass --force if you accept that next `db update` "
|
|
201
299
|
"will re-download to re-establish the signal."
|
|
202
300
|
)
|
|
203
301
|
raise RuntimeError(msg)
|
|
204
|
-
|
|
205
|
-
|
|
302
|
+
|
|
303
|
+
vs_url = _manager_module.CLINVAR_VARIANT_SUMMARY_URL
|
|
304
|
+
ss_url = _manager_module.CLINVAR_SUBMISSION_SUMMARY_URL
|
|
305
|
+
vs_path = self.data_dir / "variant_summary.txt.gz"
|
|
306
|
+
ss_path = self.data_dir / "submission_summary.txt.gz"
|
|
307
|
+
|
|
308
|
+
download(vs_url, vs_path)
|
|
309
|
+
download(ss_url, ss_path)
|
|
310
|
+
# We don't md5-verify variant_summary; signal is calibrated against
|
|
311
|
+
# submission_summary (the more frequently-updated file) and that
|
|
312
|
+
# endpoint's md5 is what we hashed below. variant_summary integrity
|
|
313
|
+
# is asserted structurally by the loader (rows that fail to parse
|
|
314
|
+
# are skipped, mismatched assemblies are dropped).
|
|
206
315
|
try:
|
|
207
|
-
verify_file_hash(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
self._db_paths[build],
|
|
211
|
-
source_url=url,
|
|
212
|
-
remote_signal=signal,
|
|
213
|
-
record_name=clinvar_record_name(build),
|
|
214
|
-
)
|
|
316
|
+
verify_file_hash(ss_path, "md5", signal.removeprefix("md5:"))
|
|
317
|
+
for build in self._builds:
|
|
318
|
+
self._setup_one_from_tsv(vs_path, ss_path, build, signal, vs_url)
|
|
215
319
|
finally:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
320
|
+
for tmp in (vs_path, ss_path):
|
|
321
|
+
try:
|
|
322
|
+
tmp.unlink()
|
|
323
|
+
except FileNotFoundError:
|
|
324
|
+
pass
|
|
325
|
+
except OSError:
|
|
326
|
+
logger.warning("Could not remove staged TSV at %s", tmp)
|
|
327
|
+
|
|
328
|
+
def _setup_one_from_tsv(
|
|
329
|
+
self,
|
|
330
|
+
vs_path: Path,
|
|
331
|
+
ss_path: Path,
|
|
332
|
+
build: str,
|
|
333
|
+
signal: str,
|
|
334
|
+
source_url: str,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Run the per-build cache build against an already-downloaded TSV pair."""
|
|
337
|
+
load_clinvar_tsv(
|
|
338
|
+
vs_path,
|
|
339
|
+
ss_path,
|
|
340
|
+
self._db_paths[build],
|
|
341
|
+
build,
|
|
342
|
+
source_url=source_url,
|
|
343
|
+
remote_signal=signal,
|
|
344
|
+
)
|
|
345
|
+
# load_clinvar_tsv stamps the record name as "clinvar"; for
|
|
346
|
+
# backward compatibility with the per-build record-name scheme
|
|
347
|
+
# (clinvar.GRCh37 / clinvar.GRCh38), rename the row in place.
|
|
348
|
+
# This keeps is_ready() / version() per-build dispatch working
|
|
349
|
+
# against the existing database_versions table layout.
|
|
350
|
+
import contextlib
|
|
351
|
+
import sqlite3
|
|
352
|
+
|
|
353
|
+
per_build_name = clinvar_record_name(build)
|
|
354
|
+
if per_build_name != "clinvar":
|
|
355
|
+
with contextlib.closing(sqlite3.connect(self._db_paths[build])) as conn:
|
|
356
|
+
conn.execute(
|
|
357
|
+
"UPDATE database_versions SET name = ? WHERE name = 'clinvar'",
|
|
358
|
+
(per_build_name,),
|
|
359
|
+
)
|
|
360
|
+
conn.commit()
|
|
222
361
|
|
|
223
362
|
def is_ready(self) -> bool:
|
|
224
363
|
"""True iff EVERY managed build has a populated, version-stamped cache.
|
|
@@ -300,11 +439,15 @@ class ClinVarAnnotator(Annotator):
|
|
|
300
439
|
# can't suppress a single-base array readout. The per-build cache
|
|
301
440
|
# may have BOTH SNV and indel rows for the same rsid; the WHERE
|
|
302
441
|
# filters those out so we keep only the SNV REF.
|
|
303
|
-
|
|
442
|
+
#
|
|
443
|
+
# GH #28 item 1: iterate the cursor directly instead of fetchall()
|
|
444
|
+
# so peak memory is one row + the dict, not the entire result set
|
|
445
|
+
# twice.
|
|
446
|
+
cursor = conn.execute(
|
|
304
447
|
"SELECT DISTINCT rsid, ref FROM clinvar_variants WHERE length(ref) = 1"
|
|
305
|
-
)
|
|
448
|
+
)
|
|
306
449
|
out: dict[str, str] = {}
|
|
307
|
-
for rsid, ref in
|
|
450
|
+
for rsid, ref in cursor:
|
|
308
451
|
# If a rsid has multiple single-base REFs (shouldn't happen at
|
|
309
452
|
# one position but defending against future data shapes), keep
|
|
310
453
|
# the first.
|
|
@@ -313,23 +456,21 @@ class ClinVarAnnotator(Annotator):
|
|
|
313
456
|
return out
|
|
314
457
|
|
|
315
458
|
def fetch_remote_signal(self) -> str | None:
|
|
316
|
-
r"""
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
459
|
+
r"""Freshness signal derived from the per-SCV submission_summary md5.
|
|
460
|
+
|
|
461
|
+
v2.2 #42 stage B: pre-stage-B this returned a composite VCF-md5
|
|
462
|
+
per build. After stage B, the cache is built from a build-agnostic
|
|
463
|
+
TSV pair, and submission_summary's md5 endpoint is the change
|
|
464
|
+
cadence we want to track (per-SCV submissions land daily; the
|
|
465
|
+
slower-moving variant_summary only adds new VariationIDs).
|
|
466
|
+
Returns the signal as ``"md5:<hex>"`` to keep ``setup()``'s
|
|
467
|
+
``signal.removeprefix("md5:")`` contract unchanged.
|
|
321
468
|
"""
|
|
322
|
-
|
|
323
|
-
for build in self._builds:
|
|
324
|
-
sig = self._fetch_remote_signal_for(build)
|
|
325
|
-
if sig is None:
|
|
326
|
-
return None
|
|
327
|
-
parts.append(f"{build}:{sig}")
|
|
328
|
-
return "|".join(parts) if parts else None
|
|
469
|
+
return self._fetch_remote_signal_for_tsv()
|
|
329
470
|
|
|
330
471
|
@staticmethod
|
|
331
|
-
def
|
|
332
|
-
body = fetch_remote_text(_manager_module.
|
|
472
|
+
def _fetch_remote_signal_for_tsv() -> str | None:
|
|
473
|
+
body = fetch_remote_text(_manager_module.CLINVAR_SUBMISSION_SUMMARY_URL + ".md5")
|
|
333
474
|
if not body:
|
|
334
475
|
return None
|
|
335
476
|
first_token = body.strip().split(None, 1)[0] if body.strip() else ""
|
|
@@ -338,19 +479,29 @@ class ClinVarAnnotator(Annotator):
|
|
|
338
479
|
# as a transient signal failure rather than poisoning the
|
|
339
480
|
# cache: callers handle `None` as "freshness unknown, skip"
|
|
340
481
|
# in `db update`, and `setup()` raises rather than passing
|
|
341
|
-
# garbage to `verify_file_hash` (which would delete the
|
|
482
|
+
# garbage to `verify_file_hash` (which would delete the TSV).
|
|
342
483
|
logger.warning(
|
|
343
|
-
"clinvar
|
|
344
|
-
"is not a 32-char hex digest (got %r); treating as
|
|
345
|
-
|
|
484
|
+
"clinvar: submission_summary.md5 endpoint returned a body whose "
|
|
485
|
+
"first token is not a 32-char hex digest (got %r); treating as "
|
|
486
|
+
"no signal",
|
|
346
487
|
first_token[:32],
|
|
347
488
|
)
|
|
348
489
|
return None
|
|
349
490
|
return f"md5:{first_token}"
|
|
350
491
|
|
|
351
492
|
def cached_remote_signal(self) -> str | None:
|
|
352
|
-
"""
|
|
353
|
-
|
|
493
|
+
"""Return the signal stamped at the last successful setup, or None.
|
|
494
|
+
|
|
495
|
+
None if any managed build's cache is absent / unstamped or if the
|
|
496
|
+
builds' stamps diverge (an interrupted partial reingest).
|
|
497
|
+
|
|
498
|
+
Stage B: both builds now share the same source TSVs and therefore
|
|
499
|
+
the same remote_signal. We still cross-check that EVERY managed
|
|
500
|
+
build's cache carries the stamp; a missing build means the
|
|
501
|
+
last-setup didn't complete, treat as "no signal" and let
|
|
502
|
+
``db update`` decide.
|
|
503
|
+
"""
|
|
504
|
+
signals: set[str] = set()
|
|
354
505
|
for build in self._builds:
|
|
355
506
|
info = get_database_info(self._db_paths[build], clinvar_record_name(build))
|
|
356
507
|
if info is None or info["remote_signal"] is None:
|
|
@@ -358,8 +509,16 @@ class ClinVarAnnotator(Annotator):
|
|
|
358
509
|
sig = info["remote_signal"]
|
|
359
510
|
if not sig:
|
|
360
511
|
return None
|
|
361
|
-
|
|
362
|
-
|
|
512
|
+
signals.add(sig)
|
|
513
|
+
if not signals:
|
|
514
|
+
return None
|
|
515
|
+
# All builds should carry the same signal (single download, single
|
|
516
|
+
# ingest run). If they diverge — e.g. someone reingested one build
|
|
517
|
+
# against a newer TSV — treat as "no signal" so the next
|
|
518
|
+
# `db update` re-syncs both.
|
|
519
|
+
if len(signals) > 1:
|
|
520
|
+
return None
|
|
521
|
+
return signals.pop()
|
|
363
522
|
|
|
364
523
|
def annotate(self, variant: Variant) -> list[Annotation]:
|
|
365
524
|
"""Return ClinVar annotations whose REF/ALT matches the user's genotype.
|
|
@@ -438,7 +597,11 @@ class ClinVarAnnotator(Annotator):
|
|
|
438
597
|
alt=alt,
|
|
439
598
|
)
|
|
440
599
|
)
|
|
441
|
-
|
|
600
|
+
# GH #109: collapse identical per-SCV rows from the #42 TSV loader.
|
|
601
|
+
# Without this, a variant with N identical Pathogenic submissions
|
|
602
|
+
# emits N identical Annotation objects (visible duplicates on every
|
|
603
|
+
# surface). Distinct (significance, condition) pairs survive.
|
|
604
|
+
return _collapse_identical_scvs(annotations)
|
|
442
605
|
|
|
443
606
|
def bulk_resolve_rsids(self, variants: list[Variant]) -> dict[tuple[str, int, str, str], str]:
|
|
444
607
|
"""Resolve rsIDs by (chromosome, position) for variants with no ID.
|
|
@@ -572,6 +735,12 @@ class ClinVarAnnotator(Annotator):
|
|
|
572
735
|
continue
|
|
573
736
|
user_is_multibase = len(variant.allele1) > 1 or len(variant.allele2) > 1
|
|
574
737
|
user_diploid = _user_diploid(variant)
|
|
738
|
+
# GH #109: collect per-variant Annotations and dedup before
|
|
739
|
+
# yielding, mirroring the annotate() collapse. Without this,
|
|
740
|
+
# bulk callers (the analyze pipeline goes through
|
|
741
|
+
# batch_annotate) leak the same per-SCV duplicates the
|
|
742
|
+
# single-variant path used to.
|
|
743
|
+
variant_annotations: list[Annotation] = []
|
|
575
744
|
for row in rows:
|
|
576
745
|
(
|
|
577
746
|
_chrom,
|
|
@@ -601,21 +770,24 @@ class ClinVarAnnotator(Annotator):
|
|
|
601
770
|
f"{clnsig.replace('_', ' ') if clnsig else 'unknown significance'}"
|
|
602
771
|
)
|
|
603
772
|
references = [f"clinvar:allele/{allele_id}"] if allele_id else []
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
773
|
+
variant_annotations.append(
|
|
774
|
+
Annotation(
|
|
775
|
+
source=self.name,
|
|
776
|
+
rsid=variant.rsid,
|
|
777
|
+
significance=f"clinvar_{sig_label}",
|
|
778
|
+
category="clinical",
|
|
779
|
+
magnitude=_magnitude(clnsig),
|
|
780
|
+
description=description,
|
|
781
|
+
attribution=self.attribution,
|
|
782
|
+
genotype_match=user_diploid,
|
|
783
|
+
references=references,
|
|
784
|
+
condition="" if not condition or condition == "." else condition,
|
|
785
|
+
gene=gene or "",
|
|
786
|
+
review_status=review_status or "",
|
|
787
|
+
alt=alt,
|
|
788
|
+
)
|
|
618
789
|
)
|
|
790
|
+
yield from _collapse_identical_scvs(variant_annotations)
|
|
619
791
|
|
|
620
792
|
|
|
621
793
|
def _user_diploid(variant: Variant) -> str:
|
|
@@ -417,6 +417,63 @@ def _emit_build_diagnostics(result: object) -> None:
|
|
|
417
417
|
)
|
|
418
418
|
|
|
419
419
|
|
|
420
|
+
def _emit_runtime_nudges(result: object) -> None:
|
|
421
|
+
"""Surface runtime-degradation notes the analyze run can detect.
|
|
422
|
+
|
|
423
|
+
Both are once-per-run, post-pipeline. Neither blocks the report; the
|
|
424
|
+
intent is transparency for cases where the silent default behavior
|
|
425
|
+
used to leave the user uncertain about WHY their count looked low.
|
|
426
|
+
|
|
427
|
+
- **GH #90: strand-aware matching inactive.** When carrier/genotype
|
|
428
|
+
matching ran with ``Variant.ref`` unavailable across the input —
|
|
429
|
+
typically array data with ``--no-gnomad`` or no gnomAD cache —
|
|
430
|
+
the strand-flip path in ``utils/allele.py`` falls back to the
|
|
431
|
+
v2.0.x direct-match-only behavior. Behavior is correct (forward-
|
|
432
|
+
normalized array data matches the direct path regardless), just
|
|
433
|
+
degraded relative to v2.1+'s contract. VCF inputs always carry
|
|
434
|
+
REF from the file, so this never fires for them.
|
|
435
|
+
- **GH #91: GRCh38 rsID-less undercount.** When the effective
|
|
436
|
+
build is GRCh38 AND most variants entered the pipeline without
|
|
437
|
+
rsIDs (the variant-caller ``ID=.`` case), annotation coverage
|
|
438
|
+
is materially lower than the GRCh37 equivalent because rsID
|
|
439
|
+
resolution falls back to ClinVar position lookup — a smaller
|
|
440
|
+
surface than dbSNP. Real fix is #62; this is the interim
|
|
441
|
+
honesty signal until then.
|
|
442
|
+
"""
|
|
443
|
+
no_ref = getattr(result, "no_ref_variant_count", 0)
|
|
444
|
+
rsidless = getattr(result, "rsidless_variant_count", 0)
|
|
445
|
+
total = getattr(result, "total_variants", 0)
|
|
446
|
+
parser_name = getattr(result, "parser_name", "")
|
|
447
|
+
diag = getattr(result, "build_diagnostics", None)
|
|
448
|
+
effective_build = diag.effective_build if diag is not None else None
|
|
449
|
+
|
|
450
|
+
# GH #90: array inputs (everything except the VCF parser) running
|
|
451
|
+
# without a usable gnomAD cache leave every variant ref=None. Gate
|
|
452
|
+
# on "the whole input lacked ref" rather than "any variant lacked
|
|
453
|
+
# ref" so a hybrid file (e.g. a few ref-less rows in an otherwise
|
|
454
|
+
# populated VCF) doesn't flap the message on.
|
|
455
|
+
if parser_name != "vcf" and total > 0 and no_ref == total:
|
|
456
|
+
console.print(
|
|
457
|
+
"[dim]strand-aware matching inactive — no reference context "
|
|
458
|
+
"(run `allelix db update` for gnomAD-backed ref resolution, "
|
|
459
|
+
"or this is expected with `--no-gnomad`)[/dim]"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# GH #91: GRCh38 + a high rsID-less fraction. The threshold (>50%)
|
|
463
|
+
# distinguishes a variant-caller VCF (mostly ID=. — fires) from a
|
|
464
|
+
# 23andMe-style array (rsID-bearing — no warning) or a mostly-
|
|
465
|
+
# rsID-bearing VCF where a handful of novel calls came in with
|
|
466
|
+
# ID=. (no warning, ID=. for novel sites is expected).
|
|
467
|
+
if effective_build == "GRCh38" and total > 0 and rsidless / total > 0.5:
|
|
468
|
+
console.print(
|
|
469
|
+
"[yellow]GRCh38 input without rsIDs: annotation coverage is "
|
|
470
|
+
"currently lower than GRCh37 because rsID resolution falls "
|
|
471
|
+
"back to ClinVar positions. Full dbSNP resolution is planned "
|
|
472
|
+
"(#62). If you have a GRCh37 build available, it currently "
|
|
473
|
+
"surfaces more annotations.[/yellow]"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
420
477
|
def _run_analysis_command(
|
|
421
478
|
file_path: Path,
|
|
422
479
|
fmt: str | None,
|
|
@@ -496,11 +553,13 @@ def _run_analysis_command(
|
|
|
496
553
|
alphamissense=cast("AlphaMissenseAnnotator | None", am_annotator),
|
|
497
554
|
cadd=cast("CaddAnnotator | None", cadd_annotator),
|
|
498
555
|
high_value_rsids=hv_rsids,
|
|
556
|
+
panel_rsids=rsids,
|
|
499
557
|
)
|
|
500
558
|
finally:
|
|
501
559
|
_unwire_parser_logging(counter, stderr_handler, snapshot)
|
|
502
560
|
|
|
503
561
|
_emit_build_diagnostics(result)
|
|
562
|
+
_emit_runtime_nudges(result)
|
|
504
563
|
|
|
505
564
|
hv_warnings = scan_no_calls(result.hv_variants, high_value)
|
|
506
565
|
if hv_warnings:
|
|
@@ -33,6 +33,25 @@ _MIN_MAG_OPT = click.option(
|
|
|
33
33
|
show_default=True,
|
|
34
34
|
help="Filter annotations below this magnitude. Use 0 for the full unfiltered set.",
|
|
35
35
|
)
|
|
36
|
+
# The focused subcommands (`allelix methylation`, `allelix pharmacogenomics`)
|
|
37
|
+
# default to a lower floor than the general `analyze` report. The intent of
|
|
38
|
+
# a focused report is to surface focused-level signal — ClinPGx LoE 3 hits
|
|
39
|
+
# on COMT / MTHFR / MTR / CBS for `methylation`, similar for the pharma
|
|
40
|
+
# subcommand — which the magnitude-5 floor that protects `analyze` from
|
|
41
|
+
# noise would filter out as if they were noise. Floor 3.0 keeps LoE 3
|
|
42
|
+
# in, keeps ClinVar Benign (mag 1.0) out.
|
|
43
|
+
_FOCUSED_MIN_MAG_OPT = click.option(
|
|
44
|
+
"--min-magnitude",
|
|
45
|
+
type=float,
|
|
46
|
+
default=3.0,
|
|
47
|
+
show_default=True,
|
|
48
|
+
help=(
|
|
49
|
+
"Filter annotations below this magnitude. Focused subcommands default "
|
|
50
|
+
"to 3.0 (vs. analyze's 5.0) so ClinPGx LoE 3 hits on panel genes — "
|
|
51
|
+
"the intended signal of a focused report — aren't filtered out. "
|
|
52
|
+
"Use 0 for the full unfiltered set."
|
|
53
|
+
),
|
|
54
|
+
)
|
|
36
55
|
_OUTPUT_OPT = click.option(
|
|
37
56
|
"--output",
|
|
38
57
|
type=click.Path(dir_okay=False, path_type=Path),
|