allelix 2.1.1__tar.gz → 2.1.2__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 (82) hide show
  1. {allelix-2.1.1 → allelix-2.1.2}/PKG-INFO +29 -30
  2. {allelix-2.1.1 → allelix-2.1.2}/README.md +26 -29
  3. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/__init__.py +10 -12
  4. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/base.py +0 -38
  5. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/cadd.py +9 -7
  6. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/clinvar.py +3 -2
  7. {allelix-2.1.1 → allelix-2.1.2}/allelix/cli/_helpers.py +7 -4
  8. {allelix-2.1.1 → allelix-2.1.2}/allelix/cli/utility.py +6 -36
  9. {allelix-2.1.1 → allelix-2.1.2}/allelix/config.py +3 -1
  10. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/cpic_loader.py +2 -2
  11. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/gwas_loader.py +2 -2
  12. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/snpedia_parser.py +6 -4
  13. {allelix-2.1.1 → allelix-2.1.2}/allelix/exporters/plink.py +50 -1
  14. {allelix-2.1.1 → allelix-2.1.2}/allelix/reports/_pipeline.py +10 -5
  15. {allelix-2.1.1 → allelix-2.1.2}/allelix.egg-info/PKG-INFO +29 -30
  16. {allelix-2.1.1 → allelix-2.1.2}/allelix.egg-info/requires.txt +2 -0
  17. {allelix-2.1.1 → allelix-2.1.2}/pyproject.toml +37 -2
  18. {allelix-2.1.1 → allelix-2.1.2}/LICENSE +0 -0
  19. {allelix-2.1.1 → allelix-2.1.2}/allelix/__init__.py +0 -0
  20. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/alphamissense.py +0 -0
  21. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/gnomad.py +0 -0
  22. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/gwas.py +0 -0
  23. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/pharmgkb.py +0 -0
  24. {allelix-2.1.1 → allelix-2.1.2}/allelix/annotators/snpedia.py +0 -0
  25. {allelix-2.1.1 → allelix-2.1.2}/allelix/cli/__init__.py +0 -0
  26. {allelix-2.1.1 → allelix-2.1.2}/allelix/cli/_options.py +0 -0
  27. {allelix-2.1.1 → allelix-2.1.2}/allelix/cli/analyze.py +0 -0
  28. {allelix-2.1.1 → allelix-2.1.2}/allelix/cli/config.py +0 -0
  29. {allelix-2.1.1 → allelix-2.1.2}/allelix/cli/db.py +0 -0
  30. {allelix-2.1.1 → allelix-2.1.2}/allelix/cli/focused.py +0 -0
  31. {allelix-2.1.1 → allelix-2.1.2}/allelix/compare.py +0 -0
  32. {allelix-2.1.1 → allelix-2.1.2}/allelix/data/__init__.py +0 -0
  33. {allelix-2.1.1 → allelix-2.1.2}/allelix/data/clinvar_clnsig_snapshot.yaml +0 -0
  34. {allelix-2.1.1 → allelix-2.1.2}/allelix/data/high_value_snps.yaml +0 -0
  35. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/__init__.py +0 -0
  36. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/_versions.py +0 -0
  37. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/alphamissense_loader.py +0 -0
  38. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/cadd_loader.py +0 -0
  39. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/gnomad_loader.py +0 -0
  40. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/loader_utils.py +0 -0
  41. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/manager.py +0 -0
  42. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/pharmgkb_loader.py +0 -0
  43. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/schema.py +0 -0
  44. {allelix-2.1.1 → allelix-2.1.2}/allelix/databases/snpedia_loader.py +0 -0
  45. {allelix-2.1.1 → allelix-2.1.2}/allelix/exporters/__init__.py +0 -0
  46. {allelix-2.1.1 → allelix-2.1.2}/allelix/models.py +0 -0
  47. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/__init__.py +0 -0
  48. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/_helpers.py +0 -0
  49. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/ancestrydna.py +0 -0
  50. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/base.py +0 -0
  51. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/ftdna.py +0 -0
  52. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/ftdna_illumina.py +0 -0
  53. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/livingdna.py +0 -0
  54. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/myhappygenes.py +0 -0
  55. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/myheritage.py +0 -0
  56. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/twentythreeandme.py +0 -0
  57. {allelix-2.1.1 → allelix-2.1.2}/allelix/parsers/vcf.py +0 -0
  58. {allelix-2.1.1 → allelix-2.1.2}/allelix/py.typed +0 -0
  59. {allelix-2.1.1 → allelix-2.1.2}/allelix/reports/__init__.py +0 -0
  60. {allelix-2.1.1 → allelix-2.1.2}/allelix/reports/diff.py +0 -0
  61. {allelix-2.1.1 → allelix-2.1.2}/allelix/reports/high_value.py +0 -0
  62. {allelix-2.1.1 → allelix-2.1.2}/allelix/reports/html.py +0 -0
  63. {allelix-2.1.1 → allelix-2.1.2}/allelix/reports/json_report.py +0 -0
  64. {allelix-2.1.1 → allelix-2.1.2}/allelix/reports/methylation.py +0 -0
  65. {allelix-2.1.1 → allelix-2.1.2}/allelix/reports/terminal.py +0 -0
  66. {allelix-2.1.1 → allelix-2.1.2}/allelix/utils/__init__.py +0 -0
  67. {allelix-2.1.1 → allelix-2.1.2}/allelix/utils/allele.py +0 -0
  68. {allelix-2.1.1 → allelix-2.1.2}/allelix/utils/build_detect.py +0 -0
  69. {allelix-2.1.1 → allelix-2.1.2}/allelix.egg-info/SOURCES.txt +0 -0
  70. {allelix-2.1.1 → allelix-2.1.2}/allelix.egg-info/dependency_links.txt +0 -0
  71. {allelix-2.1.1 → allelix-2.1.2}/allelix.egg-info/entry_points.txt +0 -0
  72. {allelix-2.1.1 → allelix-2.1.2}/allelix.egg-info/top_level.txt +0 -0
  73. {allelix-2.1.1 → allelix-2.1.2}/setup.cfg +0 -0
  74. {allelix-2.1.1 → allelix-2.1.2}/tests/test_cli.py +0 -0
  75. {allelix-2.1.1 → allelix-2.1.2}/tests/test_cli_helpers.py +0 -0
  76. {allelix-2.1.1 → allelix-2.1.2}/tests/test_compare.py +0 -0
  77. {allelix-2.1.1 → allelix-2.1.2}/tests/test_config.py +0 -0
  78. {allelix-2.1.1 → allelix-2.1.2}/tests/test_end_to_end.py +0 -0
  79. {allelix-2.1.1 → allelix-2.1.2}/tests/test_mock_data_invariants.py +0 -0
  80. {allelix-2.1.1 → allelix-2.1.2}/tests/test_models.py +0 -0
  81. {allelix-2.1.1 → allelix-2.1.2}/tests/test_registry.py +0 -0
  82. {allelix-2.1.1 → allelix-2.1.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: allelix
3
- Version: 2.1.1
3
+ Version: 2.1.2
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>
@@ -29,35 +29,33 @@ Requires-Dist: pysam>=0.22; extra == "cadd"
29
29
  Provides-Extra: vcf-index
30
30
  Requires-Dist: pysam>=0.22; extra == "vcf-index"
31
31
  Provides-Extra: dev
32
+ Requires-Dist: mypy>=1.11; extra == "dev"
32
33
  Requires-Dist: pre-commit>=3.7; extra == "dev"
33
34
  Requires-Dist: pytest>=8.0; extra == "dev"
34
35
  Requires-Dist: pytest-cov<8,>=7; extra == "dev"
35
36
  Requires-Dist: ruff>=0.6; extra == "dev"
37
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
36
38
  Dynamic: license-file
37
39
 
40
+ <div align="center">
41
+
42
+ <img src="https://allelix.io/apple-touch-icon.png" width="180" alt="Allelix" />
43
+
44
+ **Open-source genotype analysis. Format-agnostic. Offline-first.**
45
+
46
+ [Website](https://allelix.io) · [Quickstart](#quickstart) · [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md)
47
+
48
+ [![python](https://img.shields.io/pypi/pyversions/allelix.svg)](https://www.python.org/downloads/)
49
+ [![pypi](https://img.shields.io/pypi/v/allelix.svg)](https://pypi.org/project/allelix/)
50
+ [![license](https://img.shields.io/pypi/l/allelix.svg)](LICENSE)
51
+ [![CI](https://github.com/allelix/allelix/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/allelix/allelix/actions/workflows/ci.yml)
52
+ [![privacy: local-first](https://img.shields.io/badge/privacy-local--first-success)](#privacy)
53
+
54
+ </div>
55
+
38
56
  # Allelix
39
57
 
40
- Open-source command-line toolkit for analyzing raw genotype files from consumer DNA testing services. Format-agnostic ingestion, database-agnostic annotation, offline-first.
41
-
42
- > **Status:** Production — eight parser formats (including VCF + gVCF),
43
- > four annotators (ClinVar + ClinPGx + GWAS Catalog + SNPedia), three
44
- > enrichment sources (gnomAD population frequencies + AlphaMissense
45
- > pathogenicity + CADD deleteriousness), licensable-source gating for
46
- > commercial users, dual-build ClinVar caches (GRCh37 + GRCh38),
47
- > HTML/JSON/terminal reports, methylation + pharmacogenomics focused
48
- > commands, report diffing, persistent config with commercial-mode
49
- > safety switch. Build auto-detection from position data (ADR-0021).
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).
59
- > Release notes:
60
- > [`CHANGELOG.md`](https://github.com/allelix/allelix/blob/main/CHANGELOG.md).
58
+ > **Status:** Production. Eight parser formats (including VCF + gVCF), four annotators (ClinVar + ClinPGx + GWAS Catalog + SNPedia), three enrichment sources (gnomAD + AlphaMissense + CADD), licensable-source gating for commercial users, dual-build ClinVar caches (GRCh37 + GRCh38), HTML/JSON/terminal reports, methylation + pharmacogenomics focused commands, report diffing, persistent config with commercial-mode safety switch. Build auto-detection from position data (ADR-0021). No regex on prose anywhere in production. The [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md) tracks every release.
61
59
 
62
60
  ## Quickstart
63
61
 
@@ -101,16 +99,17 @@ See [Development](#development) for source installs and running tests, [Managing
101
99
 
102
100
  Adding a new format means adding one file to `allelix/parsers/` and registering an instance in the `PARSERS` list in `allelix/parsers/__init__.py`.
103
101
 
104
- ### v2.1+ roadmap
102
+ ### Roadmap
105
103
 
106
- | Feature | Notes |
104
+ | Release | Theme |
107
105
  |---|---|
108
- | Per-source scoring | Magnitude breakdown by database. Users see which source drove the composite score. |
109
- | Annotator-level strand awareness (R-1) | Strand-flip matching wired into every annotator's carrier check. Basic `compare` strand support shipped in v1.1; full annotator integration deferred from v2.0.0. |
110
- | Good / Bad / Neutral repute | Per-annotation repute field. Reframes the report from "here's what's wrong" to "here's your full picture." Requires Annotation model change + renderer updates. |
111
- | PLINK import | Read .bed/.bim/.fam as an input format (complement to the v1.7.0 export). |
112
- | PharmCAT integration | Wrap CPIC's PharmCAT as an optional external engine for star-allele / diplotype calling. Requires VCF input (shipped in v2.0.0). |
113
- | Genome Watchtower | Real-time variant monitoring via database delta feeds. Privacy-preserving: server publishes universal feed, matching happens locally against your deviation set. Replaces full re-analysis with millisecond set intersection. |
106
+ | **v2.2** | Annotation model expansion (cross-source conflict surfacing, risk-allele display, structured MTAG flag), custom-panel UX improvements, methylation panel retune + citations, ClinVar source switch (VCF → `variant_summary.txt`), CLI cleanups (`db clean` / `db path`). |
107
+ | **v2.3** | Per-source magnitude decomposition, Good / Bad / Neutral annotation repute, JSON v3 (AI-legible output contract), plausibility flagging via zygosity × gnomAD MAF, ClinVar review-status weighting, functional-medicine +/− notation. |
108
+ | **v2.4** | ACMG/AMP automated classification engine, variant consequence annotation (VEP-equivalent). |
109
+ | **v2.5+** | CNVs and repeat-expansion support, dbSNP resolution for rsID-less VCFs, gnomAD genomes (non-coding population frequencies), supplemental genotype file merging. |
110
+ | **v3.0** | Ancestry estimation (PCA + 1KG/HGDP reference panels), Polygenic Risk Score (PRS) integration. |
111
+
112
+ Issues and milestones are tracked at [github.com/allelix/allelix/issues](https://github.com/allelix/allelix/issues). Annotator-level strand-aware carrier matching (R-1) shipped in v2.1.0's ADR-0035 cluster.
114
113
 
115
114
  ## Supported Databases
116
115
 
@@ -1,26 +1,22 @@
1
+ <div align="center">
2
+
3
+ <img src="https://allelix.io/apple-touch-icon.png" width="180" alt="Allelix" />
4
+
5
+ **Open-source genotype analysis. Format-agnostic. Offline-first.**
6
+
7
+ [Website](https://allelix.io) · [Quickstart](#quickstart) · [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md)
8
+
9
+ [![python](https://img.shields.io/pypi/pyversions/allelix.svg)](https://www.python.org/downloads/)
10
+ [![pypi](https://img.shields.io/pypi/v/allelix.svg)](https://pypi.org/project/allelix/)
11
+ [![license](https://img.shields.io/pypi/l/allelix.svg)](LICENSE)
12
+ [![CI](https://github.com/allelix/allelix/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/allelix/allelix/actions/workflows/ci.yml)
13
+ [![privacy: local-first](https://img.shields.io/badge/privacy-local--first-success)](#privacy)
14
+
15
+ </div>
16
+
1
17
  # Allelix
2
18
 
3
- Open-source command-line toolkit for analyzing raw genotype files from consumer DNA testing services. Format-agnostic ingestion, database-agnostic annotation, offline-first.
4
-
5
- > **Status:** Production — eight parser formats (including VCF + gVCF),
6
- > four annotators (ClinVar + ClinPGx + GWAS Catalog + SNPedia), three
7
- > enrichment sources (gnomAD population frequencies + AlphaMissense
8
- > pathogenicity + CADD deleteriousness), licensable-source gating for
9
- > commercial users, dual-build ClinVar caches (GRCh37 + GRCh38),
10
- > HTML/JSON/terminal reports, methylation + pharmacogenomics focused
11
- > commands, report diffing, persistent config with commercial-mode
12
- > safety switch. Build auto-detection from position data (ADR-0021).
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).
22
- > Release notes:
23
- > [`CHANGELOG.md`](https://github.com/allelix/allelix/blob/main/CHANGELOG.md).
19
+ > **Status:** Production. Eight parser formats (including VCF + gVCF), four annotators (ClinVar + ClinPGx + GWAS Catalog + SNPedia), three enrichment sources (gnomAD + AlphaMissense + CADD), licensable-source gating for commercial users, dual-build ClinVar caches (GRCh37 + GRCh38), HTML/JSON/terminal reports, methylation + pharmacogenomics focused commands, report diffing, persistent config with commercial-mode safety switch. Build auto-detection from position data (ADR-0021). No regex on prose anywhere in production. The [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md) tracks every release.
24
20
 
25
21
  ## Quickstart
26
22
 
@@ -64,16 +60,17 @@ See [Development](#development) for source installs and running tests, [Managing
64
60
 
65
61
  Adding a new format means adding one file to `allelix/parsers/` and registering an instance in the `PARSERS` list in `allelix/parsers/__init__.py`.
66
62
 
67
- ### v2.1+ roadmap
63
+ ### Roadmap
68
64
 
69
- | Feature | Notes |
65
+ | Release | Theme |
70
66
  |---|---|
71
- | Per-source scoring | Magnitude breakdown by database. Users see which source drove the composite score. |
72
- | Annotator-level strand awareness (R-1) | Strand-flip matching wired into every annotator's carrier check. Basic `compare` strand support shipped in v1.1; full annotator integration deferred from v2.0.0. |
73
- | Good / Bad / Neutral repute | Per-annotation repute field. Reframes the report from "here's what's wrong" to "here's your full picture." Requires Annotation model change + renderer updates. |
74
- | PLINK import | Read .bed/.bim/.fam as an input format (complement to the v1.7.0 export). |
75
- | PharmCAT integration | Wrap CPIC's PharmCAT as an optional external engine for star-allele / diplotype calling. Requires VCF input (shipped in v2.0.0). |
76
- | Genome Watchtower | Real-time variant monitoring via database delta feeds. Privacy-preserving: server publishes universal feed, matching happens locally against your deviation set. Replaces full re-analysis with millisecond set intersection. |
67
+ | **v2.2** | Annotation model expansion (cross-source conflict surfacing, risk-allele display, structured MTAG flag), custom-panel UX improvements, methylation panel retune + citations, ClinVar source switch (VCF → `variant_summary.txt`), CLI cleanups (`db clean` / `db path`). |
68
+ | **v2.3** | Per-source magnitude decomposition, Good / Bad / Neutral annotation repute, JSON v3 (AI-legible output contract), plausibility flagging via zygosity × gnomAD MAF, ClinVar review-status weighting, functional-medicine +/− notation. |
69
+ | **v2.4** | ACMG/AMP automated classification engine, variant consequence annotation (VEP-equivalent). |
70
+ | **v2.5+** | CNVs and repeat-expansion support, dbSNP resolution for rsID-less VCFs, gnomAD genomes (non-coding population frequencies), supplemental genotype file merging. |
71
+ | **v3.0** | Ancestry estimation (PCA + 1KG/HGDP reference panels), Polygenic Risk Score (PRS) integration. |
72
+
73
+ Issues and milestones are tracked at [github.com/allelix/allelix/issues](https://github.com/allelix/allelix/issues). Annotator-level strand-aware carrier matching (R-1) shipped in v2.1.0's ADR-0035 cluster.
77
74
 
78
75
  ## Supported Databases
79
76
 
@@ -57,18 +57,16 @@ def get_annotators(
57
57
  return [clinvar, pharmgkb, gwas, snpedia, gnomad, alphamissense, cadd]
58
58
 
59
59
 
60
- _ANNOTATOR_CLASSES: dict[str, type[Annotator]] = {
61
- cls.name: cls
62
- for cls in [
63
- ClinVarAnnotator,
64
- PharmGKBAnnotator,
65
- GWASCatalogAnnotator,
66
- SNPediaAnnotator,
67
- GnomadAnnotator,
68
- AlphaMissenseAnnotator,
69
- CaddAnnotator,
70
- ]
71
- }
60
+ _ANNOTATOR_CLASS_LIST: list[type[Annotator]] = [
61
+ ClinVarAnnotator,
62
+ PharmGKBAnnotator,
63
+ GWASCatalogAnnotator,
64
+ SNPediaAnnotator,
65
+ GnomadAnnotator,
66
+ AlphaMissenseAnnotator,
67
+ CaddAnnotator,
68
+ ]
69
+ _ANNOTATOR_CLASSES: dict[str, type[Annotator]] = {cls.name: cls for cls in _ANNOTATOR_CLASS_LIST}
72
70
 
73
71
 
74
72
  def get_annotator_class(name: str) -> type[Annotator] | None:
@@ -4,7 +4,6 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- import contextlib
8
7
  from abc import ABC, abstractmethod
9
8
  from dataclasses import dataclass
10
9
  from enum import Enum, auto
@@ -141,43 +140,6 @@ class Annotator(ABC):
141
140
  """Bind the annotator to a data directory (created elsewhere)."""
142
141
  self.data_dir = data_dir
143
142
 
144
- def __del__(self) -> None:
145
- """Safety-net resource release on GC. Deliberately retained.
146
-
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.
171
-
172
- ``contextlib.suppress(Exception)`` is deliberate — ``__del__``
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.
177
- """
178
- with contextlib.suppress(Exception):
179
- self.close()
180
-
181
143
  def __enter__(self) -> Annotator:
182
144
  """Return self for `with` usage."""
183
145
  return self
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import logging
26
26
  import sqlite3
27
- from typing import TYPE_CHECKING, ClassVar
27
+ from typing import TYPE_CHECKING, Any, ClassVar
28
28
 
29
29
  from allelix.annotators.base import Annotator, LicenseDescriptor
30
30
  from allelix.databases._versions import CADD_SCHEMA_VERSION
@@ -90,9 +90,11 @@ class CaddAnnotator(Annotator):
90
90
  self._conn: sqlite3.Connection | None = None
91
91
  self._full_mode = full_mode
92
92
  self._tabix_path = data_dir / CADD_FULL_FILENAME
93
- self._tabix: object | None = None
93
+ # pysam ships no type stubs (see [tool.mypy.overrides]); the tabix
94
+ # handle is held as Any so call sites can use `.fetch()` / `.close()`.
95
+ self._tabix: Any = None
94
96
  self._indel_tabix_path = data_dir / CADD_INDEL_FILENAME
95
- self._indel_tabix: object | None = None
97
+ self._indel_tabix: Any = None
96
98
 
97
99
  def _connection(self) -> sqlite3.Connection:
98
100
  if self._conn is None:
@@ -104,11 +106,11 @@ class CaddAnnotator(Annotator):
104
106
  self._conn = sqlite3.connect(self._db_path)
105
107
  return self._conn
106
108
 
107
- def _open_tabix(self) -> object:
109
+ def _open_tabix(self) -> Any: # noqa: ANN401 — pysam ships no type stubs
108
110
  """Open the tabix file for full-mode queries."""
109
111
  if self._tabix is None:
110
112
  try:
111
- import pysam # type: ignore[import-untyped]
113
+ import pysam
112
114
  except ImportError:
113
115
  raise ImportError(
114
116
  "Full CADD mode requires pysam. Install with: pip install 'allelix[cadd]'"
@@ -122,7 +124,7 @@ class CaddAnnotator(Annotator):
122
124
  self._tabix = pysam.TabixFile(str(self._tabix_path))
123
125
  return self._tabix
124
126
 
125
- def _open_indel_tabix(self) -> object | None:
127
+ def _open_indel_tabix(self) -> Any: # noqa: ANN401 — pysam ships no type stubs
126
128
  """Open the indel tabix file. Returns None if file doesn't exist."""
127
129
  if self._indel_tabix is None:
128
130
  if not self._indel_tabix_path.exists():
@@ -131,7 +133,7 @@ class CaddAnnotator(Annotator):
131
133
  if not tbi.exists():
132
134
  return None
133
135
  try:
134
- import pysam # type: ignore[import-untyped]
136
+ import pysam
135
137
  except ImportError:
136
138
  return None
137
139
  self._indel_tabix = pysam.TabixFile(str(self._indel_tabix_path))
@@ -500,9 +500,10 @@ class ClinVarAnnotator(Annotator):
500
500
  for rows in clinvar_rows.values():
501
501
  rows.sort()
502
502
  for v in build_variants:
503
- rows = clinvar_rows.get((v.chromosome, v.position))
504
- if not rows:
503
+ pos_rows = clinvar_rows.get((v.chromosome, v.position))
504
+ if not pos_rows:
505
505
  continue
506
+ rows = pos_rows
506
507
  user_alleles = {v.allele1, v.allele2}
507
508
  # Stamp only on a UNIQUE consistent match. Multiple matches
508
509
  # mean the Variant model can't distinguish them (the 1/1
@@ -16,7 +16,7 @@ import logging
16
16
  import re
17
17
  import sys
18
18
  import time
19
- from typing import TYPE_CHECKING, NamedTuple
19
+ from typing import TYPE_CHECKING, NamedTuple, cast
20
20
 
21
21
  import click
22
22
  from rich.console import Console
@@ -34,7 +34,10 @@ from allelix.reports.terminal import render_terminal, render_terminal_diff
34
34
  if TYPE_CHECKING:
35
35
  from pathlib import Path
36
36
 
37
+ from allelix.annotators.alphamissense import AlphaMissenseAnnotator
37
38
  from allelix.annotators.base import Annotator
39
+ from allelix.annotators.cadd import CaddAnnotator
40
+ from allelix.annotators.gnomad import GnomadAnnotator
38
41
  from allelix.parsers.base import GenotypeParser
39
42
 
40
43
  console = Console()
@@ -489,9 +492,9 @@ def _run_analysis_command(
489
492
  ready,
490
493
  skipped_count_provider=lambda: counter.count,
491
494
  build_override=build,
492
- gnomad=gnomad_annotator,
493
- alphamissense=am_annotator,
494
- cadd=cadd_annotator,
495
+ gnomad=cast("GnomadAnnotator | None", gnomad_annotator),
496
+ alphamissense=cast("AlphaMissenseAnnotator | None", am_annotator),
497
+ cadd=cast("CaddAnnotator | None", cadd_annotator),
495
498
  high_value_rsids=hv_rsids,
496
499
  )
497
500
  finally:
@@ -5,7 +5,7 @@
5
5
  from __future__ import annotations
6
6
 
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING
8
+ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  import click
11
11
  from rich.table import Table
@@ -205,7 +205,7 @@ def _execute_tabix_extract( # pragma: no cover
205
205
  wanted: set[str],
206
206
  sample: str | None,
207
207
  data_dir: Path | None,
208
- pysam: object,
208
+ pysam: Any, # noqa: ANN401 — pysam ships no type stubs
209
209
  ) -> dict[str, Variant] | None:
210
210
  """Real tabix execution.
211
211
 
@@ -443,7 +443,7 @@ def export_plink_cmd(
443
443
  coding when available; falls back to monomorphic (A2=0) for positions
444
444
  without gnomAD coverage.
445
445
  """
446
- from allelix.exporters.plink import _orient_genotype, export_plink
446
+ from allelix.exporters.plink import export_plink, resolve_ref_alt_via_gnomad
447
447
 
448
448
  parser = _helpers._resolve_parser(file_path, fmt, sample=sample)
449
449
  prefix = output_prefix if output_prefix else file_path.with_suffix("")
@@ -462,45 +462,15 @@ def export_plink_cmd(
462
462
  key=lambda v: (chrom_order.get(v.chromosome, 99), v.chromosome, v.position),
463
463
  )
464
464
 
465
- variant_by_rsid: dict[str, Variant] = {}
466
- for v in variants:
467
- if not v.is_no_call:
468
- variant_by_rsid[v.rsid] = v
469
- rsids = set(variant_by_rsid)
465
+ variant_by_rsid: dict[str, Variant] = {v.rsid: v for v in variants if not v.is_no_call}
470
466
 
471
- ref_alt_map: dict[str, tuple[str, str]] = {}
472
- gnomad = None
473
467
  try:
474
- from allelix.annotators.gnomad import GnomadAnnotator
475
-
476
- gnomad = GnomadAnnotator(resolved)
477
- if gnomad.is_ready():
478
- coord_map = gnomad.bulk_resolve_coordinates(rsids)
479
- for rsid, coords in coord_map.items():
480
- if len(coords) == 1:
481
- _, _, ref, alt = coords[0]
482
- ref_alt_map[rsid] = (ref, alt)
483
- else:
484
- v = variant_by_rsid[rsid]
485
- pair = {v.allele1, v.allele2}
486
- for _, _, ref, alt in coords:
487
- if _orient_genotype(
488
- v.allele1, v.allele2, ref, alt
489
- ) is not None and pair <= {ref, alt}:
490
- ref_alt_map[rsid] = (ref, alt)
491
- break
492
- else:
493
- for _, _, ref, alt in coords:
494
- if _orient_genotype(v.allele1, v.allele2, ref, alt) is not None:
495
- ref_alt_map[rsid] = (ref, alt)
496
- break
468
+ ref_alt_map = resolve_ref_alt_via_gnomad(variant_by_rsid, resolved)
497
469
  except Exception:
498
470
  console.print(
499
471
  "[yellow]gnomAD coordinate resolution failed; using fallback allele coding.[/yellow]"
500
472
  )
501
- finally:
502
- if gnomad is not None:
503
- gnomad.close()
473
+ ref_alt_map = {}
504
474
 
505
475
  written, skipped, indel_skip, mono = export_plink(
506
476
  iter(variants), prefix, effective_build, ref_alt_map or None
@@ -15,6 +15,8 @@ from allelix.annotators.base import permission as check_permission
15
15
  if TYPE_CHECKING:
16
16
  from pathlib import Path
17
17
 
18
+ from allelix.annotators.base import Annotator
19
+
18
20
  logger = logging.getLogger(__name__)
19
21
 
20
22
  CONFIG_FILENAME = "config.toml"
@@ -46,7 +48,7 @@ class AllelixConfig:
46
48
  def is_enabled(
47
49
  self,
48
50
  source_name: str,
49
- annotator_classes: dict[str, type] | None = None,
51
+ annotator_classes: dict[str, type[Annotator]] | None = None,
50
52
  ) -> bool:
51
53
  """Check if a source is enabled, respecting the permission ladder.
52
54
 
@@ -211,8 +211,8 @@ def fetch_cpic_allele_functions(
211
211
 
212
212
  out: dict[tuple[str, str], str] = {}
213
213
  for row in location_values:
214
- rsid = location_to_rsid.get(row.get("locationid"))
215
- function_class = allele_to_function.get(row.get("alleledefinitionid"))
214
+ rsid = location_to_rsid.get(row.get("locationid")) # type: ignore[arg-type]
215
+ function_class = allele_to_function.get(row.get("alleledefinitionid")) # type: ignore[arg-type]
216
216
  base = (row.get("variantallele") or "").strip().upper()
217
217
  if not rsid or function_class is None:
218
218
  continue
@@ -15,7 +15,7 @@ import os
15
15
  import re
16
16
  import sqlite3
17
17
  from datetime import UTC, datetime
18
- from typing import TYPE_CHECKING
18
+ from typing import TYPE_CHECKING, cast
19
19
 
20
20
  from allelix.databases.schema import GWAS_SCHEMA
21
21
 
@@ -458,7 +458,7 @@ def iter_gwas_records(tsv_path: Path) -> Iterator[dict[str, object]]:
458
458
  if existing is None:
459
459
  best[key] = record
460
460
  else:
461
- ep = existing["p_value"]
461
+ ep = cast("float | None", existing["p_value"])
462
462
  if p_value is not None and (ep is None or p_value < ep):
463
463
  best[key] = record
464
464
 
@@ -61,8 +61,10 @@ def _parse_title_alleles(title: str) -> tuple[str, str] | None:
61
61
 
62
62
  def _tmpl_param(tmpl: object, name: str) -> str:
63
63
  """Extract a named parameter from a mwparserfromhell template."""
64
- if tmpl.has(name):
65
- return str(tmpl.get(name).value).strip()
64
+ # mwparserfromhell ships no type stubs (see [[tool.mypy.overrides]]); the
65
+ # parameter is typed as `object` at the boundary, then unwrapped here.
66
+ if tmpl.has(name): # type: ignore[attr-defined]
67
+ return str(tmpl.get(name).value).strip() # type: ignore[attr-defined]
66
68
  return ""
67
69
 
68
70
 
@@ -302,9 +304,9 @@ def _parse_raw_pages_inner(conn: sqlite3.Connection, *, verbose: bool = False) -
302
304
 
303
305
  repute = _tmpl_param(tmpl, "repute") or None
304
306
  summary = _tmpl_param(tmpl, "summary") or None
305
- gene = gene_map.get(snp_id) or None
307
+ snp_gene = gene_map.get(snp_id) or None
306
308
 
307
- batch.append((snp_id, allele1, allele2, magnitude, repute, summary, gene, scraped_at))
309
+ batch.append((snp_id, allele1, allele2, magnitude, repute, summary, snp_gene, scraped_at))
308
310
 
309
311
  if len(batch) >= 1000:
310
312
  conn.executemany(
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
9
9
  from allelix.utils.allele import complement, is_strand_ambiguous
10
10
 
11
11
  if TYPE_CHECKING:
12
- from collections.abc import Iterator
12
+ from collections.abc import Iterator, Mapping
13
13
  from pathlib import Path
14
14
 
15
15
  from allelix.models import Variant
@@ -132,6 +132,55 @@ def export_plink(
132
132
  return written, skipped, indels, monomorphic
133
133
 
134
134
 
135
+ def resolve_ref_alt_via_gnomad(
136
+ variant_by_rsid: Mapping[str, Variant], data_dir: Path
137
+ ) -> dict[str, tuple[str, str]]:
138
+ """Return per-rsid (ref, alt) for PLINK A1/A2 coding via gnomAD.
139
+
140
+ For each rsid in ``variant_by_rsid``, looks up gnomAD's coordinate
141
+ record(s). When gnomAD returns a single record, that ref/alt pair is
142
+ used directly. When gnomAD returns multiple records (multi-allelic
143
+ positions), the user's allele pair is matched against each candidate
144
+ via ``_orient_genotype`` and the first match that's also a subset of
145
+ ``{ref, alt}`` wins; if none is a subset, the first orientation-valid
146
+ record wins as a fallback.
147
+
148
+ Returns an empty dict if gnomAD isn't ready. Raises on any underlying
149
+ lookup error — the CLI catches and prints the "failed" warning, then
150
+ proceeds with empty (monomorphic) coding.
151
+ """
152
+ from allelix.annotators.gnomad import GnomadAnnotator
153
+
154
+ gnomad = GnomadAnnotator(data_dir)
155
+ try:
156
+ if not gnomad.is_ready():
157
+ return {}
158
+ coord_map = gnomad.bulk_resolve_coordinates(set(variant_by_rsid))
159
+ result: dict[str, tuple[str, str]] = {}
160
+ for rsid, coords in coord_map.items():
161
+ if len(coords) == 1:
162
+ _, _, ref, alt = coords[0]
163
+ result[rsid] = (ref, alt)
164
+ continue
165
+ v = variant_by_rsid[rsid]
166
+ pair = {v.allele1, v.allele2}
167
+ for _, _, ref, alt in coords:
168
+ if _orient_genotype(v.allele1, v.allele2, ref, alt) is not None and pair <= {
169
+ ref,
170
+ alt,
171
+ }:
172
+ result[rsid] = (ref, alt)
173
+ break
174
+ else:
175
+ for _, _, ref, alt in coords:
176
+ if _orient_genotype(v.allele1, v.allele2, ref, alt) is not None:
177
+ result[rsid] = (ref, alt)
178
+ break
179
+ return result
180
+ finally:
181
+ gnomad.close()
182
+
183
+
135
184
  def _fallback_coding(v: Variant) -> tuple[str, str, int, bool]:
136
185
  """Fallback allele coding when ref/alt is unknown.
137
186
 
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
  import contextlib
18
18
  import logging
19
19
  from dataclasses import dataclass, field, replace
20
- from typing import TYPE_CHECKING
20
+ from typing import TYPE_CHECKING, cast
21
21
 
22
22
  from allelix.utils.build_detect import (
23
23
  BUILD_GRCH36,
@@ -35,6 +35,7 @@ if TYPE_CHECKING:
35
35
  from allelix.annotators.alphamissense import AlphaMissenseAnnotator
36
36
  from allelix.annotators.base import Annotator
37
37
  from allelix.annotators.cadd import CaddAnnotator
38
+ from allelix.annotators.clinvar import ClinVarAnnotator
38
39
  from allelix.annotators.gnomad import GnomadAnnotator
39
40
  from allelix.models import Annotation, Variant
40
41
  from allelix.parsers.base import GenotypeParser
@@ -293,9 +294,9 @@ def _enrich_cadd(
293
294
 
294
295
  for a in annotations:
295
296
  if a.alt:
296
- coords = coord_map.get(a.rsid)
297
- if coords:
298
- a.cadd_phred = _lookup_user_allele(a.alt, coords, scores)
297
+ ann_coords = coord_map.get(a.rsid)
298
+ if ann_coords:
299
+ a.cadd_phred = _lookup_user_allele(a.alt, ann_coords, scores)
299
300
  elif resolved_coords and a.rsid in resolved_coords:
300
301
  # Position fallback: rsID was resolved on the fly via
301
302
  # ClinVar; the resolved tuple carries the user's actual
@@ -399,7 +400,9 @@ def run_analysis(
399
400
  # rsID-indexed annotators. GH #8.
400
401
  rsidless = [v for v in batch_buf if not v.rsid.startswith("rs")]
401
402
  if rsidless:
402
- resolution = clinvar_resolver.bulk_resolve_rsids(rsidless)
403
+ resolution = cast("ClinVarAnnotator", clinvar_resolver).bulk_resolve_rsids(
404
+ rsidless
405
+ )
403
406
  for (chrom, pos, ref, alt), rsid in resolution.items():
404
407
  resolved_coords[rsid] = (chrom, pos, ref, alt)
405
408
  # ADR-0035 PR 4: populate Variant.ref for array data via gnomAD's
@@ -600,6 +603,7 @@ class _BuildDetectionState:
600
603
  if variant.rsid in KNOWN_SNP_POSITIONS:
601
604
  result = detect_build(self._buffer)
602
605
  if result.is_confident:
606
+ assert result.build is not None # is_confident ⇒ build set
603
607
  self.detected = result.build
604
608
  self.matched_count = result.matched
605
609
  self.inspected_count = result.inspected
@@ -653,6 +657,7 @@ class _BuildDetectionState:
653
657
  )
654
658
  self.matched_count = result.matched
655
659
  self.inspected_count = result.inspected
660
+ assert self.effective is not None # all branches above set it
656
661
  out = [replace(v, build=self.effective) for v in self._buffer]
657
662
  self._buffer.clear()
658
663
  return out
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: allelix
3
- Version: 2.1.1
3
+ Version: 2.1.2
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>
@@ -29,35 +29,33 @@ Requires-Dist: pysam>=0.22; extra == "cadd"
29
29
  Provides-Extra: vcf-index
30
30
  Requires-Dist: pysam>=0.22; extra == "vcf-index"
31
31
  Provides-Extra: dev
32
+ Requires-Dist: mypy>=1.11; extra == "dev"
32
33
  Requires-Dist: pre-commit>=3.7; extra == "dev"
33
34
  Requires-Dist: pytest>=8.0; extra == "dev"
34
35
  Requires-Dist: pytest-cov<8,>=7; extra == "dev"
35
36
  Requires-Dist: ruff>=0.6; extra == "dev"
37
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
36
38
  Dynamic: license-file
37
39
 
40
+ <div align="center">
41
+
42
+ <img src="https://allelix.io/apple-touch-icon.png" width="180" alt="Allelix" />
43
+
44
+ **Open-source genotype analysis. Format-agnostic. Offline-first.**
45
+
46
+ [Website](https://allelix.io) · [Quickstart](#quickstart) · [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md)
47
+
48
+ [![python](https://img.shields.io/pypi/pyversions/allelix.svg)](https://www.python.org/downloads/)
49
+ [![pypi](https://img.shields.io/pypi/v/allelix.svg)](https://pypi.org/project/allelix/)
50
+ [![license](https://img.shields.io/pypi/l/allelix.svg)](LICENSE)
51
+ [![CI](https://github.com/allelix/allelix/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/allelix/allelix/actions/workflows/ci.yml)
52
+ [![privacy: local-first](https://img.shields.io/badge/privacy-local--first-success)](#privacy)
53
+
54
+ </div>
55
+
38
56
  # Allelix
39
57
 
40
- Open-source command-line toolkit for analyzing raw genotype files from consumer DNA testing services. Format-agnostic ingestion, database-agnostic annotation, offline-first.
41
-
42
- > **Status:** Production — eight parser formats (including VCF + gVCF),
43
- > four annotators (ClinVar + ClinPGx + GWAS Catalog + SNPedia), three
44
- > enrichment sources (gnomAD population frequencies + AlphaMissense
45
- > pathogenicity + CADD deleteriousness), licensable-source gating for
46
- > commercial users, dual-build ClinVar caches (GRCh37 + GRCh38),
47
- > HTML/JSON/terminal reports, methylation + pharmacogenomics focused
48
- > commands, report diffing, persistent config with commercial-mode
49
- > safety switch. Build auto-detection from position data (ADR-0021).
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).
59
- > Release notes:
60
- > [`CHANGELOG.md`](https://github.com/allelix/allelix/blob/main/CHANGELOG.md).
58
+ > **Status:** Production. Eight parser formats (including VCF + gVCF), four annotators (ClinVar + ClinPGx + GWAS Catalog + SNPedia), three enrichment sources (gnomAD + AlphaMissense + CADD), licensable-source gating for commercial users, dual-build ClinVar caches (GRCh37 + GRCh38), HTML/JSON/terminal reports, methylation + pharmacogenomics focused commands, report diffing, persistent config with commercial-mode safety switch. Build auto-detection from position data (ADR-0021). No regex on prose anywhere in production. The [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md) tracks every release.
61
59
 
62
60
  ## Quickstart
63
61
 
@@ -101,16 +99,17 @@ See [Development](#development) for source installs and running tests, [Managing
101
99
 
102
100
  Adding a new format means adding one file to `allelix/parsers/` and registering an instance in the `PARSERS` list in `allelix/parsers/__init__.py`.
103
101
 
104
- ### v2.1+ roadmap
102
+ ### Roadmap
105
103
 
106
- | Feature | Notes |
104
+ | Release | Theme |
107
105
  |---|---|
108
- | Per-source scoring | Magnitude breakdown by database. Users see which source drove the composite score. |
109
- | Annotator-level strand awareness (R-1) | Strand-flip matching wired into every annotator's carrier check. Basic `compare` strand support shipped in v1.1; full annotator integration deferred from v2.0.0. |
110
- | Good / Bad / Neutral repute | Per-annotation repute field. Reframes the report from "here's what's wrong" to "here's your full picture." Requires Annotation model change + renderer updates. |
111
- | PLINK import | Read .bed/.bim/.fam as an input format (complement to the v1.7.0 export). |
112
- | PharmCAT integration | Wrap CPIC's PharmCAT as an optional external engine for star-allele / diplotype calling. Requires VCF input (shipped in v2.0.0). |
113
- | Genome Watchtower | Real-time variant monitoring via database delta feeds. Privacy-preserving: server publishes universal feed, matching happens locally against your deviation set. Replaces full re-analysis with millisecond set intersection. |
106
+ | **v2.2** | Annotation model expansion (cross-source conflict surfacing, risk-allele display, structured MTAG flag), custom-panel UX improvements, methylation panel retune + citations, ClinVar source switch (VCF → `variant_summary.txt`), CLI cleanups (`db clean` / `db path`). |
107
+ | **v2.3** | Per-source magnitude decomposition, Good / Bad / Neutral annotation repute, JSON v3 (AI-legible output contract), plausibility flagging via zygosity × gnomAD MAF, ClinVar review-status weighting, functional-medicine +/− notation. |
108
+ | **v2.4** | ACMG/AMP automated classification engine, variant consequence annotation (VEP-equivalent). |
109
+ | **v2.5+** | CNVs and repeat-expansion support, dbSNP resolution for rsID-less VCFs, gnomAD genomes (non-coding population frequencies), supplemental genotype file merging. |
110
+ | **v3.0** | Ancestry estimation (PCA + 1KG/HGDP reference panels), Polygenic Risk Score (PRS) integration. |
111
+
112
+ Issues and milestones are tracked at [github.com/allelix/allelix/issues](https://github.com/allelix/allelix/issues). Annotator-level strand-aware carrier matching (R-1) shipped in v2.1.0's ADR-0035 cluster.
114
113
 
115
114
  ## Supported Databases
116
115
 
@@ -7,10 +7,12 @@ rich>=13.7
7
7
  pysam>=0.22
8
8
 
9
9
  [dev]
10
+ mypy>=1.11
10
11
  pre-commit>=3.7
11
12
  pytest>=8.0
12
13
  pytest-cov<8,>=7
13
14
  ruff>=0.6
15
+ types-PyYAML>=6.0
14
16
 
15
17
  [vcf-index]
16
18
  pysam>=0.22
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "allelix"
7
- version = "2.1.1"
7
+ version = "2.1.2"
8
8
  description = "Open-source genotype analysis toolkit. Format-agnostic ingestion, database-agnostic annotation, offline-first."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -36,10 +36,12 @@ vcf-index = [
36
36
  "pysam>=0.22",
37
37
  ]
38
38
  dev = [
39
+ "mypy>=1.11",
39
40
  "pre-commit>=3.7",
40
41
  "pytest>=8.0",
41
42
  "pytest-cov>=7,<8",
42
43
  "ruff>=0.6",
44
+ "types-PyYAML>=6.0",
43
45
  ]
44
46
 
45
47
  [project.scripts]
@@ -89,7 +91,7 @@ convention = "google"
89
91
 
90
92
  [tool.pytest.ini_options]
91
93
  testpaths = ["tests"]
92
- addopts = "-v --strict-markers --tb=short --cov=allelix --cov-report=term-missing --cov-fail-under=92"
94
+ addopts = "-v --strict-markers --tb=short --cov=allelix --cov-branch --cov-report=term-missing --cov-fail-under=92"
93
95
  markers = [
94
96
  "slow: marks tests that download databases or process large files",
95
97
  "integration: marks end-to-end pipeline tests",
@@ -99,6 +101,39 @@ filterwarnings = [
99
101
  "error::pytest.PytestUnraisableExceptionWarning",
100
102
  ]
101
103
 
104
+ # GH allelix-dev #31: coverage gate now measures BOTH line and branch
105
+ # coverage. Pre-#31 the gate was line-only — a line counted as covered
106
+ # the moment any input crossed it, which is how the v2.0.x carrier-match
107
+ # case/strand and download-truncation correctness bugs shipped green.
108
+ # The 92% floor is calibrated against the branch metric (currently 92.44%
109
+ # with branch coverage enabled); primary annotators sit at 95%+. Raise
110
+ # the floor when new tests close documented missing branches; do not
111
+ # lower it.
112
+ [tool.coverage.run]
113
+ branch = true
114
+
102
115
  [tool.coverage.report]
103
116
  precision = 2
104
117
  fail_under = 92.00
118
+
119
+ # GH allelix-dev #33: backs the py.typed promise with a real type checker.
120
+ # The starting baseline is intentionally permissive (no strict mode, no
121
+ # disallow_untyped_defs) so we get the gate in CI immediately instead of
122
+ # negotiating dozens of remediation PRs. Ratchet later by tightening the
123
+ # global flags one at a time and chasing the new errors.
124
+ [tool.mypy]
125
+ python_version = "3.11"
126
+ check_untyped_defs = true
127
+ warn_unused_ignores = true
128
+ warn_redundant_casts = true
129
+ warn_unreachable = true
130
+ no_implicit_optional = true
131
+ # Don't enforce on tests — ruff's per-file-ignores already exempt tests
132
+ # from ANN, and unit-test fixtures use a lot of pytest magic that mypy
133
+ # would flag without strict-mode benefit.
134
+ exclude = ["tests/", "scripts/"]
135
+
136
+ # Third-party libraries without published type stubs.
137
+ [[tool.mypy.overrides]]
138
+ module = ["pysam", "mwparserfromhell.*"]
139
+ ignore_missing_imports = true
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes