allelix 2.2.3__tar.gz → 2.3.0__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 (88) hide show
  1. {allelix-2.2.3 → allelix-2.3.0}/PKG-INFO +8 -3
  2. {allelix-2.2.3 → allelix-2.3.0}/README.md +7 -2
  3. {allelix-2.2.3 → allelix-2.3.0}/allelix/cli/_helpers.py +38 -0
  4. {allelix-2.2.3 → allelix-2.3.0}/allelix/cli/_options.py +14 -0
  5. {allelix-2.2.3 → allelix-2.3.0}/allelix/cli/analyze.py +4 -0
  6. {allelix-2.2.3 → allelix-2.3.0}/allelix/reports/_pipeline.py +53 -1
  7. allelix-2.3.0/allelix/reports/vcf.py +579 -0
  8. {allelix-2.2.3 → allelix-2.3.0}/allelix.egg-info/PKG-INFO +8 -3
  9. {allelix-2.2.3 → allelix-2.3.0}/allelix.egg-info/SOURCES.txt +2 -0
  10. {allelix-2.2.3 → allelix-2.3.0}/pyproject.toml +1 -1
  11. allelix-2.3.0/tests/test_cli_vcf_out.py +369 -0
  12. {allelix-2.2.3 → allelix-2.3.0}/LICENSE +0 -0
  13. {allelix-2.2.3 → allelix-2.3.0}/allelix/__init__.py +0 -0
  14. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/__init__.py +0 -0
  15. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/alphamissense.py +0 -0
  16. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/base.py +0 -0
  17. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/cadd.py +0 -0
  18. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/clinvar.py +0 -0
  19. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/gnomad.py +0 -0
  20. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/gwas.py +0 -0
  21. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/pharmgkb.py +0 -0
  22. {allelix-2.2.3 → allelix-2.3.0}/allelix/annotators/snpedia.py +0 -0
  23. {allelix-2.2.3 → allelix-2.3.0}/allelix/cli/__init__.py +0 -0
  24. {allelix-2.2.3 → allelix-2.3.0}/allelix/cli/config.py +0 -0
  25. {allelix-2.2.3 → allelix-2.3.0}/allelix/cli/db.py +0 -0
  26. {allelix-2.2.3 → allelix-2.3.0}/allelix/cli/focused.py +0 -0
  27. {allelix-2.2.3 → allelix-2.3.0}/allelix/cli/utility.py +0 -0
  28. {allelix-2.2.3 → allelix-2.3.0}/allelix/compare.py +0 -0
  29. {allelix-2.2.3 → allelix-2.3.0}/allelix/config.py +0 -0
  30. {allelix-2.2.3 → allelix-2.3.0}/allelix/data/__init__.py +0 -0
  31. {allelix-2.2.3 → allelix-2.3.0}/allelix/data/clinvar_clnsig_snapshot.yaml +0 -0
  32. {allelix-2.2.3 → allelix-2.3.0}/allelix/data/high_value_snps.yaml +0 -0
  33. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/__init__.py +0 -0
  34. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/_versions.py +0 -0
  35. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/alphamissense_loader.py +0 -0
  36. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/cadd_loader.py +0 -0
  37. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/cpic_loader.py +0 -0
  38. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/gnomad_loader.py +0 -0
  39. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/gwas_loader.py +0 -0
  40. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/loader_utils.py +0 -0
  41. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/manager.py +0 -0
  42. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/pharmgkb_loader.py +0 -0
  43. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/schema.py +0 -0
  44. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/snpedia_loader.py +0 -0
  45. {allelix-2.2.3 → allelix-2.3.0}/allelix/databases/snpedia_parser.py +0 -0
  46. {allelix-2.2.3 → allelix-2.3.0}/allelix/exporters/__init__.py +0 -0
  47. {allelix-2.2.3 → allelix-2.3.0}/allelix/exporters/plink.py +0 -0
  48. {allelix-2.2.3 → allelix-2.3.0}/allelix/models.py +0 -0
  49. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/__init__.py +0 -0
  50. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/_helpers.py +0 -0
  51. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/ancestrydna.py +0 -0
  52. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/base.py +0 -0
  53. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/ftdna.py +0 -0
  54. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/ftdna_famfinder.py +0 -0
  55. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/ftdna_illumina.py +0 -0
  56. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/livingdna.py +0 -0
  57. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/myhappygenes.py +0 -0
  58. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/myheritage.py +0 -0
  59. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/twentythreeandme.py +0 -0
  60. {allelix-2.2.3 → allelix-2.3.0}/allelix/parsers/vcf.py +0 -0
  61. {allelix-2.2.3 → allelix-2.3.0}/allelix/py.typed +0 -0
  62. {allelix-2.2.3 → allelix-2.3.0}/allelix/reports/__init__.py +0 -0
  63. {allelix-2.2.3 → allelix-2.3.0}/allelix/reports/diff.py +0 -0
  64. {allelix-2.2.3 → allelix-2.3.0}/allelix/reports/high_value.py +0 -0
  65. {allelix-2.2.3 → allelix-2.3.0}/allelix/reports/html.py +0 -0
  66. {allelix-2.2.3 → allelix-2.3.0}/allelix/reports/json_report.py +0 -0
  67. {allelix-2.2.3 → allelix-2.3.0}/allelix/reports/methylation.py +0 -0
  68. {allelix-2.2.3 → allelix-2.3.0}/allelix/reports/terminal.py +0 -0
  69. {allelix-2.2.3 → allelix-2.3.0}/allelix/utils/__init__.py +0 -0
  70. {allelix-2.2.3 → allelix-2.3.0}/allelix/utils/allele.py +0 -0
  71. {allelix-2.2.3 → allelix-2.3.0}/allelix/utils/build_detect.py +0 -0
  72. {allelix-2.2.3 → allelix-2.3.0}/allelix.egg-info/dependency_links.txt +0 -0
  73. {allelix-2.2.3 → allelix-2.3.0}/allelix.egg-info/entry_points.txt +0 -0
  74. {allelix-2.2.3 → allelix-2.3.0}/allelix.egg-info/requires.txt +0 -0
  75. {allelix-2.2.3 → allelix-2.3.0}/allelix.egg-info/top_level.txt +0 -0
  76. {allelix-2.2.3 → allelix-2.3.0}/setup.cfg +0 -0
  77. {allelix-2.2.3 → allelix-2.3.0}/tests/test_changelog_sanitize.py +0 -0
  78. {allelix-2.2.3 → allelix-2.3.0}/tests/test_cli.py +0 -0
  79. {allelix-2.2.3 → allelix-2.3.0}/tests/test_cli_helpers.py +0 -0
  80. {allelix-2.2.3 → allelix-2.3.0}/tests/test_compare.py +0 -0
  81. {allelix-2.2.3 → allelix-2.3.0}/tests/test_config.py +0 -0
  82. {allelix-2.2.3 → allelix-2.3.0}/tests/test_end_to_end.py +0 -0
  83. {allelix-2.2.3 → allelix-2.3.0}/tests/test_extract_rsidless_gh128.py +0 -0
  84. {allelix-2.2.3 → allelix-2.3.0}/tests/test_mock_data_invariants.py +0 -0
  85. {allelix-2.2.3 → allelix-2.3.0}/tests/test_models.py +0 -0
  86. {allelix-2.2.3 → allelix-2.3.0}/tests/test_original_rsid_never_serializes.py +0 -0
  87. {allelix-2.2.3 → allelix-2.3.0}/tests/test_registry.py +0 -0
  88. {allelix-2.2.3 → allelix-2.3.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: allelix
3
- Version: 2.2.3
3
+ Version: 2.3.0
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>
@@ -43,7 +43,7 @@ Dynamic: license-file
43
43
 
44
44
  **Open-source genotype analysis. Format-agnostic. Offline-first.**
45
45
 
46
- [Website](https://allelix.io) · [Quickstart](#quickstart) · [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md)
46
+ [Website](https://allelix.io) · [Demo](https://analyze.allelix.io) · [Quickstart](#quickstart) · [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md)
47
47
 
48
48
  [![python](https://img.shields.io/pypi/pyversions/allelix.svg)](https://www.python.org/downloads/)
49
49
  [![pypi](https://img.shields.io/pypi/v/allelix.svg)](https://pypi.org/project/allelix/)
@@ -55,7 +55,7 @@ Dynamic: license-file
55
55
 
56
56
  # Allelix
57
57
 
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.
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 and annotated VCF output for pipeline integration (v2.3.0+), 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.
59
59
 
60
60
  ## Quickstart
61
61
 
@@ -80,6 +80,11 @@ allelix analyze trio.vcf.gz --sample HG002 --output report.html
80
80
 
81
81
  # Filter to a custom panel (rsIDs + gene names, one per line; '#' comments and blank lines ignored)
82
82
  allelix analyze your_genotype_file.txt --filter-file my_panel.txt --output report.html
83
+
84
+ # Annotated VCF output — VCF-in / VCF-out for pipeline integration
85
+ # Writes a valid VCF with annotations as INFO fields + full provenance in the header.
86
+ # Refuses to overwrite; requires VCF or gVCF input.
87
+ allelix analyze your_wgs.vcf.gz --vcf-out annotated.vcf
83
88
  ```
84
89
 
85
90
  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.
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Open-source genotype analysis. Format-agnostic. Offline-first.**
6
6
 
7
- [Website](https://allelix.io) · [Quickstart](#quickstart) · [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md)
7
+ [Website](https://allelix.io) · [Demo](https://analyze.allelix.io) · [Quickstart](#quickstart) · [Changelog](https://github.com/allelix/allelix/blob/main/CHANGELOG.md)
8
8
 
9
9
  [![python](https://img.shields.io/pypi/pyversions/allelix.svg)](https://www.python.org/downloads/)
10
10
  [![pypi](https://img.shields.io/pypi/v/allelix.svg)](https://pypi.org/project/allelix/)
@@ -16,7 +16,7 @@
16
16
 
17
17
  # Allelix
18
18
 
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.
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 and annotated VCF output for pipeline integration (v2.3.0+), 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.
20
20
 
21
21
  ## Quickstart
22
22
 
@@ -41,6 +41,11 @@ allelix analyze trio.vcf.gz --sample HG002 --output report.html
41
41
 
42
42
  # Filter to a custom panel (rsIDs + gene names, one per line; '#' comments and blank lines ignored)
43
43
  allelix analyze your_genotype_file.txt --filter-file my_panel.txt --output report.html
44
+
45
+ # Annotated VCF output — VCF-in / VCF-out for pipeline integration
46
+ # Writes a valid VCF with annotations as INFO fields + full provenance in the header.
47
+ # Refuses to overwrite; requires VCF or gVCF input.
48
+ allelix analyze your_wgs.vcf.gz --vcf-out annotated.vcf
44
49
  ```
45
50
 
46
51
  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.
@@ -496,12 +496,29 @@ def _run_analysis_command(
496
496
  no_alphamissense: bool = False,
497
497
  no_cadd: bool = False,
498
498
  sample: str | None = None,
499
+ vcf_out: Path | None = None,
499
500
  ) -> None:
500
501
  resolved = resolve_data_dir(data_dir)
501
502
  if not no_update:
502
503
  _maybe_refresh_databases(resolved)
503
504
  parser = _resolve_parser(file_path, fmt, sample=sample)
504
505
 
506
+ # #150: --vcf-out preflight. Bail before any expensive setup so a
507
+ # user who mistyped the flag or aimed it at a 23andMe file finds
508
+ # out immediately, not after the analyze pipeline runs.
509
+ if vcf_out is not None:
510
+ if parser.name != "vcf":
511
+ raise click.ClickException(
512
+ f"--vcf-out requires VCF or gVCF input; got {parser.display_name} "
513
+ f"({parser.name!r}). Convert to VCF first, or drop --vcf-out to "
514
+ "produce the default report."
515
+ )
516
+ if vcf_out.exists():
517
+ raise click.ClickException(
518
+ f"--vcf-out refuses to overwrite an existing file: {vcf_out}. "
519
+ "Move or delete the existing file, or choose a different path."
520
+ )
521
+
505
522
  from allelix.config import load_config
506
523
 
507
524
  cfg = load_config(resolved)
@@ -554,6 +571,7 @@ def _run_analysis_command(
554
571
  cadd=cast("CaddAnnotator | None", cadd_annotator),
555
572
  high_value_rsids=hv_rsids,
556
573
  panel_rsids=rsids,
574
+ collect_vcf_write_inputs=vcf_out is not None,
557
575
  )
558
576
  finally:
559
577
  _unwire_parser_logging(counter, stderr_handler, snapshot)
@@ -650,6 +668,26 @@ def _run_analysis_command(
650
668
  )
651
669
  console.print(f"[green]Wrote {rendered:,} annotation(s) to {output}[/green]")
652
670
 
671
+ if vcf_out is not None:
672
+ from allelix.reports.vcf import render_vcf
673
+
674
+ # Pipeline populates result.vcf_write_inputs whenever the
675
+ # ``collect_vcf_write_inputs`` flag is set — which the preflight
676
+ # above guarantees for this branch — so the None case here is
677
+ # a defensive assert, not a runtime path.
678
+ assert result.vcf_write_inputs is not None
679
+ stamped = render_vcf(
680
+ input_path=file_path,
681
+ output_path=vcf_out,
682
+ build=result.build,
683
+ annotators_used=result.annotators_used,
684
+ annotations_by_key=result.vcf_write_inputs.annotations_by_key,
685
+ recovered_rsids=result.vcf_write_inputs.recovered_rsids,
686
+ )
687
+ console.print(
688
+ f"[green]Wrote annotated VCF with {stamped:,} stamped row(s) to {vcf_out}[/green]"
689
+ )
690
+
653
691
  console.print(
654
692
  f"[dim]{len(result.annotations):,} total annotation(s) from {len(ready)} "
655
693
  f"database(s) across {result.total_variants:,} variant(s).[/dim]"
@@ -169,3 +169,17 @@ _SAMPLE_OPT = click.option(
169
169
  "or any plain VCF tool to list available samples."
170
170
  ),
171
171
  )
172
+ # #150: annotated VCF output. Requires VCF/gVCF input (any other format
173
+ # errors out early). Existing output paths are refused (no clobber, no
174
+ # --force in v2.3.0). See ADR-0036 for the output contract.
175
+ _VCF_OUT_OPT = click.option(
176
+ "--vcf-out",
177
+ "vcf_out",
178
+ type=click.Path(dir_okay=False, path_type=Path),
179
+ default=None,
180
+ help=(
181
+ "Write an annotated VCF at this path (v2.3.0+; experimental output "
182
+ "contract, ADR-0036). Requires VCF or gVCF input. Refuses to "
183
+ "overwrite an existing file."
184
+ ),
185
+ )
@@ -29,6 +29,7 @@ from allelix.cli._options import (
29
29
  _REPORT_FORMAT_OPT,
30
30
  _SAMPLE_OPT,
31
31
  _SNPEDIA_MIN_MAG_OPT,
32
+ _VCF_OUT_OPT,
32
33
  )
33
34
 
34
35
  if TYPE_CHECKING:
@@ -61,6 +62,7 @@ if TYPE_CHECKING:
61
62
  @_NO_ALPHAMISSENSE_OPT
62
63
  @_NO_CADD_OPT
63
64
  @_SAMPLE_OPT
65
+ @_VCF_OUT_OPT
64
66
  def analyze(
65
67
  file_path: Path,
66
68
  fmt: str | None,
@@ -82,6 +84,7 @@ def analyze(
82
84
  no_alphamissense: bool,
83
85
  no_cadd: bool,
84
86
  sample: str | None,
87
+ vcf_out: Path | None,
85
88
  ) -> None:
86
89
  """Annotate a genotype file against all ready reference databases."""
87
90
  filter_genes: frozenset[str] | None = None
@@ -113,4 +116,5 @@ def analyze(
113
116
  no_alphamissense=no_alphamissense,
114
117
  no_cadd=no_cadd,
115
118
  sample=sample,
119
+ vcf_out=vcf_out,
116
120
  )
@@ -39,6 +39,7 @@ if TYPE_CHECKING:
39
39
  from allelix.annotators.gnomad import GnomadAnnotator
40
40
  from allelix.models import Annotation, Variant
41
41
  from allelix.parsers.base import GenotypeParser
42
+ from allelix.reports.vcf import VcfWriteInputs
42
43
 
43
44
 
44
45
  # How many input variants to buffer while waiting for detection to
@@ -155,6 +156,12 @@ class AnalysisResult:
155
156
  # Used by the CLI to surface the GRCh38 rsID-less undercount
156
157
  # warning until #62's dbSNP resolution lands.
157
158
  rsidless_variant_count: int = 0
159
+ # #150: pipeline-produced inputs for the annotated VCF writer.
160
+ # Populated only when ``run_analysis`` is called with
161
+ # ``collect_vcf_write_inputs=True`` — the ``--vcf-out`` code path.
162
+ # None on every other analyze run (zero memory / CPU overhead on the
163
+ # default path). Consumed by ``allelix.reports.vcf.render_vcf``.
164
+ vcf_write_inputs: VcfWriteInputs | None = None
158
165
 
159
166
  def panel_coverage(
160
167
  self, filtered_annotations: list[Annotation] | None = None
@@ -439,6 +446,7 @@ def run_analysis(
439
446
  cadd: CaddAnnotator | None = None,
440
447
  high_value_rsids: set[str] | None = None,
441
448
  panel_rsids: frozenset[str] | None = None,
449
+ collect_vcf_write_inputs: bool = False,
442
450
  ) -> AnalysisResult:
443
451
  """Stream the file once; batch-annotate; return a fully-populated result.
444
452
 
@@ -515,6 +523,13 @@ def run_analysis(
515
523
  # back to position-keyed gnomAD / AlphaMissense lookups for resolved
516
524
  # rsIDs that don't appear in those caches' rsid index.
517
525
  resolved_coords: dict[str, tuple[str, int, str, str]] = {}
526
+ # #150: annotation dict keyed by (chrom, pos, ref, alt) for the
527
+ # annotated-VCF writer. Populated per batch inside ``_flush`` when
528
+ # ``collect_vcf_write_inputs`` is True. Values are references to the
529
+ # Annotation objects in ``annotations`` — enrichment mutations
530
+ # (gnomAD AF, AlphaMissense, CADD) that happen after streaming are
531
+ # visible through these refs without a per-annotation copy.
532
+ vcf_annotations_by_key: dict[tuple[str, int, str, str], list[Annotation]] = {}
518
533
 
519
534
  with contextlib.ExitStack() as stack:
520
535
  bound = [stack.enter_context(a) for a in annotators]
@@ -617,8 +632,29 @@ def run_analysis(
617
632
  nonlocal no_ref_total, rsidless_total
618
633
  no_ref_total += sum(1 for v in batch_buf if v.ref is None)
619
634
  rsidless_total += sum(1 for v in batch_buf if not v.rsid.startswith("rs"))
635
+ batch_new: list[Annotation] = []
620
636
  for annotator in bound:
621
- annotations.extend(annotator.batch_annotate(batch_buf))
637
+ batch_new.extend(annotator.batch_annotate(batch_buf))
638
+ annotations.extend(batch_new)
639
+ # #150: collect writer inputs for this batch. Only touches
640
+ # variants whose ``ref`` is populated (VCF inputs; array
641
+ # data with ref=None can't produce a well-formed VCF key).
642
+ # Skips annotations with empty ``alt`` — GWAS rows keyed by
643
+ # rsID alone can't be VCF-stamped without allele context.
644
+ if collect_vcf_write_inputs:
645
+ rsid_to_coords: dict[str, tuple[str, int, str]] = {
646
+ v.rsid: (v.chromosome, v.position, v.ref)
647
+ for v in batch_buf
648
+ if v.ref is not None
649
+ }
650
+ for a in batch_new:
651
+ if not a.alt:
652
+ continue
653
+ coords = rsid_to_coords.get(a.rsid)
654
+ if coords is None:
655
+ continue
656
+ chrom, pos, ref = coords
657
+ vcf_annotations_by_key.setdefault((chrom, pos, ref, a.alt), []).append(a)
622
658
  batch_buf.clear()
623
659
 
624
660
  def _accept(v: Variant) -> None:
@@ -733,6 +769,21 @@ def run_analysis(
733
769
  result_annotated = None
734
770
  result_genotypes = None
735
771
 
772
+ vcf_write_inputs: VcfWriteInputs | None = None
773
+ if collect_vcf_write_inputs:
774
+ # Import here to avoid a top-level cycle with allelix.reports.vcf
775
+ # (which reaches into the annotator registry for attribution).
776
+ from allelix.reports.vcf import VcfWriteInputs as _VcfWriteInputs
777
+
778
+ vcf_recovered_rsids = {
779
+ (chrom, pos, ref, alt): rsid
780
+ for rsid, (chrom, pos, ref, alt) in resolved_coords.items()
781
+ }
782
+ vcf_write_inputs = _VcfWriteInputs(
783
+ annotations_by_key=vcf_annotations_by_key,
784
+ recovered_rsids=vcf_recovered_rsids,
785
+ )
786
+
736
787
  return AnalysisResult(
737
788
  file_path=file_path,
738
789
  parser_name=parser.name,
@@ -751,6 +802,7 @@ def run_analysis(
751
802
  panel_genotypes=result_genotypes,
752
803
  no_ref_variant_count=no_ref_total,
753
804
  rsidless_variant_count=rsidless_total,
805
+ vcf_write_inputs=vcf_write_inputs,
754
806
  )
755
807
 
756
808