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
dnakit/io/api.py
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
"""Public file and stream I/O entry points."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import codecs
|
|
6
|
+
import gzip
|
|
7
|
+
import hashlib
|
|
8
|
+
import io
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
12
|
+
from contextlib import suppress
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, BinaryIO, Literal, TextIO, TypeAlias, cast, overload
|
|
16
|
+
|
|
17
|
+
from dnakit.core._json import to_json_compatible
|
|
18
|
+
from dnakit.core.collection import DNASet
|
|
19
|
+
from dnakit.core.enums import ExecutionMode, ImplementationLabel, OriginClass
|
|
20
|
+
from dnakit.core.facade import DNA
|
|
21
|
+
from dnakit.core.provenance import ArtifactRef, ImplementationInfo, Provenance
|
|
22
|
+
from dnakit.core.record import DNARecord
|
|
23
|
+
from dnakit.core.sequence import DNASequence
|
|
24
|
+
from dnakit.exceptions import ConfigurationError, InputFormatError
|
|
25
|
+
|
|
26
|
+
from ._formats import (
|
|
27
|
+
iter_delimited,
|
|
28
|
+
iter_fasta,
|
|
29
|
+
iter_fastq,
|
|
30
|
+
iter_json_array,
|
|
31
|
+
iter_json_lines,
|
|
32
|
+
write_delimited,
|
|
33
|
+
write_fasta,
|
|
34
|
+
write_fastq,
|
|
35
|
+
write_json_array,
|
|
36
|
+
write_json_lines,
|
|
37
|
+
)
|
|
38
|
+
from .config import ReadConfig, WriteConfig
|
|
39
|
+
from .genbank import iter_genbank, write_genbank
|
|
40
|
+
from .results import GeneratedID, WriteResult
|
|
41
|
+
from .source import RecordSource
|
|
42
|
+
|
|
43
|
+
PathSource: TypeAlias = str | os.PathLike[str]
|
|
44
|
+
ReadableSource: TypeAlias = PathSource | TextIO | BinaryIO
|
|
45
|
+
WritableTarget: TypeAlias = PathSource | TextIO | BinaryIO
|
|
46
|
+
WritableRecords: TypeAlias = DNA | DNASequence | DNARecord | Iterable[DNASequence | DNARecord]
|
|
47
|
+
ReadMode: TypeAlias = Literal["stream", "dna"]
|
|
48
|
+
|
|
49
|
+
_FORMAT_ALIASES = {
|
|
50
|
+
"fa": "fasta",
|
|
51
|
+
"fasta": "fasta",
|
|
52
|
+
"fna": "fasta",
|
|
53
|
+
"ffn": "fasta",
|
|
54
|
+
"faa": "fasta",
|
|
55
|
+
"fq": "fastq",
|
|
56
|
+
"fastq": "fastq",
|
|
57
|
+
"csv": "csv",
|
|
58
|
+
"tsv": "tsv",
|
|
59
|
+
"tab": "tsv",
|
|
60
|
+
"json": "json",
|
|
61
|
+
"jsonl": "jsonl",
|
|
62
|
+
"ndjson": "jsonl",
|
|
63
|
+
"gb": "genbank",
|
|
64
|
+
"gbk": "genbank",
|
|
65
|
+
"gbff": "genbank",
|
|
66
|
+
"genbank": "genbank",
|
|
67
|
+
}
|
|
68
|
+
_UNSUPPORTED_FORMATS = {
|
|
69
|
+
"parquet": "Parquet support requires the optional advanced table backend.",
|
|
70
|
+
"gff": "GFF support belongs to the advanced annotation codec.",
|
|
71
|
+
"gff3": "GFF3 support belongs to the advanced annotation codec.",
|
|
72
|
+
"bed": "BED support belongs to the advanced annotation codec.",
|
|
73
|
+
"agp": "AGP support belongs to the advanced assembly codec.",
|
|
74
|
+
"fai": "Indexed FASTA access belongs to the advanced indexing adapter.",
|
|
75
|
+
}
|
|
76
|
+
_MEDIA_TYPES = {
|
|
77
|
+
"fasta": "text/x-fasta",
|
|
78
|
+
"fastq": "text/x-fastq",
|
|
79
|
+
"csv": "text/csv",
|
|
80
|
+
"tsv": "text/tab-separated-values",
|
|
81
|
+
"json": "application/json",
|
|
82
|
+
"jsonl": "application/x-ndjson",
|
|
83
|
+
"genbank": "text/x-genbank",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _BoundedTextReader:
|
|
88
|
+
"""Proxy a decoded stream while bounding bytes returned to parsers."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, raw: TextIO, *, encoding: str, limit: int, source: str | None) -> None:
|
|
91
|
+
self._raw = raw
|
|
92
|
+
self._encoding = encoding
|
|
93
|
+
self._encoder = codecs.getincrementalencoder(encoding)()
|
|
94
|
+
self._limit = limit
|
|
95
|
+
self._source = source
|
|
96
|
+
self._count = 0
|
|
97
|
+
|
|
98
|
+
def _checked(self, value: str) -> str:
|
|
99
|
+
self._count += len(self._encoder.encode(value))
|
|
100
|
+
if self._count > self._limit:
|
|
101
|
+
raise InputFormatError(
|
|
102
|
+
"Decoded input exceeds max_input_bytes.",
|
|
103
|
+
code="INPUT_BYTE_LIMIT_EXCEEDED",
|
|
104
|
+
context={"source": self._source, "max_input_bytes": self._limit},
|
|
105
|
+
)
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
def _bounded_size(self, size: int | None) -> int:
|
|
109
|
+
remaining = self._limit - self._count
|
|
110
|
+
if size is None or size < 0:
|
|
111
|
+
return remaining + 1
|
|
112
|
+
return min(size, remaining + 1)
|
|
113
|
+
|
|
114
|
+
def read(self, size: int = -1) -> str:
|
|
115
|
+
return self._checked(self._raw.read(self._bounded_size(size)))
|
|
116
|
+
|
|
117
|
+
def readline(self, size: int = -1) -> str:
|
|
118
|
+
return self._checked(self._raw.readline(self._bounded_size(size)))
|
|
119
|
+
|
|
120
|
+
def __iter__(self) -> _BoundedTextReader:
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def __next__(self) -> str:
|
|
124
|
+
value = self.readline()
|
|
125
|
+
if not value:
|
|
126
|
+
raise StopIteration
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
def __getattr__(self, name: str) -> object:
|
|
130
|
+
return getattr(self._raw, name)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class _BoundedBinaryWriter:
|
|
134
|
+
"""Count physical output bytes before forwarding each binary write."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, raw: BinaryIO, *, limit: int) -> None:
|
|
137
|
+
self._raw = raw
|
|
138
|
+
self._limit = limit
|
|
139
|
+
self._count = 0
|
|
140
|
+
|
|
141
|
+
def write(self, value: bytes | bytearray | memoryview) -> int:
|
|
142
|
+
size = len(value)
|
|
143
|
+
if self._count + size > self._limit:
|
|
144
|
+
raise InputFormatError(
|
|
145
|
+
"Serialized output exceeds max_output_bytes.",
|
|
146
|
+
code="OUTPUT_BYTE_LIMIT_EXCEEDED",
|
|
147
|
+
context={"max_output_bytes": self._limit},
|
|
148
|
+
)
|
|
149
|
+
written = self._raw.write(value)
|
|
150
|
+
self._count += written
|
|
151
|
+
return written
|
|
152
|
+
|
|
153
|
+
def flush(self) -> None:
|
|
154
|
+
self._raw.flush()
|
|
155
|
+
|
|
156
|
+
def __getattr__(self, name: str) -> object:
|
|
157
|
+
return getattr(self._raw, name)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class _BoundedTextWriter:
|
|
161
|
+
"""Count encoded output bytes before forwarding writes to a text stream."""
|
|
162
|
+
|
|
163
|
+
def __init__(self, raw: TextIO, *, encoding: str, limit: int) -> None:
|
|
164
|
+
self._raw = raw
|
|
165
|
+
self._encoding = encoding
|
|
166
|
+
self._limit = limit
|
|
167
|
+
self._count = 0
|
|
168
|
+
|
|
169
|
+
def write(self, value: str) -> int:
|
|
170
|
+
size = len(value.encode(self._encoding))
|
|
171
|
+
if self._count + size > self._limit:
|
|
172
|
+
raise InputFormatError(
|
|
173
|
+
"Serialized output exceeds max_output_bytes.",
|
|
174
|
+
code="OUTPUT_BYTE_LIMIT_EXCEEDED",
|
|
175
|
+
context={"max_output_bytes": self._limit},
|
|
176
|
+
)
|
|
177
|
+
written = self._raw.write(value)
|
|
178
|
+
self._count += size
|
|
179
|
+
return written
|
|
180
|
+
|
|
181
|
+
def flush(self) -> None:
|
|
182
|
+
self._raw.flush()
|
|
183
|
+
|
|
184
|
+
def __getattr__(self, name: str) -> object:
|
|
185
|
+
return getattr(self._raw, name)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _path(value: object) -> Path | None:
|
|
189
|
+
if isinstance(value, (str, os.PathLike)):
|
|
190
|
+
return Path(value)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _stream_name(stream: object) -> str | None:
|
|
195
|
+
value = getattr(stream, "name", None)
|
|
196
|
+
if not isinstance(value, (str, os.PathLike)):
|
|
197
|
+
return None
|
|
198
|
+
name = os.fspath(value)
|
|
199
|
+
return name if isinstance(name, str) and name else None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _normalize_format(value: str) -> str:
|
|
203
|
+
if not isinstance(value, str):
|
|
204
|
+
raise TypeError("format must be a string or None.")
|
|
205
|
+
normalized = value.strip().lower().lstrip(".")
|
|
206
|
+
if normalized.endswith(".gz"):
|
|
207
|
+
normalized = normalized[:-3]
|
|
208
|
+
if normalized in _FORMAT_ALIASES:
|
|
209
|
+
return _FORMAT_ALIASES[normalized]
|
|
210
|
+
if normalized in _UNSUPPORTED_FORMATS:
|
|
211
|
+
raise InputFormatError(
|
|
212
|
+
f"Format {value!r} is not supported by the MVP.",
|
|
213
|
+
code="UNSUPPORTED_FORMAT",
|
|
214
|
+
context={"format": normalized},
|
|
215
|
+
hint=_UNSUPPORTED_FORMATS[normalized],
|
|
216
|
+
)
|
|
217
|
+
raise InputFormatError(
|
|
218
|
+
f"Unknown DNAKit I/O format {value!r}.",
|
|
219
|
+
code="UNSUPPORTED_FORMAT",
|
|
220
|
+
context={"format": normalized},
|
|
221
|
+
hint="Choose FASTA, FASTQ, GenBank, CSV, TSV, JSON, or JSONL.",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _infer_format(name: str | None, explicit: str | None) -> str:
|
|
226
|
+
if explicit is not None:
|
|
227
|
+
return _normalize_format(explicit)
|
|
228
|
+
if name is None:
|
|
229
|
+
raise InputFormatError(
|
|
230
|
+
"format is required for a stream without a usable filename.",
|
|
231
|
+
code="FORMAT_REQUIRED",
|
|
232
|
+
hint=("Pass format='fasta', 'fastq', 'genbank', 'csv', 'tsv', 'json', or 'jsonl'."),
|
|
233
|
+
)
|
|
234
|
+
filename = Path(name).name.lower()
|
|
235
|
+
if filename.endswith(".gz"):
|
|
236
|
+
filename = filename[:-3]
|
|
237
|
+
suffix = Path(filename).suffix.lstrip(".")
|
|
238
|
+
if not suffix:
|
|
239
|
+
raise InputFormatError(
|
|
240
|
+
"Could not infer a format from the filename.",
|
|
241
|
+
code="FORMAT_REQUIRED",
|
|
242
|
+
context={"source": name},
|
|
243
|
+
)
|
|
244
|
+
return _normalize_format(suffix)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _use_gzip(name: str | None, compression: str) -> bool:
|
|
248
|
+
if compression == "gzip":
|
|
249
|
+
return True
|
|
250
|
+
if compression == "none":
|
|
251
|
+
return False
|
|
252
|
+
return bool(name and name.lower().endswith(".gz"))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _open_read_text(
|
|
256
|
+
source: ReadableSource, config: ReadConfig
|
|
257
|
+
) -> tuple[TextIO, Callable[[], None], str | None]:
|
|
258
|
+
path = _path(source)
|
|
259
|
+
name = str(path) if path is not None else _stream_name(source)
|
|
260
|
+
compressed = _use_gzip(name, config.compression)
|
|
261
|
+
if path is not None:
|
|
262
|
+
try:
|
|
263
|
+
if compressed:
|
|
264
|
+
handle: TextIO = gzip.open( # noqa: SIM115 - lazy owner is RecordSource.
|
|
265
|
+
path, "rt", encoding=config.encoding, newline=""
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
handle = path.open("r", encoding=config.encoding, newline="")
|
|
269
|
+
except OSError as exc:
|
|
270
|
+
raise InputFormatError(
|
|
271
|
+
"Could not open input path.",
|
|
272
|
+
code="INPUT_OPEN_FAILED",
|
|
273
|
+
context={"source": str(path), "reason": str(exc)},
|
|
274
|
+
) from exc
|
|
275
|
+
bounded = _BoundedTextReader(
|
|
276
|
+
handle, encoding=config.encoding, limit=config.max_input_bytes, source=name
|
|
277
|
+
)
|
|
278
|
+
return cast(TextIO, bounded), handle.close, name
|
|
279
|
+
|
|
280
|
+
if not hasattr(source, "read"):
|
|
281
|
+
raise TypeError("source must be a path or a readable text/binary stream.")
|
|
282
|
+
close_source = config.close_source is True
|
|
283
|
+
probe = source.read(0)
|
|
284
|
+
if isinstance(probe, str):
|
|
285
|
+
text_handle = cast(TextIO, source)
|
|
286
|
+
if compressed:
|
|
287
|
+
if close_source:
|
|
288
|
+
text_handle.close()
|
|
289
|
+
raise ConfigurationError(
|
|
290
|
+
"A text stream cannot be decompressed by DNAKit.",
|
|
291
|
+
code="TEXT_STREAM_COMPRESSION_CONFLICT",
|
|
292
|
+
hint="Pass an already decompressed text stream with compression='none'.",
|
|
293
|
+
)
|
|
294
|
+
bounded = _BoundedTextReader(
|
|
295
|
+
text_handle, encoding=config.encoding, limit=config.max_input_bytes, source=name
|
|
296
|
+
)
|
|
297
|
+
return (
|
|
298
|
+
cast(TextIO, bounded),
|
|
299
|
+
text_handle.close if close_source else (lambda: None),
|
|
300
|
+
name,
|
|
301
|
+
)
|
|
302
|
+
if not isinstance(probe, bytes):
|
|
303
|
+
raise TypeError("source.read(0) must return str or bytes.")
|
|
304
|
+
|
|
305
|
+
binary_handle = cast(BinaryIO, source)
|
|
306
|
+
if compressed:
|
|
307
|
+
gzip_handle = gzip.GzipFile(fileobj=binary_handle, mode="rb")
|
|
308
|
+
gzip_wrapper = io.TextIOWrapper(gzip_handle, encoding=config.encoding, newline="")
|
|
309
|
+
|
|
310
|
+
def close_binary_gzip() -> None:
|
|
311
|
+
try:
|
|
312
|
+
gzip_wrapper.close()
|
|
313
|
+
finally:
|
|
314
|
+
if close_source:
|
|
315
|
+
binary_handle.close()
|
|
316
|
+
|
|
317
|
+
bounded = _BoundedTextReader(
|
|
318
|
+
gzip_wrapper, encoding=config.encoding, limit=config.max_input_bytes, source=name
|
|
319
|
+
)
|
|
320
|
+
return cast(TextIO, bounded), close_binary_gzip, name
|
|
321
|
+
|
|
322
|
+
binary_wrapper = io.TextIOWrapper(binary_handle, encoding=config.encoding, newline="")
|
|
323
|
+
|
|
324
|
+
def release_binary() -> None:
|
|
325
|
+
if binary_wrapper.closed:
|
|
326
|
+
return
|
|
327
|
+
if close_source:
|
|
328
|
+
binary_wrapper.close()
|
|
329
|
+
else:
|
|
330
|
+
binary_wrapper.detach()
|
|
331
|
+
|
|
332
|
+
bounded = _BoundedTextReader(
|
|
333
|
+
binary_wrapper, encoding=config.encoding, limit=config.max_input_bytes, source=name
|
|
334
|
+
)
|
|
335
|
+
return cast(TextIO, bounded), release_binary, name
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _reader(handle: TextIO, format: str, config: ReadConfig) -> Iterator[DNARecord]:
|
|
339
|
+
if format == "fasta":
|
|
340
|
+
return iter_fasta(handle, config)
|
|
341
|
+
if format == "fastq":
|
|
342
|
+
return iter_fastq(handle, config)
|
|
343
|
+
if format in {"csv", "tsv"}:
|
|
344
|
+
delimiter = config.delimiter or ("," if format == "csv" else "\t")
|
|
345
|
+
return iter_delimited(handle, config, delimiter=delimiter)
|
|
346
|
+
if format == "json":
|
|
347
|
+
return iter_json_array(handle, config)
|
|
348
|
+
if format == "jsonl":
|
|
349
|
+
return iter_json_lines(handle, config)
|
|
350
|
+
if format == "genbank":
|
|
351
|
+
return iter_genbank(handle, config)
|
|
352
|
+
raise AssertionError(f"Unreachable normalized format: {format}")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _guard_reader(
|
|
356
|
+
iterator: Iterator[DNARecord], *, source_name: str | None, max_records: int
|
|
357
|
+
) -> Iterator[DNARecord]:
|
|
358
|
+
try:
|
|
359
|
+
for index, record in enumerate(iterator, start=1):
|
|
360
|
+
if index > max_records:
|
|
361
|
+
raise InputFormatError(
|
|
362
|
+
"Input exceeds the configured record limit.",
|
|
363
|
+
code="INPUT_RECORD_LIMIT_EXCEEDED",
|
|
364
|
+
context={"source": source_name, "max_records": max_records},
|
|
365
|
+
)
|
|
366
|
+
yield record
|
|
367
|
+
except (InputFormatError, ConfigurationError):
|
|
368
|
+
raise
|
|
369
|
+
except UnicodeError as exc:
|
|
370
|
+
raise InputFormatError(
|
|
371
|
+
"Input could not be decoded with the configured encoding.",
|
|
372
|
+
code="INPUT_DECODE_FAILED",
|
|
373
|
+
context={"source": source_name, "reason": str(exc)},
|
|
374
|
+
) from exc
|
|
375
|
+
except OSError as exc:
|
|
376
|
+
raise InputFormatError(
|
|
377
|
+
"Input stream could not be read or decompressed.",
|
|
378
|
+
code="INPUT_READ_FAILED",
|
|
379
|
+
context={"source": source_name, "reason": str(exc)},
|
|
380
|
+
) from exc
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@overload
|
|
384
|
+
def read(
|
|
385
|
+
source: ReadableSource,
|
|
386
|
+
*,
|
|
387
|
+
format: str | None = None,
|
|
388
|
+
config: ReadConfig | None = None,
|
|
389
|
+
mode: Literal["stream"] = "stream",
|
|
390
|
+
) -> RecordSource: ...
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@overload
|
|
394
|
+
def read(
|
|
395
|
+
source: ReadableSource,
|
|
396
|
+
*,
|
|
397
|
+
format: str | None = None,
|
|
398
|
+
config: ReadConfig | None = None,
|
|
399
|
+
mode: Literal["dna"],
|
|
400
|
+
) -> DNA: ...
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def read(
|
|
404
|
+
source: ReadableSource,
|
|
405
|
+
*,
|
|
406
|
+
format: str | None = None,
|
|
407
|
+
config: ReadConfig | None = None,
|
|
408
|
+
mode: ReadMode = "stream",
|
|
409
|
+
) -> RecordSource | DNA:
|
|
410
|
+
"""Read DNA with one name, choosing a lazy stream or unified DNA object."""
|
|
411
|
+
|
|
412
|
+
resolved = ReadConfig() if config is None else config
|
|
413
|
+
if not isinstance(resolved, ReadConfig):
|
|
414
|
+
raise TypeError("config must be ReadConfig or None.")
|
|
415
|
+
if mode not in {"stream", "dna"}:
|
|
416
|
+
raise ConfigurationError(
|
|
417
|
+
"mode must be 'stream' or 'dna'.",
|
|
418
|
+
code="INVALID_READ_MODE",
|
|
419
|
+
context={"mode": mode},
|
|
420
|
+
)
|
|
421
|
+
path = _path(source)
|
|
422
|
+
name = str(path) if path is not None else _stream_name(source)
|
|
423
|
+
resolved_format = _infer_format(name, format)
|
|
424
|
+
handle, close_callback, source_name = _open_read_text(source, resolved)
|
|
425
|
+
try:
|
|
426
|
+
iterator = _guard_reader(
|
|
427
|
+
_reader(handle, resolved_format, resolved),
|
|
428
|
+
source_name=source_name,
|
|
429
|
+
max_records=resolved.max_records,
|
|
430
|
+
)
|
|
431
|
+
except BaseException:
|
|
432
|
+
close_callback()
|
|
433
|
+
raise
|
|
434
|
+
records = RecordSource(
|
|
435
|
+
iterator,
|
|
436
|
+
close_callback=close_callback,
|
|
437
|
+
source_name=source_name,
|
|
438
|
+
format=resolved_format,
|
|
439
|
+
)
|
|
440
|
+
if mode == "dna":
|
|
441
|
+
return DNA(records.collect())
|
|
442
|
+
return records
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def read_one(
|
|
446
|
+
source: ReadableSource,
|
|
447
|
+
*,
|
|
448
|
+
format: str | None = None,
|
|
449
|
+
config: ReadConfig | None = None,
|
|
450
|
+
) -> DNARecord:
|
|
451
|
+
"""Read exactly one record, rejecting empty and multi-record inputs."""
|
|
452
|
+
|
|
453
|
+
with read(source, format=format, config=config, mode="stream") as records:
|
|
454
|
+
try:
|
|
455
|
+
first = next(records)
|
|
456
|
+
except StopIteration as exc:
|
|
457
|
+
raise InputFormatError(
|
|
458
|
+
"Expected exactly one record, but the input was empty.",
|
|
459
|
+
code="EXPECTED_ONE_RECORD",
|
|
460
|
+
context={"record_count": 0},
|
|
461
|
+
) from exc
|
|
462
|
+
try:
|
|
463
|
+
next(records)
|
|
464
|
+
except StopIteration:
|
|
465
|
+
return first
|
|
466
|
+
raise InputFormatError(
|
|
467
|
+
"Expected exactly one record, but the input contained multiple records.",
|
|
468
|
+
code="EXPECTED_ONE_RECORD",
|
|
469
|
+
context={"record_count": "at_least_2"},
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def read_set(
|
|
474
|
+
source: ReadableSource,
|
|
475
|
+
*,
|
|
476
|
+
format: str | None = None,
|
|
477
|
+
config: ReadConfig | None = None,
|
|
478
|
+
) -> DNASet:
|
|
479
|
+
"""Explicitly materialize every input record into a repeatable DNASet."""
|
|
480
|
+
|
|
481
|
+
return read(source, format=format, config=config, mode="stream").collect()
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _records_with_ids(
|
|
485
|
+
records: WritableRecords,
|
|
486
|
+
config: WriteConfig,
|
|
487
|
+
generated_ids: list[GeneratedID],
|
|
488
|
+
) -> Iterator[DNARecord]:
|
|
489
|
+
if isinstance(records, (DNASequence, DNARecord)):
|
|
490
|
+
values: Iterable[DNASequence | DNARecord] = (records,)
|
|
491
|
+
elif isinstance(records, Iterable):
|
|
492
|
+
values = records
|
|
493
|
+
else:
|
|
494
|
+
raise TypeError("records must be DNASequence, DNARecord, or an iterable of them.")
|
|
495
|
+
for index, value in enumerate(values):
|
|
496
|
+
if isinstance(value, DNARecord):
|
|
497
|
+
yield value
|
|
498
|
+
elif isinstance(value, DNASequence):
|
|
499
|
+
if config.anonymous_id_policy == "error":
|
|
500
|
+
raise InputFormatError(
|
|
501
|
+
"Anonymous DNASequence output is disabled by configuration.",
|
|
502
|
+
code="ANONYMOUS_SEQUENCE_ID_REQUIRED",
|
|
503
|
+
context={"input_index": index},
|
|
504
|
+
)
|
|
505
|
+
generated_id = f"{config.anonymous_id_prefix}{index + 1}"
|
|
506
|
+
generated_ids.append(GeneratedID(index, generated_id))
|
|
507
|
+
yield DNARecord(value, generated_id)
|
|
508
|
+
else:
|
|
509
|
+
raise TypeError(
|
|
510
|
+
f"records[{index}] must be DNASequence or DNARecord, not {type(value).__name__}."
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _writer(
|
|
515
|
+
handle: TextIO,
|
|
516
|
+
records: Iterable[DNARecord],
|
|
517
|
+
format: str,
|
|
518
|
+
config: WriteConfig,
|
|
519
|
+
) -> int:
|
|
520
|
+
if format == "fasta":
|
|
521
|
+
return write_fasta(handle, records, config)
|
|
522
|
+
if format == "fastq":
|
|
523
|
+
return write_fastq(handle, records, config)
|
|
524
|
+
if format in {"csv", "tsv"}:
|
|
525
|
+
delimiter = config.delimiter or ("," if format == "csv" else "\t")
|
|
526
|
+
return write_delimited(handle, records, config, delimiter=delimiter)
|
|
527
|
+
if format == "json":
|
|
528
|
+
return write_json_array(handle, records, config)
|
|
529
|
+
if format == "jsonl":
|
|
530
|
+
return write_json_lines(handle, records, config)
|
|
531
|
+
if format == "genbank":
|
|
532
|
+
return write_genbank(handle, records, config)
|
|
533
|
+
raise AssertionError(f"Unreachable normalized format: {format}")
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _write_to_stream(
|
|
537
|
+
target: TextIO | BinaryIO,
|
|
538
|
+
records: Iterable[DNARecord],
|
|
539
|
+
format: str,
|
|
540
|
+
config: WriteConfig,
|
|
541
|
+
*,
|
|
542
|
+
compressed: bool,
|
|
543
|
+
) -> int:
|
|
544
|
+
close_target = config.close_target is True
|
|
545
|
+
try:
|
|
546
|
+
cast(Any, target).write("")
|
|
547
|
+
is_text_target = True
|
|
548
|
+
except TypeError:
|
|
549
|
+
is_text_target = False
|
|
550
|
+
if is_text_target:
|
|
551
|
+
text_target = cast(TextIO, target)
|
|
552
|
+
if compressed:
|
|
553
|
+
if close_target:
|
|
554
|
+
text_target.close()
|
|
555
|
+
raise ConfigurationError(
|
|
556
|
+
"A text target cannot be gzip-compressed by DNAKit.",
|
|
557
|
+
code="TEXT_STREAM_COMPRESSION_CONFLICT",
|
|
558
|
+
hint="Pass a binary stream or an output path ending in .gz.",
|
|
559
|
+
)
|
|
560
|
+
try:
|
|
561
|
+
destination = (
|
|
562
|
+
cast(
|
|
563
|
+
TextIO,
|
|
564
|
+
_BoundedTextWriter(
|
|
565
|
+
text_target,
|
|
566
|
+
encoding=config.encoding,
|
|
567
|
+
limit=config.max_output_bytes,
|
|
568
|
+
),
|
|
569
|
+
)
|
|
570
|
+
if config.max_output_bytes is not None
|
|
571
|
+
else text_target
|
|
572
|
+
)
|
|
573
|
+
count = _writer(destination, records, format, config)
|
|
574
|
+
text_target.flush()
|
|
575
|
+
return count
|
|
576
|
+
finally:
|
|
577
|
+
if close_target:
|
|
578
|
+
text_target.close()
|
|
579
|
+
|
|
580
|
+
binary_target = cast(BinaryIO, target)
|
|
581
|
+
bounded_target = (
|
|
582
|
+
cast(
|
|
583
|
+
BinaryIO,
|
|
584
|
+
_BoundedBinaryWriter(binary_target, limit=config.max_output_bytes),
|
|
585
|
+
)
|
|
586
|
+
if config.max_output_bytes is not None
|
|
587
|
+
else binary_target
|
|
588
|
+
)
|
|
589
|
+
raw: BinaryIO | None = None
|
|
590
|
+
try:
|
|
591
|
+
raw = (
|
|
592
|
+
cast(
|
|
593
|
+
BinaryIO,
|
|
594
|
+
gzip.GzipFile(
|
|
595
|
+
filename="",
|
|
596
|
+
fileobj=bounded_target,
|
|
597
|
+
mode="wb",
|
|
598
|
+
compresslevel=config.compression_level,
|
|
599
|
+
mtime=0,
|
|
600
|
+
),
|
|
601
|
+
)
|
|
602
|
+
if compressed
|
|
603
|
+
else bounded_target
|
|
604
|
+
)
|
|
605
|
+
wrapper = io.TextIOWrapper(raw, encoding=config.encoding, newline="")
|
|
606
|
+
except BaseException:
|
|
607
|
+
if compressed and raw is not None:
|
|
608
|
+
with suppress(BaseException):
|
|
609
|
+
raw.close()
|
|
610
|
+
if close_target:
|
|
611
|
+
binary_target.close()
|
|
612
|
+
raise
|
|
613
|
+
try:
|
|
614
|
+
count = _writer(wrapper, records, format, config)
|
|
615
|
+
wrapper.flush()
|
|
616
|
+
return count
|
|
617
|
+
finally:
|
|
618
|
+
if compressed:
|
|
619
|
+
try:
|
|
620
|
+
wrapper.close()
|
|
621
|
+
finally:
|
|
622
|
+
if close_target:
|
|
623
|
+
binary_target.close()
|
|
624
|
+
elif close_target:
|
|
625
|
+
wrapper.close()
|
|
626
|
+
elif not wrapper.closed:
|
|
627
|
+
wrapper.detach()
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _artifact(path: Path, format: str) -> ArtifactRef:
|
|
631
|
+
digest = hashlib.sha256()
|
|
632
|
+
with path.open("rb") as handle:
|
|
633
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
634
|
+
digest.update(chunk)
|
|
635
|
+
stat = path.stat()
|
|
636
|
+
return ArtifactRef(
|
|
637
|
+
relative_path=os.path.relpath(path.resolve(), Path.cwd()),
|
|
638
|
+
media_type=_MEDIA_TYPES[format],
|
|
639
|
+
schema_version="dnakit-io-v1",
|
|
640
|
+
sha256=digest.hexdigest(),
|
|
641
|
+
byte_size=stat.st_size,
|
|
642
|
+
created_at=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _write_path(
|
|
647
|
+
path: Path,
|
|
648
|
+
records: Iterable[DNARecord],
|
|
649
|
+
format: str,
|
|
650
|
+
config: WriteConfig,
|
|
651
|
+
) -> tuple[int, ArtifactRef]:
|
|
652
|
+
if path.exists() and not config.overwrite:
|
|
653
|
+
raise FileExistsError(f"Refusing to overwrite existing output: {path}")
|
|
654
|
+
parent = path.parent
|
|
655
|
+
if not parent.exists():
|
|
656
|
+
if config.create_parents:
|
|
657
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
658
|
+
else:
|
|
659
|
+
raise FileNotFoundError(f"Output parent directory does not exist: {parent}")
|
|
660
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
661
|
+
prefix=f".{path.name}.", suffix=".tmp", dir=parent
|
|
662
|
+
)
|
|
663
|
+
temporary_path = Path(temporary_name)
|
|
664
|
+
os.close(descriptor)
|
|
665
|
+
compressed = _use_gzip(str(path), config.compression)
|
|
666
|
+
try:
|
|
667
|
+
with temporary_path.open("wb") as binary_target:
|
|
668
|
+
stream_config = WriteConfig(
|
|
669
|
+
encoding=config.encoding,
|
|
670
|
+
overwrite=config.overwrite,
|
|
671
|
+
create_parents=config.create_parents,
|
|
672
|
+
compression=config.compression,
|
|
673
|
+
compression_level=config.compression_level,
|
|
674
|
+
close_target=False,
|
|
675
|
+
line_width=config.line_width,
|
|
676
|
+
phred_offset=config.phred_offset,
|
|
677
|
+
delimiter=config.delimiter,
|
|
678
|
+
anonymous_id_policy=config.anonymous_id_policy,
|
|
679
|
+
anonymous_id_prefix=config.anonymous_id_prefix,
|
|
680
|
+
feature_policy=config.feature_policy,
|
|
681
|
+
json_indent=config.json_indent,
|
|
682
|
+
max_output_bytes=config.max_output_bytes,
|
|
683
|
+
)
|
|
684
|
+
count = _write_to_stream(
|
|
685
|
+
binary_target, records, format, stream_config, compressed=compressed
|
|
686
|
+
)
|
|
687
|
+
binary_target.flush()
|
|
688
|
+
os.fsync(binary_target.fileno())
|
|
689
|
+
if config.overwrite:
|
|
690
|
+
os.replace(temporary_path, path)
|
|
691
|
+
else:
|
|
692
|
+
os.link(temporary_path, path)
|
|
693
|
+
temporary_path.unlink()
|
|
694
|
+
except BaseException:
|
|
695
|
+
temporary_path.unlink(missing_ok=True)
|
|
696
|
+
raise
|
|
697
|
+
return count, _artifact(path, format)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def write(
|
|
701
|
+
records: WritableRecords,
|
|
702
|
+
target: WritableTarget,
|
|
703
|
+
*,
|
|
704
|
+
format: str | None = None,
|
|
705
|
+
config: WriteConfig | None = None,
|
|
706
|
+
) -> WriteResult:
|
|
707
|
+
"""Serialize records with stable anonymous IDs and safe path replacement."""
|
|
708
|
+
|
|
709
|
+
resolved = WriteConfig() if config is None else config
|
|
710
|
+
if not isinstance(resolved, WriteConfig):
|
|
711
|
+
raise TypeError("config must be WriteConfig or None.")
|
|
712
|
+
path = _path(target)
|
|
713
|
+
name = str(path) if path is not None else _stream_name(target)
|
|
714
|
+
resolved_format = _infer_format(name, format)
|
|
715
|
+
generated_ids: list[GeneratedID] = []
|
|
716
|
+
prepared = _records_with_ids(records, resolved, generated_ids)
|
|
717
|
+
if path is not None:
|
|
718
|
+
count, artifact = _write_path(path, prepared, resolved_format, resolved)
|
|
719
|
+
byte_count: int | None = artifact.byte_size
|
|
720
|
+
else:
|
|
721
|
+
if not hasattr(target, "write"):
|
|
722
|
+
raise TypeError("target must be a path or writable text/binary stream.")
|
|
723
|
+
count = _write_to_stream(
|
|
724
|
+
cast(TextIO | BinaryIO, target),
|
|
725
|
+
prepared,
|
|
726
|
+
resolved_format,
|
|
727
|
+
resolved,
|
|
728
|
+
compressed=_use_gzip(name, resolved.compression),
|
|
729
|
+
)
|
|
730
|
+
artifact = None
|
|
731
|
+
byte_count = None
|
|
732
|
+
return WriteResult(
|
|
733
|
+
resolved_format,
|
|
734
|
+
count,
|
|
735
|
+
byte_count=byte_count,
|
|
736
|
+
generated_ids=generated_ids,
|
|
737
|
+
target_artifact=artifact,
|
|
738
|
+
parameters=cast(dict[str, object], to_json_compatible(resolved)),
|
|
739
|
+
provenance=Provenance(
|
|
740
|
+
implementation=ImplementationInfo(
|
|
741
|
+
label=ImplementationLabel.REIMPLEMENTATION,
|
|
742
|
+
execution_mode=ExecutionMode.INTERNAL,
|
|
743
|
+
origin_class=OriginClass.STANDARD,
|
|
744
|
+
)
|
|
745
|
+
),
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
__all__ = [
|
|
750
|
+
"PathSource",
|
|
751
|
+
"ReadMode",
|
|
752
|
+
"ReadableSource",
|
|
753
|
+
"WritableRecords",
|
|
754
|
+
"WritableTarget",
|
|
755
|
+
"read",
|
|
756
|
+
"read_one",
|
|
757
|
+
"read_set",
|
|
758
|
+
"write",
|
|
759
|
+
]
|