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,281 @@
|
|
|
1
|
+
"""Small audited restriction-enzyme catalog and recognition-site scanning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from dnakit.core import CompoundLocation, Interval, Strand, UnresolvedLocation
|
|
10
|
+
from dnakit.exceptions import ConfigurationError
|
|
11
|
+
from dnakit.patterns._shared import (
|
|
12
|
+
SequenceInput,
|
|
13
|
+
build_result,
|
|
14
|
+
pattern_provenance,
|
|
15
|
+
resolve_sequence,
|
|
16
|
+
validate_iupac_text,
|
|
17
|
+
validate_positive_int,
|
|
18
|
+
)
|
|
19
|
+
from dnakit.patterns.motif import scan_motif
|
|
20
|
+
from dnakit.patterns.results import PatternResult, RestrictionSiteHit
|
|
21
|
+
|
|
22
|
+
RESTRICTION_CATALOG_VERSION = "common-enzymes-v1"
|
|
23
|
+
|
|
24
|
+
_COMPLEMENT = str.maketrans(
|
|
25
|
+
{
|
|
26
|
+
"A": "T",
|
|
27
|
+
"C": "G",
|
|
28
|
+
"G": "C",
|
|
29
|
+
"T": "A",
|
|
30
|
+
"R": "Y",
|
|
31
|
+
"Y": "R",
|
|
32
|
+
"S": "S",
|
|
33
|
+
"W": "W",
|
|
34
|
+
"K": "M",
|
|
35
|
+
"M": "K",
|
|
36
|
+
"B": "V",
|
|
37
|
+
"D": "H",
|
|
38
|
+
"H": "D",
|
|
39
|
+
"V": "B",
|
|
40
|
+
"N": "N",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, init=False)
|
|
46
|
+
class RestrictionEnzyme:
|
|
47
|
+
"""Recognition sequence and 0-based cleavage offsets from its forward start."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
recognition_sequence: str
|
|
51
|
+
top_cut_offset: int
|
|
52
|
+
bottom_cut_offset: int
|
|
53
|
+
source: str
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
name: str,
|
|
58
|
+
recognition_sequence: str,
|
|
59
|
+
top_cut_offset: int,
|
|
60
|
+
bottom_cut_offset: int,
|
|
61
|
+
*,
|
|
62
|
+
source: str = "user-provided",
|
|
63
|
+
) -> None:
|
|
64
|
+
if not isinstance(name, str) or not name.strip():
|
|
65
|
+
raise ConfigurationError("Restriction enzyme name must be non-empty.")
|
|
66
|
+
site = validate_iupac_text(recognition_sequence, "recognition_sequence")
|
|
67
|
+
for value, field_name in (
|
|
68
|
+
(top_cut_offset, "top_cut_offset"),
|
|
69
|
+
(bottom_cut_offset, "bottom_cut_offset"),
|
|
70
|
+
):
|
|
71
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
72
|
+
raise ConfigurationError(f"{field_name} must be an integer.")
|
|
73
|
+
if value < 0 or value > len(site):
|
|
74
|
+
raise ConfigurationError(
|
|
75
|
+
f"{field_name} must lie inside the recognition-site boundary.",
|
|
76
|
+
hint=(
|
|
77
|
+
"Use an external enzyme definition adapter for Type IIS cuts "
|
|
78
|
+
"outside the site."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
if not isinstance(source, str) or not source.strip():
|
|
82
|
+
raise ConfigurationError("Restriction enzyme source must be non-empty.")
|
|
83
|
+
object.__setattr__(self, "name", name)
|
|
84
|
+
object.__setattr__(self, "recognition_sequence", site)
|
|
85
|
+
object.__setattr__(self, "top_cut_offset", top_cut_offset)
|
|
86
|
+
object.__setattr__(self, "bottom_cut_offset", bottom_cut_offset)
|
|
87
|
+
object.__setattr__(self, "source", source)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_SOURCE = "DNAKit common-enzyme boundary catalog; verify release work against REBASE/vendor data"
|
|
91
|
+
BUILTIN_RESTRICTION_ENZYMES: Mapping[str, RestrictionEnzyme] = {
|
|
92
|
+
enzyme.name: enzyme
|
|
93
|
+
for enzyme in (
|
|
94
|
+
RestrictionEnzyme("BamHI", "GGATCC", 1, 5, source=_SOURCE),
|
|
95
|
+
RestrictionEnzyme("EcoRI", "GAATTC", 1, 5, source=_SOURCE),
|
|
96
|
+
RestrictionEnzyme("HaeIII", "GGCC", 2, 2, source=_SOURCE),
|
|
97
|
+
RestrictionEnzyme("HindIII", "AAGCTT", 1, 5, source=_SOURCE),
|
|
98
|
+
RestrictionEnzyme("NotI", "GCGGCCGC", 2, 6, source=_SOURCE),
|
|
99
|
+
RestrictionEnzyme("SmaI", "CCCGGG", 3, 3, source=_SOURCE),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resolve_enzymes(
|
|
105
|
+
enzymes: Iterable[str | RestrictionEnzyme],
|
|
106
|
+
*,
|
|
107
|
+
max_enzymes: int,
|
|
108
|
+
) -> tuple[RestrictionEnzyme, ...]:
|
|
109
|
+
if isinstance(enzymes, (str, bytes)):
|
|
110
|
+
raise ConfigurationError("enzymes must be an iterable of names or definitions.")
|
|
111
|
+
resolved: list[RestrictionEnzyme] = []
|
|
112
|
+
for item in enzymes:
|
|
113
|
+
if len(resolved) >= max_enzymes:
|
|
114
|
+
raise ConfigurationError("Restriction panel exceeds max_enzymes.")
|
|
115
|
+
if isinstance(item, RestrictionEnzyme):
|
|
116
|
+
resolved.append(item)
|
|
117
|
+
elif isinstance(item, str):
|
|
118
|
+
try:
|
|
119
|
+
resolved.append(BUILTIN_RESTRICTION_ENZYMES[item])
|
|
120
|
+
except KeyError as exc:
|
|
121
|
+
raise ConfigurationError(
|
|
122
|
+
"Restriction enzyme is absent from the small built-in catalog.",
|
|
123
|
+
context={"enzyme": item, "available": sorted(BUILTIN_RESTRICTION_ENZYMES)},
|
|
124
|
+
hint=(
|
|
125
|
+
"Pass a RestrictionEnzyme definition or use a future "
|
|
126
|
+
"Biopython/REBASE adapter."
|
|
127
|
+
),
|
|
128
|
+
) from exc
|
|
129
|
+
else:
|
|
130
|
+
raise ConfigurationError("Each enzyme must be a name or RestrictionEnzyme.")
|
|
131
|
+
if not resolved:
|
|
132
|
+
raise ConfigurationError("At least one restriction enzyme is required.")
|
|
133
|
+
names = [enzyme.name for enzyme in resolved]
|
|
134
|
+
if len(names) != len(set(names)):
|
|
135
|
+
raise ConfigurationError("Restriction enzyme names must be unique in one scan.")
|
|
136
|
+
return tuple(resolved)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _location_start(location: object) -> int | None:
|
|
140
|
+
if isinstance(location, Interval):
|
|
141
|
+
return location.start
|
|
142
|
+
if isinstance(location, CompoundLocation):
|
|
143
|
+
return location.parts[0].start
|
|
144
|
+
if isinstance(location, UnresolvedLocation):
|
|
145
|
+
return None
|
|
146
|
+
raise AssertionError("Unexpected restriction-site location.")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def scan_restriction_sites(
|
|
150
|
+
value: SequenceInput,
|
|
151
|
+
enzymes: Iterable[str | RestrictionEnzyme],
|
|
152
|
+
*,
|
|
153
|
+
max_matches: int = 100_000,
|
|
154
|
+
max_scan_length: int = 1_000_000,
|
|
155
|
+
max_scan_cells: int = 50_000_000,
|
|
156
|
+
max_enzymes: int = 10_000,
|
|
157
|
+
) -> PatternResult[RestrictionSiteHit]:
|
|
158
|
+
"""Scan a small built-in or user-provided restriction-enzyme panel.
|
|
159
|
+
|
|
160
|
+
This is not a complete REBASE snapshot. The result records the catalog
|
|
161
|
+
boundary and every user/built-in definition source.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
sequence, sequence_id = resolve_sequence(value)
|
|
165
|
+
validate_positive_int(max_matches, "max_matches")
|
|
166
|
+
validate_positive_int(max_scan_length, "max_scan_length")
|
|
167
|
+
validate_positive_int(max_scan_cells, "max_scan_cells")
|
|
168
|
+
validate_positive_int(max_enzymes, "max_enzymes")
|
|
169
|
+
definitions = _resolve_enzymes(enzymes, max_enzymes=max_enzymes)
|
|
170
|
+
estimated_total_cells = (
|
|
171
|
+
2 * sequence.symbol_length * sum(len(enzyme.recognition_sequence) for enzyme in definitions)
|
|
172
|
+
)
|
|
173
|
+
if estimated_total_cells > max_scan_cells:
|
|
174
|
+
raise ConfigurationError(
|
|
175
|
+
"Restriction panel exceeds max_scan_cells.",
|
|
176
|
+
context={
|
|
177
|
+
"estimated_total_cells": estimated_total_cells,
|
|
178
|
+
"max_scan_cells": max_scan_cells,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
hits: list[RestrictionSiteHit] = []
|
|
182
|
+
truncated = False
|
|
183
|
+
for enzyme in definitions:
|
|
184
|
+
remaining = max_matches - len(hits)
|
|
185
|
+
if remaining <= 0:
|
|
186
|
+
truncated = True
|
|
187
|
+
break
|
|
188
|
+
motif_result = scan_motif(
|
|
189
|
+
sequence,
|
|
190
|
+
enzyme.recognition_sequence,
|
|
191
|
+
mode="iupac",
|
|
192
|
+
name=enzyme.name,
|
|
193
|
+
strand=Strand.BOTH,
|
|
194
|
+
merge_strands=(
|
|
195
|
+
enzyme.recognition_sequence
|
|
196
|
+
== enzyme.recognition_sequence.translate(_COMPLEMENT)[::-1]
|
|
197
|
+
),
|
|
198
|
+
max_matches=remaining,
|
|
199
|
+
max_scan_length=max_scan_length,
|
|
200
|
+
max_scan_cells=max_scan_cells,
|
|
201
|
+
)
|
|
202
|
+
for motif_hit in motif_result.hits:
|
|
203
|
+
symbol_start = _location_start(motif_hit.symbol_location)
|
|
204
|
+
coordinate_start = _location_start(motif_hit.coordinate_location)
|
|
205
|
+
if symbol_start is None:
|
|
206
|
+
raise AssertionError("Symbol locations are always resolved.")
|
|
207
|
+
site_length = len(enzyme.recognition_sequence)
|
|
208
|
+
if motif_hit.strand is Strand.REVERSE:
|
|
209
|
+
top_offset = site_length - enzyme.bottom_cut_offset
|
|
210
|
+
bottom_offset = site_length - enzyme.top_cut_offset
|
|
211
|
+
else:
|
|
212
|
+
top_offset = enzyme.top_cut_offset
|
|
213
|
+
bottom_offset = enzyme.bottom_cut_offset
|
|
214
|
+
top_cut = None if coordinate_start is None else coordinate_start + top_offset
|
|
215
|
+
bottom_cut = None if coordinate_start is None else coordinate_start + bottom_offset
|
|
216
|
+
if sequence.topology.value == "circular":
|
|
217
|
+
span = sequence.coordinate_span
|
|
218
|
+
if span is None:
|
|
219
|
+
raise AssertionError("A scanned circular sequence has a known span.")
|
|
220
|
+
if top_cut is not None:
|
|
221
|
+
top_cut %= span
|
|
222
|
+
if bottom_cut is not None:
|
|
223
|
+
bottom_cut %= span
|
|
224
|
+
hits.append(
|
|
225
|
+
RestrictionSiteHit(
|
|
226
|
+
enzyme=enzyme.name,
|
|
227
|
+
recognition_sequence=motif_hit.matched_sequence,
|
|
228
|
+
strand=motif_hit.strand,
|
|
229
|
+
symbol_location=motif_hit.symbol_location,
|
|
230
|
+
coordinate_location=motif_hit.coordinate_location,
|
|
231
|
+
top_cut=top_cut,
|
|
232
|
+
bottom_cut=bottom_cut,
|
|
233
|
+
wraps_origin=motif_hit.wraps_origin,
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
truncated = truncated or motif_result.truncated
|
|
237
|
+
if truncated:
|
|
238
|
+
break
|
|
239
|
+
result = build_result(
|
|
240
|
+
sequence,
|
|
241
|
+
sequence_id,
|
|
242
|
+
name="restriction_site_scan",
|
|
243
|
+
method="iupac_recognition_and_offset_map",
|
|
244
|
+
algorithm_version="1.0",
|
|
245
|
+
parameters={
|
|
246
|
+
"enzymes": [
|
|
247
|
+
{
|
|
248
|
+
"name": enzyme.name,
|
|
249
|
+
"recognition_sequence": enzyme.recognition_sequence,
|
|
250
|
+
"top_cut_offset": enzyme.top_cut_offset,
|
|
251
|
+
"bottom_cut_offset": enzyme.bottom_cut_offset,
|
|
252
|
+
"source": enzyme.source,
|
|
253
|
+
}
|
|
254
|
+
for enzyme in definitions
|
|
255
|
+
],
|
|
256
|
+
"builtin_catalog_version": RESTRICTION_CATALOG_VERSION,
|
|
257
|
+
"complete_rebase_catalog": False,
|
|
258
|
+
"type_iis_outside_site_supported": False,
|
|
259
|
+
"max_scan_length": max_scan_length,
|
|
260
|
+
"estimated_total_scan_cells": estimated_total_cells,
|
|
261
|
+
"max_scan_cells": max_scan_cells,
|
|
262
|
+
"max_enzymes": max_enzymes,
|
|
263
|
+
},
|
|
264
|
+
hits=hits,
|
|
265
|
+
max_matches=max_matches,
|
|
266
|
+
truncated=truncated,
|
|
267
|
+
provenance=pattern_provenance(
|
|
268
|
+
reimplementation=True,
|
|
269
|
+
reference_name="DNAKit common restriction-enzyme definitions",
|
|
270
|
+
reference_version=RESTRICTION_CATALOG_VERSION,
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
return cast(PatternResult[RestrictionSiteHit], result)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
__all__ = [
|
|
277
|
+
"BUILTIN_RESTRICTION_ENZYMES",
|
|
278
|
+
"RESTRICTION_CATALOG_VERSION",
|
|
279
|
+
"RestrictionEnzyme",
|
|
280
|
+
"scan_restriction_sites",
|
|
281
|
+
]
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Auditable immutable results for sequence-pattern analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Generic, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
from dnakit.core import (
|
|
10
|
+
CompoundLocation,
|
|
11
|
+
DNASequence,
|
|
12
|
+
Interval,
|
|
13
|
+
Location,
|
|
14
|
+
Provenance,
|
|
15
|
+
Strand,
|
|
16
|
+
UnresolvedLocation,
|
|
17
|
+
)
|
|
18
|
+
from dnakit.core._json import FrozenDict, to_json_compatible
|
|
19
|
+
from dnakit.core.issues import Issue
|
|
20
|
+
from dnakit.exceptions import ConfigurationError
|
|
21
|
+
|
|
22
|
+
HitT = TypeVar("HitT")
|
|
23
|
+
_IUPAC = frozenset("ACGTRYSWKMBDHVN")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _require_non_negative(value: int, name: str) -> None:
|
|
27
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
28
|
+
raise ConfigurationError(f"{name} must be a non-negative integer.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _require_positive(value: int, name: str) -> None:
|
|
32
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
33
|
+
raise ConfigurationError(f"{name} must be a positive integer.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _require_text(value: str, name: str) -> None:
|
|
37
|
+
if not isinstance(value, str) or not value.strip():
|
|
38
|
+
raise ConfigurationError(f"{name} must be a non-empty string.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _require_location(value: object, name: str) -> None:
|
|
42
|
+
if not isinstance(value, (Interval, CompoundLocation, UnresolvedLocation)):
|
|
43
|
+
raise ConfigurationError(f"{name} must be an internal Location.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _require_strand(value: object, *, allow_both: bool = False) -> None:
|
|
47
|
+
accepted = (
|
|
48
|
+
(Strand.FORWARD, Strand.REVERSE, Strand.BOTH)
|
|
49
|
+
if allow_both
|
|
50
|
+
else (
|
|
51
|
+
Strand.FORWARD,
|
|
52
|
+
Strand.REVERSE,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
if value not in accepted:
|
|
56
|
+
raise ConfigurationError("Result strand is outside the supported strand values.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _require_iupac(value: str, name: str) -> None:
|
|
60
|
+
_require_text(value, name)
|
|
61
|
+
if set(value) - _IUPAC:
|
|
62
|
+
raise ConfigurationError(f"{name} must contain uppercase DNA IUPAC symbols.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class PatternResult(Generic[HitT]):
|
|
67
|
+
"""A bounded pattern-analysis result with complete algorithm context."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
method: str
|
|
71
|
+
algorithm_version: str
|
|
72
|
+
sequence_id: str | None
|
|
73
|
+
parameters: FrozenDict
|
|
74
|
+
hits: tuple[HitT, ...]
|
|
75
|
+
inspected_symbol_count: int
|
|
76
|
+
gap_count: int
|
|
77
|
+
unknown_gap_count: int
|
|
78
|
+
max_matches: int
|
|
79
|
+
truncated: bool
|
|
80
|
+
coordinate_system: str
|
|
81
|
+
gap_policy: str
|
|
82
|
+
topology: str
|
|
83
|
+
provenance: Provenance
|
|
84
|
+
issues: tuple[Issue, ...]
|
|
85
|
+
|
|
86
|
+
def __post_init__(self) -> None:
|
|
87
|
+
for name in ("name", "method", "algorithm_version", "coordinate_system", "gap_policy"):
|
|
88
|
+
value = getattr(self, name)
|
|
89
|
+
if not isinstance(value, str) or not value.strip():
|
|
90
|
+
raise ConfigurationError(f"PatternResult {name} must be non-empty.")
|
|
91
|
+
if self.sequence_id is not None and (
|
|
92
|
+
not isinstance(self.sequence_id, str) or not self.sequence_id.strip()
|
|
93
|
+
):
|
|
94
|
+
raise ConfigurationError("PatternResult sequence_id must be non-empty or None.")
|
|
95
|
+
for name in ("inspected_symbol_count", "gap_count", "unknown_gap_count"):
|
|
96
|
+
_require_non_negative(getattr(self, name), name)
|
|
97
|
+
if not isinstance(self.parameters, FrozenDict):
|
|
98
|
+
raise ConfigurationError("PatternResult parameters must be FrozenDict.")
|
|
99
|
+
if not isinstance(self.hits, tuple):
|
|
100
|
+
raise ConfigurationError("PatternResult hits must be a tuple.")
|
|
101
|
+
if isinstance(self.max_matches, bool) or not isinstance(self.max_matches, int):
|
|
102
|
+
raise ConfigurationError("max_matches must be a positive integer.")
|
|
103
|
+
if self.max_matches <= 0 or len(self.hits) > self.max_matches:
|
|
104
|
+
raise ConfigurationError("PatternResult hits exceed max_matches.")
|
|
105
|
+
if not isinstance(self.truncated, bool):
|
|
106
|
+
raise ConfigurationError("PatternResult truncated must be boolean.")
|
|
107
|
+
if self.coordinate_system != "0-based-half-open":
|
|
108
|
+
raise ConfigurationError("PatternResult coordinates must be 0-based half-open.")
|
|
109
|
+
if self.topology not in ("linear", "circular"):
|
|
110
|
+
raise ConfigurationError("PatternResult topology must be linear or circular.")
|
|
111
|
+
if not isinstance(self.provenance, Provenance):
|
|
112
|
+
raise ConfigurationError("PatternResult provenance must be Provenance.")
|
|
113
|
+
if any(not isinstance(issue, Issue) for issue in self.issues):
|
|
114
|
+
raise ConfigurationError("PatternResult issues must contain Issue objects.")
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> dict[str, Any]:
|
|
117
|
+
return cast(dict[str, Any], to_json_compatible(self))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True)
|
|
121
|
+
class MotifHit:
|
|
122
|
+
"""One exact, IUPAC, regular-expression, or PWM match."""
|
|
123
|
+
|
|
124
|
+
motif_name: str
|
|
125
|
+
matched_sequence: str
|
|
126
|
+
strand: Strand
|
|
127
|
+
symbol_location: Location
|
|
128
|
+
coordinate_location: Location
|
|
129
|
+
score: float | None
|
|
130
|
+
threshold: float | None
|
|
131
|
+
wraps_origin: bool
|
|
132
|
+
|
|
133
|
+
def __post_init__(self) -> None:
|
|
134
|
+
_require_text(self.motif_name, "motif_name")
|
|
135
|
+
_require_text(self.matched_sequence, "matched_sequence")
|
|
136
|
+
_require_strand(self.strand, allow_both=True)
|
|
137
|
+
_require_location(self.symbol_location, "symbol_location")
|
|
138
|
+
_require_location(self.coordinate_location, "coordinate_location")
|
|
139
|
+
validate_finite_score(self.score, "score")
|
|
140
|
+
validate_finite_score(self.threshold, "threshold")
|
|
141
|
+
if (self.score is None) != (self.threshold is None):
|
|
142
|
+
raise ConfigurationError(
|
|
143
|
+
"MotifHit score and threshold must both be set or both be None."
|
|
144
|
+
)
|
|
145
|
+
if not isinstance(self.wraps_origin, bool):
|
|
146
|
+
raise ConfigurationError("wraps_origin must be boolean.")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class CodonSite:
|
|
151
|
+
"""One in-frame start or stop codon."""
|
|
152
|
+
|
|
153
|
+
kind: str
|
|
154
|
+
codon: str
|
|
155
|
+
strand: Strand
|
|
156
|
+
frame: int
|
|
157
|
+
symbol_location: Location
|
|
158
|
+
coordinate_location: Location
|
|
159
|
+
|
|
160
|
+
def __post_init__(self) -> None:
|
|
161
|
+
if self.kind not in ("start", "stop"):
|
|
162
|
+
raise ConfigurationError("CodonSite kind must be start or stop.")
|
|
163
|
+
if len(self.codon) != 3 or set(self.codon) - set("ACGT"):
|
|
164
|
+
raise ConfigurationError("CodonSite codon must be a canonical DNA triplet.")
|
|
165
|
+
_require_strand(self.strand)
|
|
166
|
+
if self.frame not in (-3, -2, -1, 1, 2, 3):
|
|
167
|
+
raise ConfigurationError("CodonSite frame must be one of +/-1, +/-2, or +/-3.")
|
|
168
|
+
_require_location(self.symbol_location, "symbol_location")
|
|
169
|
+
_require_location(self.coordinate_location, "coordinate_location")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class ORFHit:
|
|
174
|
+
"""One start-anchored open reading frame."""
|
|
175
|
+
|
|
176
|
+
strand: Strand
|
|
177
|
+
frame: int
|
|
178
|
+
symbol_location: Location
|
|
179
|
+
coordinate_location: Location
|
|
180
|
+
nucleotide_length: int
|
|
181
|
+
codon_count: int
|
|
182
|
+
start_codon: str
|
|
183
|
+
stop_codon: str | None
|
|
184
|
+
complete: bool
|
|
185
|
+
translation: str
|
|
186
|
+
|
|
187
|
+
def __post_init__(self) -> None:
|
|
188
|
+
_require_strand(self.strand)
|
|
189
|
+
if self.frame not in (-3, -2, -1, 1, 2, 3):
|
|
190
|
+
raise ConfigurationError("ORFHit frame must be one of +/-1, +/-2, or +/-3.")
|
|
191
|
+
_require_location(self.symbol_location, "symbol_location")
|
|
192
|
+
_require_location(self.coordinate_location, "coordinate_location")
|
|
193
|
+
_require_positive(self.nucleotide_length, "nucleotide_length")
|
|
194
|
+
_require_positive(self.codon_count, "codon_count")
|
|
195
|
+
if self.nucleotide_length != 3 * self.codon_count:
|
|
196
|
+
raise ConfigurationError("ORF nucleotide_length must equal three times codon_count.")
|
|
197
|
+
if len(self.start_codon) != 3 or set(self.start_codon) - set("ACGT"):
|
|
198
|
+
raise ConfigurationError("ORF start_codon must be a canonical DNA triplet.")
|
|
199
|
+
if self.stop_codon is not None and (
|
|
200
|
+
len(self.stop_codon) != 3 or set(self.stop_codon) - set("ACGT")
|
|
201
|
+
):
|
|
202
|
+
raise ConfigurationError("ORF stop_codon must be a canonical triplet or None.")
|
|
203
|
+
if not isinstance(self.complete, bool) or self.complete != (self.stop_codon is not None):
|
|
204
|
+
raise ConfigurationError("ORF complete must agree with stop_codon presence.")
|
|
205
|
+
if not isinstance(self.translation, str) or len(self.translation) != self.codon_count:
|
|
206
|
+
raise ConfigurationError("ORF translation length must equal codon_count.")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass(frozen=True)
|
|
210
|
+
class RestrictionSiteHit:
|
|
211
|
+
"""One recognition site and its top/bottom strand cut coordinates."""
|
|
212
|
+
|
|
213
|
+
enzyme: str
|
|
214
|
+
recognition_sequence: str
|
|
215
|
+
strand: Strand
|
|
216
|
+
symbol_location: Location
|
|
217
|
+
coordinate_location: Location
|
|
218
|
+
top_cut: int | None
|
|
219
|
+
bottom_cut: int | None
|
|
220
|
+
wraps_origin: bool
|
|
221
|
+
|
|
222
|
+
def __post_init__(self) -> None:
|
|
223
|
+
_require_text(self.enzyme, "enzyme")
|
|
224
|
+
_require_iupac(self.recognition_sequence, "recognition_sequence")
|
|
225
|
+
_require_strand(self.strand, allow_both=True)
|
|
226
|
+
_require_location(self.symbol_location, "symbol_location")
|
|
227
|
+
_require_location(self.coordinate_location, "coordinate_location")
|
|
228
|
+
for name in ("top_cut", "bottom_cut"):
|
|
229
|
+
value = getattr(self, name)
|
|
230
|
+
if value is not None:
|
|
231
|
+
_require_non_negative(value, name)
|
|
232
|
+
if not isinstance(self.wraps_origin, bool):
|
|
233
|
+
raise ConfigurationError("wraps_origin must be boolean.")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True)
|
|
237
|
+
class GuideCandidate:
|
|
238
|
+
"""A rule-matched CRISPR guide and adjacent PAM; no efficiency prediction."""
|
|
239
|
+
|
|
240
|
+
nuclease: str
|
|
241
|
+
strand: Strand
|
|
242
|
+
guide_sequence: str
|
|
243
|
+
pam_sequence: str
|
|
244
|
+
guide_symbol_location: Location
|
|
245
|
+
pam_symbol_location: Location
|
|
246
|
+
guide_coordinate_location: Location
|
|
247
|
+
pam_coordinate_location: Location
|
|
248
|
+
gc_fraction: float
|
|
249
|
+
wraps_origin: bool
|
|
250
|
+
|
|
251
|
+
def __post_init__(self) -> None:
|
|
252
|
+
_require_text(self.nuclease, "nuclease")
|
|
253
|
+
_require_strand(self.strand)
|
|
254
|
+
_require_iupac(self.guide_sequence, "guide_sequence")
|
|
255
|
+
_require_iupac(self.pam_sequence, "pam_sequence")
|
|
256
|
+
for name in (
|
|
257
|
+
"guide_symbol_location",
|
|
258
|
+
"pam_symbol_location",
|
|
259
|
+
"guide_coordinate_location",
|
|
260
|
+
"pam_coordinate_location",
|
|
261
|
+
):
|
|
262
|
+
_require_location(getattr(self, name), name)
|
|
263
|
+
if (
|
|
264
|
+
isinstance(self.gc_fraction, bool)
|
|
265
|
+
or not isinstance(self.gc_fraction, (int, float))
|
|
266
|
+
or not math.isfinite(self.gc_fraction)
|
|
267
|
+
or not 0 <= self.gc_fraction <= 1
|
|
268
|
+
):
|
|
269
|
+
raise ConfigurationError("GuideCandidate gc_fraction must be between 0 and 1.")
|
|
270
|
+
if not isinstance(self.wraps_origin, bool):
|
|
271
|
+
raise ConfigurationError("wraps_origin must be boolean.")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass(frozen=True)
|
|
275
|
+
class RegionHit:
|
|
276
|
+
"""One region identified by a deterministic window or composition rule."""
|
|
277
|
+
|
|
278
|
+
kind: str
|
|
279
|
+
symbol_location: Location
|
|
280
|
+
coordinate_location: Location
|
|
281
|
+
length: int
|
|
282
|
+
score: float | None
|
|
283
|
+
attributes: FrozenDict
|
|
284
|
+
|
|
285
|
+
def __post_init__(self) -> None:
|
|
286
|
+
_require_text(self.kind, "kind")
|
|
287
|
+
_require_location(self.symbol_location, "symbol_location")
|
|
288
|
+
_require_location(self.coordinate_location, "coordinate_location")
|
|
289
|
+
_require_positive(self.length, "length")
|
|
290
|
+
validate_finite_score(self.score, "score")
|
|
291
|
+
if not isinstance(self.attributes, FrozenDict):
|
|
292
|
+
raise ConfigurationError("RegionHit attributes must be FrozenDict.")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@dataclass(frozen=True)
|
|
296
|
+
class PalindromeHit:
|
|
297
|
+
"""One reverse-complement palindrome."""
|
|
298
|
+
|
|
299
|
+
sequence: str
|
|
300
|
+
symbol_location: Location
|
|
301
|
+
coordinate_location: Location
|
|
302
|
+
length: int
|
|
303
|
+
|
|
304
|
+
def __post_init__(self) -> None:
|
|
305
|
+
_require_iupac(self.sequence, "sequence")
|
|
306
|
+
_require_location(self.symbol_location, "symbol_location")
|
|
307
|
+
_require_location(self.coordinate_location, "coordinate_location")
|
|
308
|
+
_require_positive(self.length, "length")
|
|
309
|
+
if len(self.sequence) != self.length:
|
|
310
|
+
raise ConfigurationError("Palindrome sequence length must equal length.")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass(frozen=True)
|
|
314
|
+
class InvertedRepeatHit:
|
|
315
|
+
"""Two reverse-complement arms separated by a loop."""
|
|
316
|
+
|
|
317
|
+
left_arm: str
|
|
318
|
+
right_arm: str
|
|
319
|
+
arm_length: int
|
|
320
|
+
loop_length: int
|
|
321
|
+
symbol_location: Location
|
|
322
|
+
coordinate_location: Location
|
|
323
|
+
left_symbol_location: Location
|
|
324
|
+
loop_symbol_location: Location
|
|
325
|
+
right_symbol_location: Location
|
|
326
|
+
|
|
327
|
+
def __post_init__(self) -> None:
|
|
328
|
+
_require_iupac(self.left_arm, "left_arm")
|
|
329
|
+
_require_iupac(self.right_arm, "right_arm")
|
|
330
|
+
_require_positive(self.arm_length, "arm_length")
|
|
331
|
+
_require_non_negative(self.loop_length, "loop_length")
|
|
332
|
+
if len(self.left_arm) != self.arm_length or len(self.right_arm) != self.arm_length:
|
|
333
|
+
raise ConfigurationError("Inverted-repeat arm lengths must equal arm_length.")
|
|
334
|
+
for name in (
|
|
335
|
+
"symbol_location",
|
|
336
|
+
"coordinate_location",
|
|
337
|
+
"left_symbol_location",
|
|
338
|
+
"loop_symbol_location",
|
|
339
|
+
"right_symbol_location",
|
|
340
|
+
):
|
|
341
|
+
_require_location(getattr(self, name), name)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@dataclass(frozen=True)
|
|
345
|
+
class TandemRepeatHit:
|
|
346
|
+
"""One maximal exact tandem repeat using its smallest accepted period."""
|
|
347
|
+
|
|
348
|
+
unit: str
|
|
349
|
+
unit_length: int
|
|
350
|
+
repeat_count: int
|
|
351
|
+
sequence: str
|
|
352
|
+
symbol_location: Location
|
|
353
|
+
coordinate_location: Location
|
|
354
|
+
|
|
355
|
+
def __post_init__(self) -> None:
|
|
356
|
+
_require_iupac(self.unit, "unit")
|
|
357
|
+
_require_positive(self.unit_length, "unit_length")
|
|
358
|
+
_require_positive(self.repeat_count, "repeat_count")
|
|
359
|
+
if self.repeat_count < 2:
|
|
360
|
+
raise ConfigurationError("TandemRepeatHit repeat_count must be at least 2.")
|
|
361
|
+
if len(self.unit) != self.unit_length or self.sequence != self.unit * self.repeat_count:
|
|
362
|
+
raise ConfigurationError(
|
|
363
|
+
"Tandem-repeat sequence must equal unit repeated repeat_count times."
|
|
364
|
+
)
|
|
365
|
+
_require_location(self.symbol_location, "symbol_location")
|
|
366
|
+
_require_location(self.coordinate_location, "coordinate_location")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@dataclass(frozen=True)
|
|
370
|
+
class LowComplexityResult:
|
|
371
|
+
"""Low-complexity regions and a gap-preserving IUPAC masked sequence."""
|
|
372
|
+
|
|
373
|
+
analysis: PatternResult[RegionHit]
|
|
374
|
+
masked_sequence: DNASequence
|
|
375
|
+
mask_symbol: str
|
|
376
|
+
|
|
377
|
+
def __post_init__(self) -> None:
|
|
378
|
+
if not isinstance(self.analysis, PatternResult):
|
|
379
|
+
raise ConfigurationError("LowComplexityResult analysis must be PatternResult.")
|
|
380
|
+
if not isinstance(self.masked_sequence, DNASequence):
|
|
381
|
+
raise ConfigurationError("LowComplexityResult masked_sequence must be DNASequence.")
|
|
382
|
+
if not isinstance(self.mask_symbol, str) or len(self.mask_symbol) != 1:
|
|
383
|
+
raise ConfigurationError("LowComplexityResult mask_symbol must be one character.")
|
|
384
|
+
_require_iupac(self.mask_symbol, "mask_symbol")
|
|
385
|
+
|
|
386
|
+
def to_dict(self) -> dict[str, Any]:
|
|
387
|
+
return cast(dict[str, Any], to_json_compatible(self))
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def validate_finite_score(value: float | None, name: str) -> None:
|
|
391
|
+
"""Validate optional scores used by public pattern definitions."""
|
|
392
|
+
|
|
393
|
+
if value is not None and (
|
|
394
|
+
isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value)
|
|
395
|
+
):
|
|
396
|
+
raise ConfigurationError(f"{name} must be a finite number or None.")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
__all__ = [
|
|
400
|
+
"CodonSite",
|
|
401
|
+
"GuideCandidate",
|
|
402
|
+
"InvertedRepeatHit",
|
|
403
|
+
"LowComplexityResult",
|
|
404
|
+
"MotifHit",
|
|
405
|
+
"ORFHit",
|
|
406
|
+
"PalindromeHit",
|
|
407
|
+
"PatternResult",
|
|
408
|
+
"RegionHit",
|
|
409
|
+
"RestrictionSiteHit",
|
|
410
|
+
"TandemRepeatHit",
|
|
411
|
+
]
|
dnakit/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|