nonhuman-screen 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jlanej
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: nonhuman-screen
3
+ Version: 0.1.0
4
+ Summary: Classify sequencing reads by non-human taxonomic content (kraken2), including the allele-based non-human fraction of reads supporting a variant.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/jlanej/nonhuman-screen
7
+ Project-URL: Documentation, https://github.com/jlanej/nonhuman-screen/tree/main/docs
8
+ Keywords: kraken2,contamination,metagenomics,non-human,taxonomy,variant,bioinformatics,sequencing
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: bam
18
+ Requires-Dist: pysam>=0.21.0; extra == "bam"
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7.0; extra == "dev"
21
+ Requires-Dist: pysam>=0.21.0; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # nonhuman-screen
25
+
26
+ Classify sequencing reads by **non-human taxonomic content** using
27
+ [kraken2](https://github.com/DerrickWood/kraken2), and reduce the per-read
28
+ verdicts to a **non-human fraction (NHF)** plus per-domain breakdowns
29
+ (bacterial / archaeal / fungal / protist / viral / UniVec-Core).
30
+
31
+ The headline capability is the **allele-based NHF**: given a BAM/CRAM and a
32
+ variant, compute the non-human fraction of the reads that support that
33
+ variant's ALT allele — in one call, no boilerplate:
34
+
35
+ ```python
36
+ from nonhuman_screen import classify_variant_alt_reads
37
+
38
+ v = classify_variant_alt_reads(
39
+ "sample.bam", "kraken2_db", "chr1", 12345, "A", "T", ref_fasta="ref.fa",
40
+ )
41
+ v.nonhuman_fraction # 0.0–1.0 over ALT-supporting reads
42
+ v.fractions.bacterial # per-domain breakdown
43
+ v.supporting_reads # denominator
44
+ ```
45
+
46
+ `nonhuman-screen` is **sample-agnostic**: it classifies whatever reads you hand
47
+ it and never asks whose they are. The same call works on a proband, a parent, a
48
+ tumour, or a plain FASTQ — so it can back higher-level analyses (e.g. flagging
49
+ inherited variant calls that are actually parental contamination) without
50
+ knowing anything about pedigrees.
51
+
52
+ > Extracted from [`kmer-denovo-filter`](https://github.com/jlanej/kmer_denovo_filter),
53
+ > where this methodology was originally developed, so it can be reused
54
+ > independently.
55
+
56
+ ## How it works (in one paragraph)
57
+
58
+ Reads are written to a temporary FASTQ and classified by kraken2's LCA
59
+ algorithm. Each read's assigned taxid is mapped to a domain using the
60
+ database's `nodes.dmp` taxonomy. A read counts as **non-human** only if it is
61
+ classified and lies outside the human lineage, outside the human clade, and
62
+ outside UniVec-Core. Two conservative guards reduce false positives: a
63
+ **human-homology guard** (any k-mer voting for human, taxid 9606, drops the read
64
+ from every non-human numerator — important for integrating viruses such as
65
+ HBV/HPV/ERVs) and **UniVec-Core exclusion** (synthetic vector/adapter sequences,
66
+ taxid 81077, are tracked separately and never counted as contamination). See
67
+ [docs/methodology.md](docs/methodology.md).
68
+
69
+ ## Install
70
+
71
+ ```bash
72
+ pip install nonhuman-screen # core engine (standard library only)
73
+ pip install 'nonhuman-screen[bam]' # + BAM/CRAM & allele-based NHF (pysam)
74
+ ```
75
+
76
+ The **`bam` extra is required for the BAM/allele helpers and for the
77
+ `nonhuman-screen` CLI** (both classify modes import pysam). The core install
78
+ covers only `Kraken2Runner.classify_sequences` on in-memory reads.
79
+
80
+ You also need the `kraken2` binary on `PATH` and a kraken2 database — see
81
+ [docs/database.md](docs/database.md).
82
+
83
+ ## Usage
84
+
85
+ ### Allele-based NHF for many variants (batched — one kraken2 call)
86
+
87
+ ```python
88
+ from nonhuman_screen import classify_variants_alt_reads
89
+
90
+ variants = [("chr1", 12345, "A", "T"), ("chr2", 999, "G", "GAC")] # pos 0-based
91
+ for v in classify_variants_alt_reads("sample.bam", "kraken2_db", variants,
92
+ ref_fasta="ref.fa"):
93
+ print(v.variant_key, v.nonhuman_fraction, v.supporting_reads)
94
+ ```
95
+
96
+ The reads supporting all variants' ALT alleles are gathered and classified in a
97
+ **single** kraken2 invocation (its database load dominates runtime), then split
98
+ back per variant. You never re-implement fetch/allele-filter/classify yourself.
99
+
100
+ ### Classify an arbitrary set of reads
101
+
102
+ ```python
103
+ from nonhuman_screen import Kraken2Runner
104
+
105
+ result = Kraken2Runner("kraken2_db").classify_sequences(
106
+ {"read1": "ACGT...", "read2": "TTGCA..."}
107
+ )
108
+ result.nonhuman_fraction # 0.0–1.0
109
+ result.fractions() # TaxonomicFractions(nonhuman=…, bacterial=…, …)
110
+ result.taxonomy_available # False if nodes.dmp was missing (fractions unreliable)
111
+ result.nonhuman_read_names # set of read names
112
+ ```
113
+
114
+ ### Command line
115
+
116
+ ```bash
117
+ # Per-variant allele NHF table (headline)
118
+ nonhuman-screen classify \
119
+ --bam sample.bam --kraken2-db kraken2_db --ref-fasta ref.fa \
120
+ --variants candidates.vcf.gz --out-prefix sample_contam
121
+ # -> sample_contam.variant_nhf.tsv, sample_contam.summary.json
122
+
123
+ # Whole-BAM summary
124
+ nonhuman-screen classify --bam sample.bam --kraken2-db kraken2_db
125
+ ```
126
+
127
+ See [docs/cli.md](docs/cli.md).
128
+
129
+ ## Coordinate convention
130
+
131
+ All `pos` arguments in the Python API are **0-based** (pysam reference
132
+ coordinates); variant keys are `"{chrom}:{pos}:{ref}:{alt}"`. The CLI reads a
133
+ VCF and handles the conversion for you.
134
+
135
+ ## API summary
136
+
137
+ | Symbol | Needs `[bam]` | Purpose |
138
+ |---|:---:|---|
139
+ | `Kraken2Runner.classify_sequences(seqs)` | no | Classify `{name: seq}` → `ClassificationResult` |
140
+ | `ClassificationResult` | no | Per-domain read-name sets, counts, `fractions()`, `nonhuman_fraction`, `taxonomy_available` |
141
+ | `TaxonomicFractions` | no | Per-domain fractions; `from_result` / `over_reads` |
142
+ | `read_supports_alt(read, variant_pos, ref, alt)` | no | Does an aligned read carry the ALT allele? |
143
+ | `parse_kmer_votes(kmer_string)` | no | Parse kraken2 per-read k-mer detail into taxid votes |
144
+ | `VariantNHF` | no | Result of the allele-based helpers: `variant_key`, `nonhuman_fraction`, `supporting_reads`, `fractions`, `to_dict()` (pos 0-based) |
145
+ | `classify_variant_alt_reads(...)` | yes | Allele-based NHF for one variant → `VariantNHF` |
146
+ | `classify_variants_alt_reads(...)` | yes | Batched allele-based NHF for many variants |
147
+ | `classify_reads_from_bam(...)` | yes | Classify reads by name and/or locus |
148
+ | `reads_supporting_alt(...)` | yes | Read names supporting an ALT allele |
149
+
150
+ ## Docker
151
+
152
+ The included [`Dockerfile`](Dockerfile) installs the pinned kraken2 (v2.17.1) and
153
+ the package with the `[bam]` extra:
154
+
155
+ ```bash
156
+ docker build -t nonhuman-screen .
157
+
158
+ docker run --rm -v "$PWD:/data" nonhuman-screen \
159
+ classify --bam /data/sample.bam --kraken2-db /data/kraken2_db \
160
+ --variants /data/calls.vcf.gz --out-prefix /data/contam
161
+ ```
162
+
163
+ The image entrypoint is `nonhuman-screen`; mount your BAM and kraken2 database in.
164
+ You still supply the kraken2 database yourself (see [docs/database.md](docs/database.md)).
165
+
166
+ ## License
167
+
168
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,145 @@
1
+ # nonhuman-screen
2
+
3
+ Classify sequencing reads by **non-human taxonomic content** using
4
+ [kraken2](https://github.com/DerrickWood/kraken2), and reduce the per-read
5
+ verdicts to a **non-human fraction (NHF)** plus per-domain breakdowns
6
+ (bacterial / archaeal / fungal / protist / viral / UniVec-Core).
7
+
8
+ The headline capability is the **allele-based NHF**: given a BAM/CRAM and a
9
+ variant, compute the non-human fraction of the reads that support that
10
+ variant's ALT allele — in one call, no boilerplate:
11
+
12
+ ```python
13
+ from nonhuman_screen import classify_variant_alt_reads
14
+
15
+ v = classify_variant_alt_reads(
16
+ "sample.bam", "kraken2_db", "chr1", 12345, "A", "T", ref_fasta="ref.fa",
17
+ )
18
+ v.nonhuman_fraction # 0.0–1.0 over ALT-supporting reads
19
+ v.fractions.bacterial # per-domain breakdown
20
+ v.supporting_reads # denominator
21
+ ```
22
+
23
+ `nonhuman-screen` is **sample-agnostic**: it classifies whatever reads you hand
24
+ it and never asks whose they are. The same call works on a proband, a parent, a
25
+ tumour, or a plain FASTQ — so it can back higher-level analyses (e.g. flagging
26
+ inherited variant calls that are actually parental contamination) without
27
+ knowing anything about pedigrees.
28
+
29
+ > Extracted from [`kmer-denovo-filter`](https://github.com/jlanej/kmer_denovo_filter),
30
+ > where this methodology was originally developed, so it can be reused
31
+ > independently.
32
+
33
+ ## How it works (in one paragraph)
34
+
35
+ Reads are written to a temporary FASTQ and classified by kraken2's LCA
36
+ algorithm. Each read's assigned taxid is mapped to a domain using the
37
+ database's `nodes.dmp` taxonomy. A read counts as **non-human** only if it is
38
+ classified and lies outside the human lineage, outside the human clade, and
39
+ outside UniVec-Core. Two conservative guards reduce false positives: a
40
+ **human-homology guard** (any k-mer voting for human, taxid 9606, drops the read
41
+ from every non-human numerator — important for integrating viruses such as
42
+ HBV/HPV/ERVs) and **UniVec-Core exclusion** (synthetic vector/adapter sequences,
43
+ taxid 81077, are tracked separately and never counted as contamination). See
44
+ [docs/methodology.md](docs/methodology.md).
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install nonhuman-screen # core engine (standard library only)
50
+ pip install 'nonhuman-screen[bam]' # + BAM/CRAM & allele-based NHF (pysam)
51
+ ```
52
+
53
+ The **`bam` extra is required for the BAM/allele helpers and for the
54
+ `nonhuman-screen` CLI** (both classify modes import pysam). The core install
55
+ covers only `Kraken2Runner.classify_sequences` on in-memory reads.
56
+
57
+ You also need the `kraken2` binary on `PATH` and a kraken2 database — see
58
+ [docs/database.md](docs/database.md).
59
+
60
+ ## Usage
61
+
62
+ ### Allele-based NHF for many variants (batched — one kraken2 call)
63
+
64
+ ```python
65
+ from nonhuman_screen import classify_variants_alt_reads
66
+
67
+ variants = [("chr1", 12345, "A", "T"), ("chr2", 999, "G", "GAC")] # pos 0-based
68
+ for v in classify_variants_alt_reads("sample.bam", "kraken2_db", variants,
69
+ ref_fasta="ref.fa"):
70
+ print(v.variant_key, v.nonhuman_fraction, v.supporting_reads)
71
+ ```
72
+
73
+ The reads supporting all variants' ALT alleles are gathered and classified in a
74
+ **single** kraken2 invocation (its database load dominates runtime), then split
75
+ back per variant. You never re-implement fetch/allele-filter/classify yourself.
76
+
77
+ ### Classify an arbitrary set of reads
78
+
79
+ ```python
80
+ from nonhuman_screen import Kraken2Runner
81
+
82
+ result = Kraken2Runner("kraken2_db").classify_sequences(
83
+ {"read1": "ACGT...", "read2": "TTGCA..."}
84
+ )
85
+ result.nonhuman_fraction # 0.0–1.0
86
+ result.fractions() # TaxonomicFractions(nonhuman=…, bacterial=…, …)
87
+ result.taxonomy_available # False if nodes.dmp was missing (fractions unreliable)
88
+ result.nonhuman_read_names # set of read names
89
+ ```
90
+
91
+ ### Command line
92
+
93
+ ```bash
94
+ # Per-variant allele NHF table (headline)
95
+ nonhuman-screen classify \
96
+ --bam sample.bam --kraken2-db kraken2_db --ref-fasta ref.fa \
97
+ --variants candidates.vcf.gz --out-prefix sample_contam
98
+ # -> sample_contam.variant_nhf.tsv, sample_contam.summary.json
99
+
100
+ # Whole-BAM summary
101
+ nonhuman-screen classify --bam sample.bam --kraken2-db kraken2_db
102
+ ```
103
+
104
+ See [docs/cli.md](docs/cli.md).
105
+
106
+ ## Coordinate convention
107
+
108
+ All `pos` arguments in the Python API are **0-based** (pysam reference
109
+ coordinates); variant keys are `"{chrom}:{pos}:{ref}:{alt}"`. The CLI reads a
110
+ VCF and handles the conversion for you.
111
+
112
+ ## API summary
113
+
114
+ | Symbol | Needs `[bam]` | Purpose |
115
+ |---|:---:|---|
116
+ | `Kraken2Runner.classify_sequences(seqs)` | no | Classify `{name: seq}` → `ClassificationResult` |
117
+ | `ClassificationResult` | no | Per-domain read-name sets, counts, `fractions()`, `nonhuman_fraction`, `taxonomy_available` |
118
+ | `TaxonomicFractions` | no | Per-domain fractions; `from_result` / `over_reads` |
119
+ | `read_supports_alt(read, variant_pos, ref, alt)` | no | Does an aligned read carry the ALT allele? |
120
+ | `parse_kmer_votes(kmer_string)` | no | Parse kraken2 per-read k-mer detail into taxid votes |
121
+ | `VariantNHF` | no | Result of the allele-based helpers: `variant_key`, `nonhuman_fraction`, `supporting_reads`, `fractions`, `to_dict()` (pos 0-based) |
122
+ | `classify_variant_alt_reads(...)` | yes | Allele-based NHF for one variant → `VariantNHF` |
123
+ | `classify_variants_alt_reads(...)` | yes | Batched allele-based NHF for many variants |
124
+ | `classify_reads_from_bam(...)` | yes | Classify reads by name and/or locus |
125
+ | `reads_supporting_alt(...)` | yes | Read names supporting an ALT allele |
126
+
127
+ ## Docker
128
+
129
+ The included [`Dockerfile`](Dockerfile) installs the pinned kraken2 (v2.17.1) and
130
+ the package with the `[bam]` extra:
131
+
132
+ ```bash
133
+ docker build -t nonhuman-screen .
134
+
135
+ docker run --rm -v "$PWD:/data" nonhuman-screen \
136
+ classify --bam /data/sample.bam --kraken2-db /data/kraken2_db \
137
+ --variants /data/calls.vcf.gz --out-prefix /data/contam
138
+ ```
139
+
140
+ The image entrypoint is `nonhuman-screen`; mount your BAM and kraken2 database in.
141
+ You still supply the kraken2 database yourself (see [docs/database.md](docs/database.md)).
142
+
143
+ ## License
144
+
145
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nonhuman-screen"
7
+ version = "0.1.0"
8
+ description = "Classify sequencing reads by non-human taxonomic content (kraken2), including the allele-based non-human fraction of reads supporting a variant."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ keywords = [
13
+ "kraken2", "contamination", "metagenomics", "non-human",
14
+ "taxonomy", "variant", "bioinformatics", "sequencing",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
22
+ ]
23
+ # The core engine (Kraken2Runner.classify_sequences) is standard-library only
24
+ # and shells out to the `kraken2` binary. No third-party runtime dependency.
25
+ dependencies = []
26
+
27
+ [project.optional-dependencies]
28
+ # BAM/CRAM + allele-based NHF helpers.
29
+ bam = ["pysam>=0.21.0"]
30
+ dev = ["pytest>=7.0", "pysam>=0.21.0"]
31
+
32
+ [project.scripts]
33
+ nonhuman-screen = "nonhuman_screen.cli:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/jlanej/nonhuman-screen"
37
+ Documentation = "https://github.com/jlanej/nonhuman-screen/tree/main/docs"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.setuptools.package-data]
43
+ nonhuman_screen = ["py.typed"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,63 @@
1
+ """nonhuman-screen: classify sequencing reads by non-human taxonomic content.
2
+
3
+ Wraps kraken2's LCA classification and reduces per-read verdicts to a
4
+ non-human fraction (NHF) plus per-domain breakdowns, with a human-homology
5
+ guard and UniVec-Core exclusion. Sample-agnostic: it classifies whatever
6
+ named reads you hand it, so it works equally on a proband, a parent, a tumour,
7
+ or a plain FASTQ.
8
+
9
+ Two levels of API:
10
+
11
+ * Engine (stdlib only) — ``Kraken2Runner.classify_sequences({name: seq})``.
12
+ * BAM/allele helpers (needs the ``[bam]`` extra) — ``classify_variant_alt_reads``
13
+ and ``classify_variants_alt_reads`` compute the non-human fraction of the
14
+ reads supporting a variant's ALT allele.
15
+ """
16
+
17
+ from nonhuman_screen.engine import (
18
+ ClassificationResult,
19
+ Kraken2Runner,
20
+ _ARCHAEA_TAXID,
21
+ _BACTERIA_TAXID,
22
+ _EUKARYOTA_TAXID,
23
+ _FUNGI_TAXID,
24
+ _HUMAN_TAXID,
25
+ _METAZOA_TAXID,
26
+ _UNIVEC_CORE_TAXID,
27
+ _VIRIDIPLANTAE_TAXID,
28
+ _VIRUSES_TAXID,
29
+ )
30
+ from nonhuman_screen.alleles import read_supports_alt
31
+ from nonhuman_screen.result import TaxonomicFractions, VariantNHF
32
+ from nonhuman_screen.votes import parse_kmer_votes
33
+
34
+ __version__ = "0.1.0"
35
+
36
+ __all__ = [
37
+ "Kraken2Runner",
38
+ "ClassificationResult",
39
+ "TaxonomicFractions",
40
+ "VariantNHF",
41
+ "read_supports_alt",
42
+ "parse_kmer_votes",
43
+ "__version__",
44
+ ]
45
+
46
+ # BAM/allele helpers require pysam ([bam] extra). Expose them at the top level
47
+ # when available, but never make importing the core engine depend on pysam.
48
+ try:
49
+ from nonhuman_screen.bam import ( # noqa: F401
50
+ classify_reads_from_bam,
51
+ classify_variant_alt_reads,
52
+ classify_variants_alt_reads,
53
+ reads_supporting_alt,
54
+ )
55
+
56
+ __all__ += [
57
+ "classify_reads_from_bam",
58
+ "classify_variant_alt_reads",
59
+ "classify_variants_alt_reads",
60
+ "reads_supporting_alt",
61
+ ]
62
+ except ImportError:
63
+ pass
@@ -0,0 +1,85 @@
1
+ """Allele-support determination for reads at a variant locus.
2
+
3
+ Pure-Python helpers that decide whether an aligned read carries a given
4
+ alternate allele. They operate on a pysam ``AlignedSegment`` passed in by
5
+ the caller but do **not** import pysam themselves, so this module has no
6
+ third-party dependency.
7
+ """
8
+
9
+
10
+ def _is_symbolic(allele):
11
+ """Return True if *allele* is a symbolic VCF allele with no literal sequence.
12
+
13
+ Symbolic alleles include ``<DEL>``, ``<INS>``, ``<DUP>``, breakend
14
+ notation containing ``[`` or ``]``, and the overlapping-deletion
15
+ marker ``*``.
16
+ """
17
+ if not allele:
18
+ return True
19
+ return allele[0] == "<" or allele == "*" or "[" in allele or "]" in allele
20
+
21
+
22
+ def read_supports_alt(
23
+ read, variant_pos, ref, alt, min_baseq=0, *,
24
+ aligned_pairs=None, seq=None, quals=None,
25
+ ):
26
+ """Return True if *read* carries the alternate allele at *variant_pos*.
27
+
28
+ Extracts the exact read sequence aligned to the reference span of the
29
+ variant and compares it strictly to the candidate alternate allele.
30
+ Handles SNPs, MNPs, insertions, deletions, and complex indels natively.
31
+
32
+ Returns ``False`` for symbolic alleles or when *alt* is ``None``.
33
+
34
+ Args:
35
+ min_baseq: Minimum base quality threshold for bases considered as
36
+ alt support.
37
+ aligned_pairs: Optional pre-computed result of
38
+ ``read.get_aligned_pairs(matches_only=False)``. Computed from
39
+ *read* when not provided.
40
+ seq: Optional pre-decoded ``read.query_sequence``. Decoded from
41
+ *read* when not provided.
42
+ quals: Optional pre-decoded ``read.query_qualities``. Decoded from
43
+ *read* only when ``min_baseq > 0`` and not provided.
44
+ """
45
+ if alt is None or _is_symbolic(alt):
46
+ return False
47
+
48
+ if seq is None:
49
+ seq = read.query_sequence
50
+ if seq is None:
51
+ return False
52
+ if min_baseq > 0 and quals is None:
53
+ quals = read.query_qualities
54
+
55
+ if aligned_pairs is None:
56
+ aligned_pairs = read.get_aligned_pairs(matches_only=False)
57
+
58
+ extracted_seq = []
59
+ in_variant_region = False
60
+
61
+ for qpos, rpos in aligned_pairs:
62
+ # Stop collecting once we reach or pass the end of the reference allele span
63
+ if rpos is not None and rpos >= variant_pos + len(ref):
64
+ break
65
+
66
+ # Start collecting when we hit the exact start of the variant
67
+ if rpos == variant_pos:
68
+ in_variant_region = True
69
+
70
+ if in_variant_region:
71
+ # qpos is None for deleted bases (skip), otherwise append the read base
72
+ if qpos is not None:
73
+ if (
74
+ min_baseq > 0 and quals is not None
75
+ and quals[qpos] < min_baseq
76
+ ):
77
+ return False
78
+ extracted_seq.append(seq[qpos])
79
+
80
+ # If the variant region was skipped entirely due to read boundaries
81
+ if not in_variant_region:
82
+ return False
83
+
84
+ return "".join(extracted_seq).upper() == alt.upper()
85
+