dnakit 0.1.0.dev0__py3-none-any.whl
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.
- dnakit/__init__.py +108 -0
- dnakit/_version.py +5 -0
- dnakit/alignment/__init__.py +6 -0
- dnakit/alignment/pairwise.py +512 -0
- dnakit/alignment/results.py +188 -0
- dnakit/backends/__init__.py +80 -0
- dnakit/backends/external.py +517 -0
- dnakit/backends/registry.py +207 -0
- dnakit/batch.py +452 -0
- dnakit/cache/__init__.py +5 -0
- dnakit/cache/store.py +424 -0
- dnakit/chunking.py +920 -0
- dnakit/cli/__init__.py +5 -0
- dnakit/cli/__main__.py +6 -0
- dnakit/cli/app.py +814 -0
- dnakit/cli/workflow.py +94 -0
- dnakit/config/__init__.py +5 -0
- dnakit/config/loader.py +381 -0
- dnakit/core/__init__.py +80 -0
- dnakit/core/_json.py +206 -0
- dnakit/core/backend_info.py +70 -0
- dnakit/core/collection.py +163 -0
- dnakit/core/coordinates.py +295 -0
- dnakit/core/enums.py +95 -0
- dnakit/core/facade.py +719 -0
- dnakit/core/feature.py +90 -0
- dnakit/core/gap.py +82 -0
- dnakit/core/issues.py +62 -0
- dnakit/core/provenance.py +305 -0
- dnakit/core/record.py +161 -0
- dnakit/core/results.py +134 -0
- dnakit/core/sequence.py +327 -0
- dnakit/datasets/__init__.py +100 -0
- dnakit/datasets/_advanced_shared.py +198 -0
- dnakit/datasets/_metadata.py +33 -0
- dnakit/datasets/advanced_deduplicate.py +183 -0
- dnakit/datasets/advanced_split.py +302 -0
- dnakit/datasets/clustering.py +286 -0
- dnakit/datasets/config.py +531 -0
- dnakit/datasets/deduplicate.py +303 -0
- dnakit/datasets/evaluation.py +244 -0
- dnakit/datasets/filter.py +135 -0
- dnakit/datasets/neural_clustering.py +221 -0
- dnakit/datasets/results.py +520 -0
- dnakit/datasets/split.py +450 -0
- dnakit/descriptors/__init__.py +102 -0
- dnakit/descriptors/_dinucleotide.py +345 -0
- dnakit/descriptors/_shared.py +180 -0
- dnakit/descriptors/all.py +917 -0
- dnakit/descriptors/basic.py +193 -0
- dnakit/descriptors/codon.py +195 -0
- dnakit/descriptors/complexity.py +201 -0
- dnakit/descriptors/entropy.py +101 -0
- dnakit/descriptors/homopolymer.py +105 -0
- dnakit/descriptors/kmer.py +88 -0
- dnakit/descriptors/results.py +286 -0
- dnakit/descriptors/schema.py +414 -0
- dnakit/descriptors/window.py +203 -0
- dnakit/download/__init__.py +54 -0
- dnakit/download/catalogs.py +166 -0
- dnakit/download/ena.py +131 -0
- dnakit/download/encode.py +103 -0
- dnakit/download/files.py +469 -0
- dnakit/download/indexes.py +233 -0
- dnakit/download/metadata.py +164 -0
- dnakit/download/models.py +227 -0
- dnakit/download/ncbi.py +333 -0
- dnakit/download/sequence.py +133 -0
- dnakit/download/ucsc.py +99 -0
- dnakit/evaluation/__init__.py +81 -0
- dnakit/evaluation/_shared.py +288 -0
- dnakit/evaluation/collection.py +270 -0
- dnakit/evaluation/config.py +591 -0
- dnakit/evaluation/distribution.py +287 -0
- dnakit/evaluation/frechet.py +223 -0
- dnakit/evaluation/generative.py +381 -0
- dnakit/evaluation/reference.py +409 -0
- dnakit/evaluation/results.py +137 -0
- dnakit/evaluation/scorecard.py +211 -0
- dnakit/evaluation/sequence.py +403 -0
- dnakit/evaluation/synthesis.py +249 -0
- dnakit/exceptions.py +189 -0
- dnakit/fingerprints/__init__.py +109 -0
- dnakit/fingerprints/_shared.py +116 -0
- dnakit/fingerprints/advanced.py +829 -0
- dnakit/fingerprints/bit.py +229 -0
- dnakit/fingerprints/encoding.py +351 -0
- dnakit/fingerprints/kmer.py +170 -0
- dnakit/fingerprints/results.py +531 -0
- dnakit/fingerprints/sketch.py +246 -0
- dnakit/io/__init__.py +112 -0
- dnakit/io/_advanced_common.py +105 -0
- dnakit/io/_formats.py +994 -0
- dnakit/io/annotations.py +1179 -0
- dnakit/io/api.py +759 -0
- dnakit/io/config.py +315 -0
- dnakit/io/genbank.py +523 -0
- dnakit/io/indexed.py +1146 -0
- dnakit/io/metadata.py +452 -0
- dnakit/io/parquet.py +56 -0
- dnakit/io/results.py +131 -0
- dnakit/io/source.py +138 -0
- dnakit/io/tables.py +1237 -0
- dnakit/molbio/__init__.py +101 -0
- dnakit/molbio/_shared.py +266 -0
- dnakit/molbio/assembly.py +279 -0
- dnakit/molbio/crispr.py +181 -0
- dnakit/molbio/optimization.py +897 -0
- dnakit/molbio/primer3_design.py +667 -0
- dnakit/molbio/primers.py +899 -0
- dnakit/molbio/restriction.py +573 -0
- dnakit/molbio/results.py +583 -0
- dnakit/ops/__init__.py +588 -0
- dnakit/ops/_common.py +201 -0
- dnakit/ops/circular.py +193 -0
- dnakit/ops/concat.py +204 -0
- dnakit/ops/direction.py +27 -0
- dnakit/ops/edit.py +443 -0
- dnakit/ops/evolution.py +991 -0
- dnakit/ops/mutation.py +151 -0
- dnakit/ops/records.py +1012 -0
- dnakit/ops/translation.py +257 -0
- dnakit/patterns/__init__.py +81 -0
- dnakit/patterns/_shared.py +282 -0
- dnakit/patterns/coding.py +360 -0
- dnakit/patterns/crispr.py +337 -0
- dnakit/patterns/motif.py +721 -0
- dnakit/patterns/regions.py +341 -0
- dnakit/patterns/repeats.py +480 -0
- dnakit/patterns/restriction.py +281 -0
- dnakit/patterns/results.py +411 -0
- dnakit/py.typed +1 -0
- dnakit/references/__init__.py +27 -0
- dnakit/references/models.py +155 -0
- dnakit/references/ncbi.py +785 -0
- dnakit/representations/__init__.py +29 -0
- dnakit/representations/backends.py +928 -0
- dnakit/representations/checkpoints.py +356 -0
- dnakit/representations/extraction.py +220 -0
- dnakit/representations/models.py +395 -0
- dnakit/representations/results.py +79 -0
- dnakit/search/__init__.py +132 -0
- dnakit/search/_http.py +271 -0
- dnakit/search/_shared.py +108 -0
- dnakit/search/blast.py +516 -0
- dnakit/search/ena.py +245 -0
- dnakit/search/encode.py +102 -0
- dnakit/search/ensembl.py +874 -0
- dnakit/search/models.py +159 -0
- dnakit/search/ncbi.py +756 -0
- dnakit/search/ucsc.py +388 -0
- dnakit/secondary_structure/__init__.py +38 -0
- dnakit/secondary_structure/dotbracket.py +410 -0
- dnakit/secondary_structure/nupack.py +589 -0
- dnakit/secondary_structure/results.py +142 -0
- dnakit/similarity/__init__.py +125 -0
- dnakit/similarity/_shared.py +152 -0
- dnakit/similarity/approximate.py +258 -0
- dnakit/similarity/compare.py +162 -0
- dnakit/similarity/dashing.py +936 -0
- dnakit/similarity/distance.py +279 -0
- dnakit/similarity/index.py +365 -0
- dnakit/similarity/matrix.py +254 -0
- dnakit/similarity/results.py +676 -0
- dnakit/similarity/search.py +286 -0
- dnakit/similarity/sketch.py +96 -0
- dnakit/similarity/vector.py +662 -0
- dnakit/standardize/__init__.py +77 -0
- dnakit/standardize/_shared.py +100 -0
- dnakit/standardize/config.py +411 -0
- dnakit/standardize/gaps.py +1060 -0
- dnakit/standardize/normalize.py +565 -0
- dnakit/standardize/results.py +247 -0
- dnakit/standardize/validate.py +487 -0
- dnakit/structure3d/__init__.py +47 -0
- dnakit/structure3d/_shared.py +47 -0
- dnakit/structure3d/geometry.py +773 -0
- dnakit/structure3d/pdb.py +253 -0
- dnakit/structure3d/results.py +233 -0
- dnakit/structure3d/threedna.py +174 -0
- dnakit/thermodynamics/__init__.py +112 -0
- dnakit/thermodynamics/_shared.py +92 -0
- dnakit/thermodynamics/backends.py +653 -0
- dnakit/thermodynamics/calculations.py +837 -0
- dnakit/thermodynamics/config.py +175 -0
- dnakit/thermodynamics/equilibrium.py +340 -0
- dnakit/thermodynamics/optics.py +385 -0
- dnakit/thermodynamics/parameters.py +79 -0
- dnakit/thermodynamics/results.py +445 -0
- dnakit/visualization/__init__.py +43 -0
- dnakit/visualization/_svg.py +185 -0
- dnakit/visualization/advanced.py +314 -0
- dnakit/visualization/config.py +255 -0
- dnakit/visualization/export.py +349 -0
- dnakit/visualization/matrix.py +279 -0
- dnakit/visualization/report.py +431 -0
- dnakit/visualization/results.py +236 -0
- dnakit/visualization/sequence.py +459 -0
- dnakit/workflows/__init__.py +45 -0
- dnakit/workflows/manifest.py +365 -0
- dnakit/workflows/runner.py +1548 -0
- dnakit/workflows/schema.py +898 -0
- dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
- dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
- dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
- dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
- dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
- dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
- dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Deterministic bottom-k MinHash and FracMinHash sketches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
from dnakit.core import Gap, Topology
|
|
9
|
+
from dnakit.core._json import FrozenDict
|
|
10
|
+
from dnakit.exceptions import ConfigurationError, UnsupportedGapOperationError
|
|
11
|
+
|
|
12
|
+
from ._shared import SequenceInput, sequence_and_id, validate_bool, validate_positive_int
|
|
13
|
+
from .results import SketchResult
|
|
14
|
+
|
|
15
|
+
DEFAULT_MAX_SKETCH_HASHES = 1_000_000
|
|
16
|
+
DEFAULT_MAX_UNIQUE_HASHES = 1_000_000
|
|
17
|
+
_UINT64_SPACE = 1 << 64
|
|
18
|
+
_COMPLEMENT = str.maketrans("ACGT", "TGCA")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _hash64(word: str, seed: int) -> int:
|
|
22
|
+
payload = seed.to_bytes(8, "little", signed=False) + word.encode("ascii")
|
|
23
|
+
return int.from_bytes(hashlib.sha256(payload).digest()[:8], "big")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _canonical(word: str) -> str:
|
|
27
|
+
reverse = word.translate(_COMPLEMENT)[::-1]
|
|
28
|
+
return min(word, reverse)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_common(
|
|
32
|
+
*,
|
|
33
|
+
k: int,
|
|
34
|
+
canonical: bool,
|
|
35
|
+
seed: int,
|
|
36
|
+
max_hashes: int,
|
|
37
|
+
max_unique_hashes: int,
|
|
38
|
+
) -> None:
|
|
39
|
+
validate_positive_int(k, "k")
|
|
40
|
+
validate_bool(canonical, "canonical")
|
|
41
|
+
if isinstance(seed, bool) or not isinstance(seed, int) or not 0 <= seed < _UINT64_SPACE:
|
|
42
|
+
raise ConfigurationError(
|
|
43
|
+
"seed must be an integer in [0, 2**64).",
|
|
44
|
+
code="INVALID_SKETCH_SEED",
|
|
45
|
+
)
|
|
46
|
+
validate_positive_int(max_hashes, "max_hashes")
|
|
47
|
+
if max_hashes > DEFAULT_MAX_SKETCH_HASHES:
|
|
48
|
+
raise ConfigurationError(
|
|
49
|
+
"max_hashes exceeds the hard safety ceiling.",
|
|
50
|
+
code="SKETCH_HASH_LIMIT_INVALID",
|
|
51
|
+
context={"max_hashes": max_hashes, "hard_limit": DEFAULT_MAX_SKETCH_HASHES},
|
|
52
|
+
)
|
|
53
|
+
validate_positive_int(max_unique_hashes, "max_unique_hashes")
|
|
54
|
+
if max_unique_hashes > DEFAULT_MAX_UNIQUE_HASHES:
|
|
55
|
+
raise ConfigurationError(
|
|
56
|
+
"max_unique_hashes exceeds the hard safety ceiling.",
|
|
57
|
+
code="SKETCH_UNIQUE_HASH_LIMIT_INVALID",
|
|
58
|
+
context={
|
|
59
|
+
"max_unique_hashes": max_unique_hashes,
|
|
60
|
+
"hard_limit": DEFAULT_MAX_UNIQUE_HASHES,
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _unique_hashes(
|
|
66
|
+
value: SequenceInput,
|
|
67
|
+
*,
|
|
68
|
+
k: int,
|
|
69
|
+
canonical: bool,
|
|
70
|
+
seed: int,
|
|
71
|
+
max_unique_hashes: int,
|
|
72
|
+
) -> tuple[str | None, int, int, tuple[int, ...]]:
|
|
73
|
+
sequence, sequence_id = sequence_and_id(value)
|
|
74
|
+
if sequence.topology is Topology.CIRCULAR:
|
|
75
|
+
raise ConfigurationError(
|
|
76
|
+
"Native sketches currently require a linear sequence.",
|
|
77
|
+
code="SKETCH_CIRCULAR_UNSUPPORTED",
|
|
78
|
+
hint="Rotate a circular sequence to an explicit origin before sketching.",
|
|
79
|
+
)
|
|
80
|
+
gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
|
|
81
|
+
if gaps:
|
|
82
|
+
raise UnsupportedGapOperationError(
|
|
83
|
+
"Sketches do not silently omit or cross explicit Gap objects.",
|
|
84
|
+
code="SKETCH_GAP_NOT_ALLOWED",
|
|
85
|
+
context={"gap_count": len(gaps)},
|
|
86
|
+
)
|
|
87
|
+
if any(symbol not in "ACGT" for symbol in sequence.symbols):
|
|
88
|
+
raise ConfigurationError(
|
|
89
|
+
"Native sketches require canonical A/C/G/T symbols.",
|
|
90
|
+
code="SKETCH_AMBIGUITY_NOT_ALLOWED",
|
|
91
|
+
)
|
|
92
|
+
words: Iterable[str] = (
|
|
93
|
+
sequence.symbols[index : index + k]
|
|
94
|
+
for index in range(max(0, sequence.symbol_length - k + 1))
|
|
95
|
+
)
|
|
96
|
+
if canonical:
|
|
97
|
+
words = (_canonical(word) for word in words)
|
|
98
|
+
unique_hashes: set[int] = set()
|
|
99
|
+
for word in words:
|
|
100
|
+
unique_hashes.add(_hash64(word, seed))
|
|
101
|
+
if len(unique_hashes) > max_unique_hashes:
|
|
102
|
+
raise ConfigurationError(
|
|
103
|
+
"Sketch calculation exceeds max_unique_hashes.",
|
|
104
|
+
code="SKETCH_UNIQUE_HASH_LIMIT_EXCEEDED",
|
|
105
|
+
context={"unique_hash_count_lower_bound": max_unique_hashes + 1},
|
|
106
|
+
)
|
|
107
|
+
hashes = tuple(sorted(unique_hashes))
|
|
108
|
+
observation_count = max(0, sequence.symbol_length - k + 1)
|
|
109
|
+
return sequence_id, sequence.symbol_length, observation_count, hashes
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def minhash(
|
|
113
|
+
value: SequenceInput,
|
|
114
|
+
*,
|
|
115
|
+
k: int,
|
|
116
|
+
num_hashes: int = 1_000,
|
|
117
|
+
canonical: bool = True,
|
|
118
|
+
seed: int = 0,
|
|
119
|
+
max_hashes: int = DEFAULT_MAX_SKETCH_HASHES,
|
|
120
|
+
max_unique_hashes: int = DEFAULT_MAX_UNIQUE_HASHES,
|
|
121
|
+
) -> SketchResult:
|
|
122
|
+
"""Return the smallest distinct 64-bit SHA-256-derived k-mer hashes."""
|
|
123
|
+
|
|
124
|
+
_validate_common(
|
|
125
|
+
k=k,
|
|
126
|
+
canonical=canonical,
|
|
127
|
+
seed=seed,
|
|
128
|
+
max_hashes=max_hashes,
|
|
129
|
+
max_unique_hashes=max_unique_hashes,
|
|
130
|
+
)
|
|
131
|
+
validate_positive_int(num_hashes, "num_hashes")
|
|
132
|
+
if num_hashes > max_hashes:
|
|
133
|
+
raise ConfigurationError(
|
|
134
|
+
"num_hashes cannot exceed max_hashes.",
|
|
135
|
+
code="SKETCH_HASH_LIMIT_EXCEEDED",
|
|
136
|
+
)
|
|
137
|
+
sequence_id, symbol_length, observation_count, hashes = _unique_hashes(
|
|
138
|
+
value,
|
|
139
|
+
k=k,
|
|
140
|
+
canonical=canonical,
|
|
141
|
+
seed=seed,
|
|
142
|
+
max_unique_hashes=max_unique_hashes,
|
|
143
|
+
)
|
|
144
|
+
return SketchResult(
|
|
145
|
+
name="minhash",
|
|
146
|
+
method="bottom-k-sha256-64",
|
|
147
|
+
schema_version="dnakit.sketch.minhash.v1",
|
|
148
|
+
sequence_id=sequence_id,
|
|
149
|
+
symbol_length=symbol_length,
|
|
150
|
+
k=k,
|
|
151
|
+
canonical=canonical,
|
|
152
|
+
seed=seed,
|
|
153
|
+
selection="bottom_k",
|
|
154
|
+
hashes=hashes[:num_hashes],
|
|
155
|
+
num_hashes=num_hashes,
|
|
156
|
+
scaled=None,
|
|
157
|
+
threshold=None,
|
|
158
|
+
observation_count=observation_count,
|
|
159
|
+
unique_hash_count=len(hashes),
|
|
160
|
+
max_hashes=max_hashes,
|
|
161
|
+
parameters=FrozenDict(
|
|
162
|
+
{
|
|
163
|
+
"hash": "sha256-first-64-big-endian",
|
|
164
|
+
"deduplicate_kmers": True,
|
|
165
|
+
"max_unique_hashes": max_unique_hashes,
|
|
166
|
+
"topology": "linear",
|
|
167
|
+
}
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def fracminhash(
|
|
173
|
+
value: SequenceInput,
|
|
174
|
+
*,
|
|
175
|
+
k: int,
|
|
176
|
+
scaled: int = 1_000,
|
|
177
|
+
canonical: bool = True,
|
|
178
|
+
seed: int = 0,
|
|
179
|
+
max_hashes: int = DEFAULT_MAX_SKETCH_HASHES,
|
|
180
|
+
max_unique_hashes: int = DEFAULT_MAX_UNIQUE_HASHES,
|
|
181
|
+
) -> SketchResult:
|
|
182
|
+
"""Retain hashes below ``floor(2**64 / scaled)`` with a hard output limit."""
|
|
183
|
+
|
|
184
|
+
_validate_common(
|
|
185
|
+
k=k,
|
|
186
|
+
canonical=canonical,
|
|
187
|
+
seed=seed,
|
|
188
|
+
max_hashes=max_hashes,
|
|
189
|
+
max_unique_hashes=max_unique_hashes,
|
|
190
|
+
)
|
|
191
|
+
validate_positive_int(scaled, "scaled")
|
|
192
|
+
if scaled > _UINT64_SPACE:
|
|
193
|
+
raise ConfigurationError(
|
|
194
|
+
"scaled must not exceed 2**64.",
|
|
195
|
+
code="INVALID_FRACMINHASH_SCALED",
|
|
196
|
+
)
|
|
197
|
+
sequence_id, symbol_length, observation_count, hashes = _unique_hashes(
|
|
198
|
+
value,
|
|
199
|
+
k=k,
|
|
200
|
+
canonical=canonical,
|
|
201
|
+
seed=seed,
|
|
202
|
+
max_unique_hashes=max_unique_hashes,
|
|
203
|
+
)
|
|
204
|
+
threshold = _UINT64_SPACE // scaled
|
|
205
|
+
retained = tuple(item for item in hashes if item < threshold)
|
|
206
|
+
if len(retained) > max_hashes:
|
|
207
|
+
raise ConfigurationError(
|
|
208
|
+
"FracMinHash output exceeds max_hashes.",
|
|
209
|
+
code="SKETCH_HASH_LIMIT_EXCEEDED",
|
|
210
|
+
context={"retained_hashes": len(retained), "max_hashes": max_hashes},
|
|
211
|
+
hint="Increase scaled or explicitly raise max_hashes within the hard ceiling.",
|
|
212
|
+
)
|
|
213
|
+
return SketchResult(
|
|
214
|
+
name="fracminhash",
|
|
215
|
+
method="threshold-sha256-64",
|
|
216
|
+
schema_version="dnakit.sketch.fracminhash.v1",
|
|
217
|
+
sequence_id=sequence_id,
|
|
218
|
+
symbol_length=symbol_length,
|
|
219
|
+
k=k,
|
|
220
|
+
canonical=canonical,
|
|
221
|
+
seed=seed,
|
|
222
|
+
selection="scaled",
|
|
223
|
+
hashes=retained,
|
|
224
|
+
num_hashes=None,
|
|
225
|
+
scaled=scaled,
|
|
226
|
+
threshold=threshold,
|
|
227
|
+
observation_count=observation_count,
|
|
228
|
+
unique_hash_count=len(hashes),
|
|
229
|
+
max_hashes=max_hashes,
|
|
230
|
+
parameters=FrozenDict(
|
|
231
|
+
{
|
|
232
|
+
"hash": "sha256-first-64-big-endian",
|
|
233
|
+
"deduplicate_kmers": True,
|
|
234
|
+
"max_unique_hashes": max_unique_hashes,
|
|
235
|
+
"topology": "linear",
|
|
236
|
+
}
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
__all__ = [
|
|
242
|
+
"DEFAULT_MAX_SKETCH_HASHES",
|
|
243
|
+
"DEFAULT_MAX_UNIQUE_HASHES",
|
|
244
|
+
"fracminhash",
|
|
245
|
+
"minhash",
|
|
246
|
+
]
|
dnakit/io/__init__.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Streaming DNA record input and output."""
|
|
2
|
+
|
|
3
|
+
from dnakit.io.annotations import (
|
|
4
|
+
AGPComponent,
|
|
5
|
+
AGPDocument,
|
|
6
|
+
AGPEntry,
|
|
7
|
+
AGPGap,
|
|
8
|
+
AnnotationDocument,
|
|
9
|
+
AnnotationEntry,
|
|
10
|
+
AnnotationSource,
|
|
11
|
+
AnnotationTarget,
|
|
12
|
+
read_agp,
|
|
13
|
+
read_bed,
|
|
14
|
+
read_gff3,
|
|
15
|
+
write_agp,
|
|
16
|
+
write_bed,
|
|
17
|
+
write_gff3,
|
|
18
|
+
)
|
|
19
|
+
from dnakit.io.api import ReadMode, read, read_one, read_set, write
|
|
20
|
+
from dnakit.io.config import FeatureWritePolicy, ReadConfig, WriteConfig
|
|
21
|
+
from dnakit.io.indexed import (
|
|
22
|
+
FastaIndex,
|
|
23
|
+
FastaIndexEntry,
|
|
24
|
+
FastqIndex,
|
|
25
|
+
FastqIndexEntry,
|
|
26
|
+
build_fasta_index,
|
|
27
|
+
build_fastq_index,
|
|
28
|
+
iter_chunks,
|
|
29
|
+
load_fasta_index,
|
|
30
|
+
load_fastq_index,
|
|
31
|
+
)
|
|
32
|
+
from dnakit.io.metadata import (
|
|
33
|
+
MetadataValidationIssue,
|
|
34
|
+
MetadataValidationReport,
|
|
35
|
+
filter_by_metadata,
|
|
36
|
+
merge_metadata,
|
|
37
|
+
select_metadata,
|
|
38
|
+
validate_metadata,
|
|
39
|
+
with_metadata,
|
|
40
|
+
)
|
|
41
|
+
from dnakit.io.parquet import (
|
|
42
|
+
ParquetBackendStatus,
|
|
43
|
+
parquet_backend_status,
|
|
44
|
+
require_parquet_backend,
|
|
45
|
+
)
|
|
46
|
+
from dnakit.io.results import GeneratedID, WriteResult
|
|
47
|
+
from dnakit.io.source import RecordSource
|
|
48
|
+
from dnakit.io.tables import (
|
|
49
|
+
ParquetCompression,
|
|
50
|
+
TableColumnType,
|
|
51
|
+
TableExportResult,
|
|
52
|
+
TableReadResult,
|
|
53
|
+
TableSchema,
|
|
54
|
+
export_result,
|
|
55
|
+
export_table,
|
|
56
|
+
read_table,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"AGPComponent",
|
|
61
|
+
"AGPDocument",
|
|
62
|
+
"AGPEntry",
|
|
63
|
+
"AGPGap",
|
|
64
|
+
"AnnotationDocument",
|
|
65
|
+
"AnnotationEntry",
|
|
66
|
+
"AnnotationSource",
|
|
67
|
+
"AnnotationTarget",
|
|
68
|
+
"FastaIndex",
|
|
69
|
+
"FastaIndexEntry",
|
|
70
|
+
"FastqIndex",
|
|
71
|
+
"FastqIndexEntry",
|
|
72
|
+
"FeatureWritePolicy",
|
|
73
|
+
"GeneratedID",
|
|
74
|
+
"MetadataValidationIssue",
|
|
75
|
+
"MetadataValidationReport",
|
|
76
|
+
"ParquetBackendStatus",
|
|
77
|
+
"ParquetCompression",
|
|
78
|
+
"ReadConfig",
|
|
79
|
+
"ReadMode",
|
|
80
|
+
"RecordSource",
|
|
81
|
+
"TableColumnType",
|
|
82
|
+
"TableExportResult",
|
|
83
|
+
"TableReadResult",
|
|
84
|
+
"TableSchema",
|
|
85
|
+
"WriteConfig",
|
|
86
|
+
"WriteResult",
|
|
87
|
+
"build_fasta_index",
|
|
88
|
+
"build_fastq_index",
|
|
89
|
+
"export_result",
|
|
90
|
+
"export_table",
|
|
91
|
+
"filter_by_metadata",
|
|
92
|
+
"iter_chunks",
|
|
93
|
+
"load_fasta_index",
|
|
94
|
+
"load_fastq_index",
|
|
95
|
+
"merge_metadata",
|
|
96
|
+
"parquet_backend_status",
|
|
97
|
+
"read",
|
|
98
|
+
"read_agp",
|
|
99
|
+
"read_bed",
|
|
100
|
+
"read_gff3",
|
|
101
|
+
"read_one",
|
|
102
|
+
"read_set",
|
|
103
|
+
"read_table",
|
|
104
|
+
"require_parquet_backend",
|
|
105
|
+
"select_metadata",
|
|
106
|
+
"validate_metadata",
|
|
107
|
+
"with_metadata",
|
|
108
|
+
"write",
|
|
109
|
+
"write_agp",
|
|
110
|
+
"write_bed",
|
|
111
|
+
"write_gff3",
|
|
112
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Shared bounded text helpers for advanced codecs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Callable, Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TextIO
|
|
11
|
+
|
|
12
|
+
from dnakit.exceptions import InputFormatError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@contextmanager
|
|
16
|
+
def open_text_source(source: str | os.PathLike[str] | TextIO) -> Iterator[TextIO]:
|
|
17
|
+
"""Borrow streams and own path handles."""
|
|
18
|
+
|
|
19
|
+
if isinstance(source, (str, os.PathLike)):
|
|
20
|
+
path = Path(source)
|
|
21
|
+
try:
|
|
22
|
+
with path.open("r", encoding="utf-8", newline="") as handle:
|
|
23
|
+
yield handle
|
|
24
|
+
except OSError as exc:
|
|
25
|
+
raise InputFormatError(
|
|
26
|
+
"Could not open the advanced text input.",
|
|
27
|
+
code="INPUT_OPEN_FAILED",
|
|
28
|
+
context={"source": str(path), "reason": str(exc)},
|
|
29
|
+
) from exc
|
|
30
|
+
return
|
|
31
|
+
if not hasattr(source, "read"):
|
|
32
|
+
raise TypeError("source must be a path or readable text stream.")
|
|
33
|
+
probe = source.read(0)
|
|
34
|
+
if not isinstance(probe, str):
|
|
35
|
+
raise TypeError("advanced annotation codecs require a decoded text stream.")
|
|
36
|
+
yield source
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def checked_lines(handle: TextIO, *, max_line_length: int) -> Iterator[tuple[int, str]]:
|
|
40
|
+
"""Yield newline-free lines while enforcing a hard character limit."""
|
|
41
|
+
|
|
42
|
+
if isinstance(max_line_length, bool) or not isinstance(max_line_length, int):
|
|
43
|
+
raise TypeError("max_line_length must be an integer.")
|
|
44
|
+
if max_line_length < 1:
|
|
45
|
+
raise ValueError("max_line_length must be positive.")
|
|
46
|
+
for line_number, raw in enumerate(handle, start=1):
|
|
47
|
+
line = raw.rstrip("\r\n")
|
|
48
|
+
if len(line) > max_line_length:
|
|
49
|
+
raise InputFormatError(
|
|
50
|
+
"An input line exceeds the configured resource limit.",
|
|
51
|
+
code="LINE_TOO_LONG",
|
|
52
|
+
context={
|
|
53
|
+
"line_number": line_number,
|
|
54
|
+
"line_length": len(line),
|
|
55
|
+
"max_line_length": max_line_length,
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
yield line_number, line
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def write_text_path(
|
|
62
|
+
target: str | os.PathLike[str] | TextIO,
|
|
63
|
+
writer: Callable[[TextIO], int],
|
|
64
|
+
*,
|
|
65
|
+
overwrite: bool,
|
|
66
|
+
create_parents: bool,
|
|
67
|
+
) -> int:
|
|
68
|
+
"""Write text atomically for paths and directly to borrowed streams."""
|
|
69
|
+
|
|
70
|
+
if not isinstance(overwrite, bool) or not isinstance(create_parents, bool):
|
|
71
|
+
raise TypeError("overwrite and create_parents must be booleans.")
|
|
72
|
+
if not isinstance(target, (str, os.PathLike)):
|
|
73
|
+
if not hasattr(target, "write"):
|
|
74
|
+
raise TypeError("target must be a path or writable text stream.")
|
|
75
|
+
count = writer(target)
|
|
76
|
+
target.flush()
|
|
77
|
+
return count
|
|
78
|
+
|
|
79
|
+
path = Path(target)
|
|
80
|
+
if path.exists() and not overwrite:
|
|
81
|
+
raise FileExistsError(f"Refusing to overwrite existing output: {path}")
|
|
82
|
+
if not path.parent.exists():
|
|
83
|
+
if not create_parents:
|
|
84
|
+
raise FileNotFoundError(f"Output parent directory does not exist: {path.parent}")
|
|
85
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
descriptor, name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
|
|
87
|
+
temporary = Path(name)
|
|
88
|
+
os.close(descriptor)
|
|
89
|
+
try:
|
|
90
|
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
|
91
|
+
count = writer(handle)
|
|
92
|
+
handle.flush()
|
|
93
|
+
os.fsync(handle.fileno())
|
|
94
|
+
if overwrite:
|
|
95
|
+
os.replace(temporary, path)
|
|
96
|
+
else:
|
|
97
|
+
os.link(temporary, path)
|
|
98
|
+
temporary.unlink()
|
|
99
|
+
except BaseException:
|
|
100
|
+
temporary.unlink(missing_ok=True)
|
|
101
|
+
raise
|
|
102
|
+
return count
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ["checked_lines", "open_text_source", "write_text_path"]
|