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,295 @@
|
|
|
1
|
+
"""Internal and external DNA coordinate value objects and conversions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TypeAlias
|
|
8
|
+
|
|
9
|
+
from dnakit.core.enums import CoordinateSystem, Strand
|
|
10
|
+
from dnakit.exceptions import CoordinateError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _require_non_negative_integer(value: int, name: str) -> None:
|
|
14
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
15
|
+
raise CoordinateError(
|
|
16
|
+
f"{name} must be a non-negative integer.",
|
|
17
|
+
context={name: value},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _coerce_coordinate_system(value: CoordinateSystem | str) -> CoordinateSystem:
|
|
22
|
+
try:
|
|
23
|
+
return value if isinstance(value, CoordinateSystem) else CoordinateSystem(value)
|
|
24
|
+
except (TypeError, ValueError) as exc:
|
|
25
|
+
raise CoordinateError(
|
|
26
|
+
"Unknown coordinate system.",
|
|
27
|
+
context={"system": value},
|
|
28
|
+
hint=f"Choose one of: {', '.join(item.value for item in CoordinateSystem)}.",
|
|
29
|
+
) from exc
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _coerce_strand(value: Strand | str) -> Strand:
|
|
33
|
+
try:
|
|
34
|
+
return value if isinstance(value, Strand) else Strand(value)
|
|
35
|
+
except (TypeError, ValueError) as exc:
|
|
36
|
+
raise CoordinateError(
|
|
37
|
+
"Unknown strand value.",
|
|
38
|
+
context={"strand": value},
|
|
39
|
+
hint=f"Choose one of: {', '.join(item.value for item in Strand)}.",
|
|
40
|
+
) from exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, order=True)
|
|
44
|
+
class Interval:
|
|
45
|
+
"""An internal zero-based, half-open interval."""
|
|
46
|
+
|
|
47
|
+
start: int
|
|
48
|
+
end: int
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
_require_non_negative_integer(self.start, "start")
|
|
52
|
+
_require_non_negative_integer(self.end, "end")
|
|
53
|
+
if self.end < self.start:
|
|
54
|
+
raise CoordinateError(
|
|
55
|
+
"Internal interval end cannot be smaller than start.",
|
|
56
|
+
context={"start": self.start, "end": self.end},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def __len__(self) -> int:
|
|
60
|
+
return self.end - self.start
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, init=False)
|
|
64
|
+
class CompoundLocation:
|
|
65
|
+
"""An ordered set of internal intervals, including origin-spanning features."""
|
|
66
|
+
|
|
67
|
+
parts: tuple[Interval, ...]
|
|
68
|
+
|
|
69
|
+
def __init__(self, parts: Iterable[Interval]) -> None:
|
|
70
|
+
resolved = tuple(parts)
|
|
71
|
+
if not resolved:
|
|
72
|
+
raise CoordinateError("A compound location requires at least one interval.")
|
|
73
|
+
if any(not isinstance(part, Interval) for part in resolved):
|
|
74
|
+
raise CoordinateError("Compound location parts must all be Interval objects.")
|
|
75
|
+
object.__setattr__(self, "parts", resolved)
|
|
76
|
+
|
|
77
|
+
def __len__(self) -> int:
|
|
78
|
+
return sum(len(part) for part in self.parts)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, init=False)
|
|
82
|
+
class UnresolvedLocation:
|
|
83
|
+
"""A location that cannot be resolved, usually because a gap length is unknown."""
|
|
84
|
+
|
|
85
|
+
reason: str
|
|
86
|
+
anchors: tuple[Interval, ...]
|
|
87
|
+
|
|
88
|
+
def __init__(self, reason: str, anchors: Iterable[Interval] = ()) -> None:
|
|
89
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
90
|
+
raise CoordinateError("An unresolved location requires a non-empty reason.")
|
|
91
|
+
resolved_anchors = tuple(anchors)
|
|
92
|
+
if any(not isinstance(anchor, Interval) for anchor in resolved_anchors):
|
|
93
|
+
raise CoordinateError("Unresolved location anchors must be Interval objects.")
|
|
94
|
+
object.__setattr__(self, "reason", reason)
|
|
95
|
+
object.__setattr__(self, "anchors", resolved_anchors)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
Location: TypeAlias = Interval | CompoundLocation | UnresolvedLocation
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, init=False)
|
|
102
|
+
class ExternalInterval:
|
|
103
|
+
"""An interval retaining its external coordinate convention and strand reference."""
|
|
104
|
+
|
|
105
|
+
start: int
|
|
106
|
+
end: int
|
|
107
|
+
system: CoordinateSystem
|
|
108
|
+
strand: Strand
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
start: int,
|
|
113
|
+
end: int,
|
|
114
|
+
system: CoordinateSystem | str,
|
|
115
|
+
strand: Strand | str = Strand.UNKNOWN,
|
|
116
|
+
) -> None:
|
|
117
|
+
resolved_system = _coerce_coordinate_system(system)
|
|
118
|
+
minimum = 0 if resolved_system.value.startswith("0-") else 1
|
|
119
|
+
if isinstance(start, bool) or not isinstance(start, int) or start < minimum:
|
|
120
|
+
raise CoordinateError(
|
|
121
|
+
"External interval start is outside its coordinate system.",
|
|
122
|
+
context={"start": start, "system": resolved_system.value},
|
|
123
|
+
)
|
|
124
|
+
if isinstance(end, bool) or not isinstance(end, int) or end < minimum:
|
|
125
|
+
raise CoordinateError(
|
|
126
|
+
"External interval end is outside its coordinate system.",
|
|
127
|
+
context={"end": end, "system": resolved_system.value},
|
|
128
|
+
)
|
|
129
|
+
object.__setattr__(self, "start", start)
|
|
130
|
+
object.__setattr__(self, "end", end)
|
|
131
|
+
object.__setattr__(self, "system", resolved_system)
|
|
132
|
+
object.__setattr__(self, "strand", _coerce_strand(strand))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _validate_sequence_length(sequence_length: int | None) -> None:
|
|
136
|
+
if sequence_length is not None:
|
|
137
|
+
_require_non_negative_integer(sequence_length, "sequence_length")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _to_internal_bounds(interval: ExternalInterval) -> tuple[int, int]:
|
|
141
|
+
if interval.system is CoordinateSystem.ZERO_BASED_HALF_OPEN:
|
|
142
|
+
return interval.start, interval.end
|
|
143
|
+
if interval.system is CoordinateSystem.ZERO_BASED_CLOSED:
|
|
144
|
+
return interval.start, interval.end + 1
|
|
145
|
+
if interval.system is CoordinateSystem.ONE_BASED_CLOSED:
|
|
146
|
+
return interval.start - 1, interval.end
|
|
147
|
+
if interval.system is CoordinateSystem.ONE_BASED_HALF_OPEN:
|
|
148
|
+
return interval.start - 1, interval.end - 1
|
|
149
|
+
raise AssertionError("Unhandled coordinate system.")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _import_one(interval: ExternalInterval, sequence_length: int | None) -> tuple[Interval, ...]:
|
|
153
|
+
start, end = _to_internal_bounds(interval)
|
|
154
|
+
wraps_origin = interval.start > interval.end
|
|
155
|
+
if not wraps_origin:
|
|
156
|
+
if sequence_length is not None and (start > sequence_length or end > sequence_length):
|
|
157
|
+
raise CoordinateError(
|
|
158
|
+
"External interval exceeds the sequence length.",
|
|
159
|
+
context={
|
|
160
|
+
"start": interval.start,
|
|
161
|
+
"end": interval.end,
|
|
162
|
+
"sequence_length": sequence_length,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
return (Interval(start, end),)
|
|
166
|
+
|
|
167
|
+
if sequence_length is None:
|
|
168
|
+
raise CoordinateError(
|
|
169
|
+
"A wrapped interval requires sequence_length.",
|
|
170
|
+
context={"start": interval.start, "end": interval.end},
|
|
171
|
+
hint="Provide the circular sequence length or split the interval explicitly.",
|
|
172
|
+
)
|
|
173
|
+
if start > sequence_length or end > sequence_length:
|
|
174
|
+
raise CoordinateError(
|
|
175
|
+
"Wrapped interval exceeds the sequence length.",
|
|
176
|
+
context={
|
|
177
|
+
"start": interval.start,
|
|
178
|
+
"end": interval.end,
|
|
179
|
+
"sequence_length": sequence_length,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
parts = tuple(
|
|
183
|
+
part for part in (Interval(start, sequence_length), Interval(0, end)) if len(part) > 0
|
|
184
|
+
)
|
|
185
|
+
if not parts:
|
|
186
|
+
raise CoordinateError("Wrapped interval resolves to an empty location.")
|
|
187
|
+
return parts
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def import_location(
|
|
191
|
+
external: ExternalInterval | Sequence[ExternalInterval],
|
|
192
|
+
*,
|
|
193
|
+
sequence_length: int | None = None,
|
|
194
|
+
) -> Location:
|
|
195
|
+
"""Normalize one or more external intervals to internal coordinates."""
|
|
196
|
+
|
|
197
|
+
_validate_sequence_length(sequence_length)
|
|
198
|
+
intervals = (external,) if isinstance(external, ExternalInterval) else tuple(external)
|
|
199
|
+
if not intervals:
|
|
200
|
+
raise CoordinateError("At least one external interval is required.")
|
|
201
|
+
if any(not isinstance(interval, ExternalInterval) for interval in intervals):
|
|
202
|
+
raise CoordinateError("All external locations must be ExternalInterval objects.")
|
|
203
|
+
|
|
204
|
+
parts = tuple(part for interval in intervals for part in _import_one(interval, sequence_length))
|
|
205
|
+
return parts[0] if len(parts) == 1 else CompoundLocation(parts)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _from_internal_bounds(
|
|
209
|
+
interval: Interval,
|
|
210
|
+
target_system: CoordinateSystem,
|
|
211
|
+
) -> tuple[int, int]:
|
|
212
|
+
if target_system is CoordinateSystem.ZERO_BASED_HALF_OPEN:
|
|
213
|
+
return interval.start, interval.end
|
|
214
|
+
if target_system is CoordinateSystem.ZERO_BASED_CLOSED:
|
|
215
|
+
if len(interval) == 0:
|
|
216
|
+
raise CoordinateError("A closed coordinate system cannot represent an empty interval.")
|
|
217
|
+
return interval.start, interval.end - 1
|
|
218
|
+
if target_system is CoordinateSystem.ONE_BASED_CLOSED:
|
|
219
|
+
if len(interval) == 0:
|
|
220
|
+
raise CoordinateError("A closed coordinate system cannot represent an empty interval.")
|
|
221
|
+
return interval.start + 1, interval.end
|
|
222
|
+
if target_system is CoordinateSystem.ONE_BASED_HALF_OPEN:
|
|
223
|
+
return interval.start + 1, interval.end + 1
|
|
224
|
+
raise AssertionError("Unhandled coordinate system.")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def export_location(
|
|
228
|
+
location: Location,
|
|
229
|
+
*,
|
|
230
|
+
target_system: CoordinateSystem | str,
|
|
231
|
+
sequence_length: int | None = None,
|
|
232
|
+
) -> tuple[ExternalInterval, ...]:
|
|
233
|
+
"""Export internal coordinates without merging compound-location parts."""
|
|
234
|
+
|
|
235
|
+
_validate_sequence_length(sequence_length)
|
|
236
|
+
system = _coerce_coordinate_system(target_system)
|
|
237
|
+
if isinstance(location, UnresolvedLocation):
|
|
238
|
+
raise CoordinateError(
|
|
239
|
+
"An unresolved location cannot be exported.",
|
|
240
|
+
context={"reason": location.reason},
|
|
241
|
+
)
|
|
242
|
+
if isinstance(location, Interval):
|
|
243
|
+
parts: tuple[Interval, ...] = (location,)
|
|
244
|
+
elif isinstance(location, CompoundLocation):
|
|
245
|
+
parts = location.parts
|
|
246
|
+
else:
|
|
247
|
+
raise CoordinateError("location must be an internal Location object.")
|
|
248
|
+
|
|
249
|
+
if sequence_length is not None and any(part.end > sequence_length for part in parts):
|
|
250
|
+
raise CoordinateError(
|
|
251
|
+
"Internal location exceeds the sequence length.",
|
|
252
|
+
context={"sequence_length": sequence_length},
|
|
253
|
+
)
|
|
254
|
+
return tuple(
|
|
255
|
+
ExternalInterval(*_from_internal_bounds(part, system), system=system) for part in parts
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def reverse_strand_location(location: Location, *, sequence_length: int) -> Location:
|
|
260
|
+
"""Map an internal location through reverse-complement coordinates."""
|
|
261
|
+
|
|
262
|
+
_validate_sequence_length(sequence_length)
|
|
263
|
+
if isinstance(location, Interval):
|
|
264
|
+
return _reverse_interval(location, sequence_length)
|
|
265
|
+
if isinstance(location, CompoundLocation):
|
|
266
|
+
return CompoundLocation(
|
|
267
|
+
_reverse_interval(part, sequence_length) for part in reversed(location.parts)
|
|
268
|
+
)
|
|
269
|
+
if isinstance(location, UnresolvedLocation):
|
|
270
|
+
return UnresolvedLocation(
|
|
271
|
+
location.reason,
|
|
272
|
+
(_reverse_interval(anchor, sequence_length) for anchor in reversed(location.anchors)),
|
|
273
|
+
)
|
|
274
|
+
raise CoordinateError("location must be an internal Location object.")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _reverse_interval(location: Interval, sequence_length: int) -> Interval:
|
|
278
|
+
if location.end > sequence_length:
|
|
279
|
+
raise CoordinateError(
|
|
280
|
+
"Location exceeds the sequence length.",
|
|
281
|
+
context={"end": location.end, "sequence_length": sequence_length},
|
|
282
|
+
)
|
|
283
|
+
return Interval(sequence_length - location.end, sequence_length - location.start)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
__all__ = [
|
|
287
|
+
"CompoundLocation",
|
|
288
|
+
"ExternalInterval",
|
|
289
|
+
"Interval",
|
|
290
|
+
"Location",
|
|
291
|
+
"UnresolvedLocation",
|
|
292
|
+
"export_location",
|
|
293
|
+
"import_location",
|
|
294
|
+
"reverse_strand_location",
|
|
295
|
+
]
|
dnakit/core/enums.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Stable string enums used by DNAKit core value objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StringEnum(str, Enum):
|
|
9
|
+
"""Python 3.10-compatible string enum."""
|
|
10
|
+
|
|
11
|
+
def __str__(self) -> str:
|
|
12
|
+
return str(self.value)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DNAAlphabet(StringEnum):
|
|
16
|
+
STRICT = "strict"
|
|
17
|
+
IUPAC = "iupac"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Topology(StringEnum):
|
|
21
|
+
LINEAR = "linear"
|
|
22
|
+
CIRCULAR = "circular"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Strandedness(StringEnum):
|
|
26
|
+
SINGLE = "single"
|
|
27
|
+
DOUBLE = "double"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Strand(StringEnum):
|
|
31
|
+
FORWARD = "forward"
|
|
32
|
+
REVERSE = "reverse"
|
|
33
|
+
BOTH = "both"
|
|
34
|
+
UNKNOWN = "unknown"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GapKind(StringEnum):
|
|
38
|
+
UNKNOWN = "unknown"
|
|
39
|
+
SCAFFOLD = "scaffold"
|
|
40
|
+
CONTIG = "contig"
|
|
41
|
+
CENTROMERE = "centromere"
|
|
42
|
+
SHORT_ARM = "short_arm"
|
|
43
|
+
HETEROCHROMATIN = "heterochromatin"
|
|
44
|
+
TELOMERE = "telomere"
|
|
45
|
+
REPEAT = "repeat"
|
|
46
|
+
CONTAMINATION = "contamination"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CoordinateSystem(StringEnum):
|
|
50
|
+
ZERO_BASED_HALF_OPEN = "0-based-half-open"
|
|
51
|
+
ZERO_BASED_CLOSED = "0-based-closed"
|
|
52
|
+
ONE_BASED_CLOSED = "1-based-closed"
|
|
53
|
+
ONE_BASED_HALF_OPEN = "1-based-half-open"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class IssueSeverity(StringEnum):
|
|
57
|
+
INFO = "info"
|
|
58
|
+
WARNING = "warning"
|
|
59
|
+
ERROR = "error"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ImplementationLabel(StringEnum):
|
|
63
|
+
NATIVE = "native"
|
|
64
|
+
ADAPTER = "adapter"
|
|
65
|
+
REIMPLEMENTATION = "reimplementation"
|
|
66
|
+
NOVEL = "novel"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ExecutionMode(StringEnum):
|
|
70
|
+
INTERNAL = "internal"
|
|
71
|
+
EXTERNAL = "external"
|
|
72
|
+
HYBRID = "hybrid"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class OriginClass(StringEnum):
|
|
76
|
+
DNAKIT = "dnakit"
|
|
77
|
+
STANDARD = "standard"
|
|
78
|
+
PUBLISHED_ALGORITHM = "published_algorithm"
|
|
79
|
+
INTEGRATION = "integration"
|
|
80
|
+
NOVEL = "novel"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"CoordinateSystem",
|
|
85
|
+
"DNAAlphabet",
|
|
86
|
+
"ExecutionMode",
|
|
87
|
+
"GapKind",
|
|
88
|
+
"ImplementationLabel",
|
|
89
|
+
"IssueSeverity",
|
|
90
|
+
"OriginClass",
|
|
91
|
+
"Strand",
|
|
92
|
+
"Strandedness",
|
|
93
|
+
"StringEnum",
|
|
94
|
+
"Topology",
|
|
95
|
+
]
|