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,667 @@
|
|
|
1
|
+
"""Explicit bounded adapter for caller-installed ``primer3_core``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import stat
|
|
9
|
+
import tempfile
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from dnakit.backends import execute_bounded_command
|
|
14
|
+
from dnakit.core import (
|
|
15
|
+
BackendInfo,
|
|
16
|
+
Citation,
|
|
17
|
+
ExecutionMode,
|
|
18
|
+
ImplementationInfo,
|
|
19
|
+
ImplementationLabel,
|
|
20
|
+
Issue,
|
|
21
|
+
IssueSeverity,
|
|
22
|
+
OriginClass,
|
|
23
|
+
Provenance,
|
|
24
|
+
)
|
|
25
|
+
from dnakit.exceptions import BackendExecutionError, BackendUnavailableError, ConfigurationError
|
|
26
|
+
|
|
27
|
+
from ._shared import freeze_parameters, reverse_complement_text, validate_positive_int
|
|
28
|
+
from .results import PrimerDesignCandidate, PrimerDesignInterfaceResult
|
|
29
|
+
|
|
30
|
+
_BOULDER_KEY = re.compile(r"^[A-Z][A-Z0-9_]{0,255}$")
|
|
31
|
+
_POSITION_KEY = re.compile(r"^PRIMER_(?:LEFT|RIGHT)_\d+$")
|
|
32
|
+
_INTEGER_KEY = re.compile(r"(?:_NUM_RETURNED|_PRODUCT_SIZE)$")
|
|
33
|
+
_FLOAT_KEY = re.compile(r"(?:_TM|_GC_PERCENT|_PENALTY)$")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _explicit_executable(value: str | Path | None) -> tuple[str | None, str | None]:
|
|
37
|
+
if value is None:
|
|
38
|
+
return None, None
|
|
39
|
+
if not isinstance(value, (str, Path)):
|
|
40
|
+
raise ConfigurationError(
|
|
41
|
+
"primer3_core_path must be an explicit filesystem path or None.",
|
|
42
|
+
code="INVALID_PRIMER3_EXECUTABLE",
|
|
43
|
+
)
|
|
44
|
+
requested = str(value)
|
|
45
|
+
if (
|
|
46
|
+
not requested.strip()
|
|
47
|
+
or any(character in requested for character in ("\x00", "\r", "\n"))
|
|
48
|
+
or (not Path(requested).is_absolute() and "/" not in requested and "\\" not in requested)
|
|
49
|
+
):
|
|
50
|
+
raise ConfigurationError(
|
|
51
|
+
"primer3_core_path must be explicit, not a PATH command name.",
|
|
52
|
+
code="INVALID_PRIMER3_EXECUTABLE",
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
resolved = Path(requested).expanduser().resolve(strict=True)
|
|
56
|
+
details = resolved.stat()
|
|
57
|
+
except (OSError, RuntimeError, ValueError):
|
|
58
|
+
return requested, None
|
|
59
|
+
if not stat.S_ISREG(details.st_mode) or not os.access(resolved, os.X_OK):
|
|
60
|
+
return requested, None
|
|
61
|
+
return requested, str(resolved)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _explicit_parameter_directory(value: str | Path | None) -> tuple[str | None, str | None]:
|
|
65
|
+
if value is None:
|
|
66
|
+
return None, None
|
|
67
|
+
if not isinstance(value, (str, Path)) or not str(value).strip():
|
|
68
|
+
raise ConfigurationError(
|
|
69
|
+
"thermodynamic_parameters_path must be an explicit directory path or None.",
|
|
70
|
+
code="INVALID_PRIMER3_PARAMETER_PATH",
|
|
71
|
+
)
|
|
72
|
+
requested = str(value)
|
|
73
|
+
try:
|
|
74
|
+
resolved = Path(requested).expanduser().resolve(strict=True)
|
|
75
|
+
except (OSError, RuntimeError, ValueError):
|
|
76
|
+
return requested, None
|
|
77
|
+
return requested, str(resolved) if resolved.is_dir() else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _provenance(info: BackendInfo) -> Provenance:
|
|
81
|
+
return Provenance(
|
|
82
|
+
implementation=ImplementationInfo(
|
|
83
|
+
label=ImplementationLabel.ADAPTER,
|
|
84
|
+
execution_mode=ExecutionMode.EXTERNAL,
|
|
85
|
+
origin_class=OriginClass.INTEGRATION,
|
|
86
|
+
license_expression=info.license_expression,
|
|
87
|
+
citations=(
|
|
88
|
+
Citation(
|
|
89
|
+
"primer3",
|
|
90
|
+
title="Primer3--new capabilities and interfaces",
|
|
91
|
+
doi="10.1093/nar/gks596",
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
backend=info,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _bounded_text(value: object, name: str, *, max_length: int) -> str:
|
|
100
|
+
if not isinstance(value, str) or len(value) > max_length:
|
|
101
|
+
raise BackendExecutionError(
|
|
102
|
+
f"Primer3 output {name} must be bounded text.",
|
|
103
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
104
|
+
context={"field": name},
|
|
105
|
+
)
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _output_int(value: object, name: str, *, minimum: int = 0) -> int:
|
|
110
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
|
|
111
|
+
raise BackendExecutionError(
|
|
112
|
+
f"Primer3 output {name} must be an integer >= {minimum}.",
|
|
113
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
114
|
+
context={"field": name},
|
|
115
|
+
)
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _output_float(value: object, name: str) -> float:
|
|
120
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
121
|
+
raise BackendExecutionError(
|
|
122
|
+
f"Primer3 output {name} must be finite numeric data.",
|
|
123
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
124
|
+
context={"field": name},
|
|
125
|
+
)
|
|
126
|
+
return float(value)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _position_length(value: object, name: str) -> tuple[int, int]:
|
|
130
|
+
if not isinstance(value, (tuple, list)) or len(value) != 2:
|
|
131
|
+
raise BackendExecutionError(
|
|
132
|
+
f"Primer3 output {name} must contain position and length.",
|
|
133
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
134
|
+
context={"field": name},
|
|
135
|
+
)
|
|
136
|
+
return (
|
|
137
|
+
_output_int(value[0], f"{name}[0]"),
|
|
138
|
+
_output_int(value[1], f"{name}[1]", minimum=1),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _condition_number(conditions: Mapping[str, object], name: str) -> float:
|
|
143
|
+
value = conditions[name]
|
|
144
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
|
145
|
+
raise ConfigurationError(f"Primer design condition {name} must be numeric.")
|
|
146
|
+
return float(value)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _boulder_value(key: str, value: object) -> str:
|
|
150
|
+
if key == "SEQUENCE_TARGET":
|
|
151
|
+
if not isinstance(value, list) or len(value) != 2:
|
|
152
|
+
raise AssertionError("SEQUENCE_TARGET must contain start and length.")
|
|
153
|
+
text = f"{value[0]},{value[1]}"
|
|
154
|
+
elif key == "SEQUENCE_EXCLUDED_REGION":
|
|
155
|
+
if not isinstance(value, list):
|
|
156
|
+
raise AssertionError("SEQUENCE_EXCLUDED_REGION must be a list.")
|
|
157
|
+
text = " ".join(f"{item[0]},{item[1]}" for item in value)
|
|
158
|
+
elif key == "PRIMER_PRODUCT_SIZE_RANGE":
|
|
159
|
+
if not isinstance(value, list) or len(value) != 1:
|
|
160
|
+
raise AssertionError("PRIMER_PRODUCT_SIZE_RANGE must contain one interval.")
|
|
161
|
+
text = f"{value[0][0]}-{value[0][1]}"
|
|
162
|
+
elif isinstance(value, bool):
|
|
163
|
+
text = "1" if value else "0"
|
|
164
|
+
elif isinstance(value, (int, float, str)):
|
|
165
|
+
text = str(value)
|
|
166
|
+
else:
|
|
167
|
+
raise ConfigurationError(
|
|
168
|
+
"Primer3 request contains an unsupported Boulder-IO value.",
|
|
169
|
+
code="INVALID_PRIMER3_DESIGN_INPUT",
|
|
170
|
+
context={"key": key, "value_type": type(value).__name__},
|
|
171
|
+
)
|
|
172
|
+
if any(character in text for character in ("\x00", "\r", "\n")):
|
|
173
|
+
raise ConfigurationError(
|
|
174
|
+
"Primer3 Boulder-IO values cannot contain line breaks or NUL.",
|
|
175
|
+
code="INVALID_PRIMER3_DESIGN_INPUT",
|
|
176
|
+
context={"key": key},
|
|
177
|
+
)
|
|
178
|
+
return text
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _boulder_record(sequence_args: Mapping[str, object], global_args: Mapping[str, object]) -> str:
|
|
182
|
+
lines = [
|
|
183
|
+
f"{key}={_boulder_value(key, value)}"
|
|
184
|
+
for arguments in (sequence_args, global_args)
|
|
185
|
+
for key, value in arguments.items()
|
|
186
|
+
]
|
|
187
|
+
return "\n".join((*lines, "=", ""))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _parse_boulder_value(key: str, value: str) -> object:
|
|
191
|
+
try:
|
|
192
|
+
if _POSITION_KEY.fullmatch(key):
|
|
193
|
+
parts = value.split(",")
|
|
194
|
+
if len(parts) != 2:
|
|
195
|
+
raise ValueError
|
|
196
|
+
return [int(parts[0]), int(parts[1])]
|
|
197
|
+
if _INTEGER_KEY.search(key):
|
|
198
|
+
return int(value)
|
|
199
|
+
if _FLOAT_KEY.search(key):
|
|
200
|
+
parsed = float(value)
|
|
201
|
+
if not math.isfinite(parsed):
|
|
202
|
+
raise ValueError
|
|
203
|
+
return parsed
|
|
204
|
+
except ValueError as exc:
|
|
205
|
+
raise BackendExecutionError(
|
|
206
|
+
"primer3_core returned malformed numeric Boulder-IO output.",
|
|
207
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
208
|
+
context={"key": key},
|
|
209
|
+
) from exc
|
|
210
|
+
return value
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _parse_boulder_output(
|
|
214
|
+
text: str, *, max_result_keys: int, max_text_length: int
|
|
215
|
+
) -> dict[str, object]:
|
|
216
|
+
result: dict[str, object] = {}
|
|
217
|
+
terminated = False
|
|
218
|
+
lines = text.splitlines()
|
|
219
|
+
for index, line in enumerate(lines):
|
|
220
|
+
if line == "=":
|
|
221
|
+
terminated = True
|
|
222
|
+
if any(trailing.strip() for trailing in lines[index + 1 :]):
|
|
223
|
+
raise BackendExecutionError(
|
|
224
|
+
"primer3_core returned data after its Boulder-IO record terminator.",
|
|
225
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
226
|
+
)
|
|
227
|
+
break
|
|
228
|
+
if "=" not in line:
|
|
229
|
+
raise BackendExecutionError(
|
|
230
|
+
"primer3_core returned malformed Boulder-IO output.",
|
|
231
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
232
|
+
)
|
|
233
|
+
key, value = line.split("=", 1)
|
|
234
|
+
if not _BOULDER_KEY.fullmatch(key) or key in result:
|
|
235
|
+
raise BackendExecutionError(
|
|
236
|
+
"primer3_core returned an invalid or duplicate Boulder-IO key.",
|
|
237
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
238
|
+
context={"key": key[:256]},
|
|
239
|
+
)
|
|
240
|
+
if len(value) > max_text_length:
|
|
241
|
+
raise BackendExecutionError(
|
|
242
|
+
"primer3_core returned an oversized Boulder-IO value.",
|
|
243
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
244
|
+
context={"key": key},
|
|
245
|
+
)
|
|
246
|
+
result[key] = _parse_boulder_value(key, value)
|
|
247
|
+
if len(result) > max_result_keys:
|
|
248
|
+
raise BackendExecutionError(
|
|
249
|
+
"primer3_core output exceeded max_result_keys.",
|
|
250
|
+
code="PRIMER3_DESIGN_OUTPUT_LIMIT_EXCEEDED",
|
|
251
|
+
)
|
|
252
|
+
if not terminated:
|
|
253
|
+
raise BackendExecutionError(
|
|
254
|
+
"primer3_core output did not terminate its Boulder-IO record.",
|
|
255
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
256
|
+
)
|
|
257
|
+
return result
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _read_bounded(path: Path, max_bytes: int) -> str:
|
|
261
|
+
try:
|
|
262
|
+
if not path.is_file():
|
|
263
|
+
raise OSError
|
|
264
|
+
with path.open("rb") as handle:
|
|
265
|
+
payload = handle.read(max_bytes + 1)
|
|
266
|
+
if len(payload) > max_bytes:
|
|
267
|
+
raise OSError
|
|
268
|
+
return payload.decode("utf-8")
|
|
269
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
270
|
+
raise BackendExecutionError(
|
|
271
|
+
"primer3_core output artifact is missing, oversized, or invalid UTF-8.",
|
|
272
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
273
|
+
context={"artifact": path.name, "max_bytes": max_bytes},
|
|
274
|
+
) from exc
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class Primer3CLIDesignAdapter:
|
|
278
|
+
"""Run primer design through an explicit user-installed ``primer3_core`` path."""
|
|
279
|
+
|
|
280
|
+
def __init__(
|
|
281
|
+
self,
|
|
282
|
+
primer3_core_path: str | Path | None = None,
|
|
283
|
+
*,
|
|
284
|
+
thermodynamic_parameters_path: str | Path | None = None,
|
|
285
|
+
timeout_seconds: float = 60.0,
|
|
286
|
+
max_output_bytes: int = 10_000_000,
|
|
287
|
+
) -> None:
|
|
288
|
+
self._requested_executable, self._path = _explicit_executable(primer3_core_path)
|
|
289
|
+
self._requested_parameters, self._parameters_path = _explicit_parameter_directory(
|
|
290
|
+
thermodynamic_parameters_path
|
|
291
|
+
)
|
|
292
|
+
if (
|
|
293
|
+
isinstance(timeout_seconds, bool)
|
|
294
|
+
or not isinstance(timeout_seconds, (int, float))
|
|
295
|
+
or not 0 < timeout_seconds <= 300
|
|
296
|
+
):
|
|
297
|
+
raise ConfigurationError("timeout_seconds must be in (0, 300].")
|
|
298
|
+
if (
|
|
299
|
+
isinstance(max_output_bytes, bool)
|
|
300
|
+
or not isinstance(max_output_bytes, int)
|
|
301
|
+
or not 1 <= max_output_bytes <= 100_000_000
|
|
302
|
+
):
|
|
303
|
+
raise ConfigurationError("max_output_bytes must be in [1, 100000000].")
|
|
304
|
+
self._timeout_seconds = float(timeout_seconds)
|
|
305
|
+
self._max_output_bytes = max_output_bytes
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def info(self) -> BackendInfo:
|
|
309
|
+
parameters_valid = self._requested_parameters is None or self._parameters_path is not None
|
|
310
|
+
available = self._path is not None and parameters_valid
|
|
311
|
+
return BackendInfo(
|
|
312
|
+
"primer3-cli",
|
|
313
|
+
executable_path=self._path,
|
|
314
|
+
license_expression="GPL-2.0-or-later",
|
|
315
|
+
capabilities=("primer_design",) if available else (),
|
|
316
|
+
available=available,
|
|
317
|
+
metadata={
|
|
318
|
+
"adapter_status": "explicit-cli-execution",
|
|
319
|
+
"requested_primer3_core_path": self._requested_executable,
|
|
320
|
+
"resolved_primer3_core_path": self._path,
|
|
321
|
+
"requested_thermodynamic_parameters_path": self._requested_parameters,
|
|
322
|
+
"resolved_thermodynamic_parameters_path": self._parameters_path,
|
|
323
|
+
"probe_mode": "explicit-path-stat-only",
|
|
324
|
+
"automatic_path_search": False,
|
|
325
|
+
"automatic_install": False,
|
|
326
|
+
"automatic_download": False,
|
|
327
|
+
"redistributed": False,
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def ensure_available(self) -> None:
|
|
332
|
+
if self._requested_parameters is not None and self._parameters_path is None:
|
|
333
|
+
raise BackendUnavailableError(
|
|
334
|
+
"The explicit Primer3 thermodynamic parameter directory is unavailable.",
|
|
335
|
+
code="PRIMER3_PARAMETER_PATH_UNAVAILABLE",
|
|
336
|
+
context={"requested_path": self._requested_parameters},
|
|
337
|
+
)
|
|
338
|
+
if self._path is None:
|
|
339
|
+
raise BackendUnavailableError(
|
|
340
|
+
"The explicit primer3_core executable is unavailable.",
|
|
341
|
+
code="PRIMER3_CLI_UNAVAILABLE",
|
|
342
|
+
context={"requested_executable": self._requested_executable},
|
|
343
|
+
hint="Install Primer3 separately and pass primer3_core_path explicitly.",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def _arguments(
|
|
348
|
+
result: PrimerDesignInterfaceResult,
|
|
349
|
+
) -> tuple[dict[str, object], dict[str, object]]:
|
|
350
|
+
request = result.request
|
|
351
|
+
conditions = request.thermodynamic_conditions
|
|
352
|
+
primer_min, primer_max = request.primer_length_range
|
|
353
|
+
tm_min, tm_max = request.tm_range_celsius
|
|
354
|
+
gc_min, gc_max = request.gc_range
|
|
355
|
+
product_min, product_max = request.product_length_range
|
|
356
|
+
sequence_args: dict[str, object] = {
|
|
357
|
+
"SEQUENCE_ID": "dnakit-template",
|
|
358
|
+
"SEQUENCE_TEMPLATE": request.template.symbols,
|
|
359
|
+
"SEQUENCE_TARGET": [request.target_start, request.target_end - request.target_start],
|
|
360
|
+
"SEQUENCE_EXCLUDED_REGION": [
|
|
361
|
+
[start, end - start] for start, end in request.excluded_regions
|
|
362
|
+
],
|
|
363
|
+
}
|
|
364
|
+
global_args: dict[str, object] = {
|
|
365
|
+
"PRIMER_TASK": "generic",
|
|
366
|
+
"PRIMER_PICK_LEFT_PRIMER": 1,
|
|
367
|
+
"PRIMER_PICK_INTERNAL_OLIGO": 0,
|
|
368
|
+
"PRIMER_PICK_RIGHT_PRIMER": 1,
|
|
369
|
+
"PRIMER_NUM_RETURN": request.candidate_count,
|
|
370
|
+
"PRIMER_OPT_SIZE": (primer_min + primer_max) // 2,
|
|
371
|
+
"PRIMER_MIN_SIZE": primer_min,
|
|
372
|
+
"PRIMER_MAX_SIZE": primer_max,
|
|
373
|
+
"PRIMER_OPT_TM": (tm_min + tm_max) / 2.0,
|
|
374
|
+
"PRIMER_MIN_TM": tm_min,
|
|
375
|
+
"PRIMER_MAX_TM": tm_max,
|
|
376
|
+
"PRIMER_MIN_GC": gc_min * 100.0,
|
|
377
|
+
"PRIMER_MAX_GC": gc_max * 100.0,
|
|
378
|
+
"PRIMER_PRODUCT_SIZE_RANGE": [[product_min, product_max]],
|
|
379
|
+
"PRIMER_SALT_MONOVALENT": (
|
|
380
|
+
_condition_number(conditions, "sodium_molar")
|
|
381
|
+
+ _condition_number(conditions, "potassium_molar")
|
|
382
|
+
)
|
|
383
|
+
* 1_000.0,
|
|
384
|
+
"PRIMER_SALT_DIVALENT": _condition_number(conditions, "magnesium_molar") * 1_000.0,
|
|
385
|
+
"PRIMER_DNTP_CONC": _condition_number(conditions, "dntp_molar") * 1_000.0,
|
|
386
|
+
"PRIMER_DNA_CONC": _condition_number(conditions, "strand_concentration_molar")
|
|
387
|
+
* 1_000_000_000.0,
|
|
388
|
+
"PRIMER_DMSO_CONC": _condition_number(conditions, "dmso_percent"),
|
|
389
|
+
"PRIMER_DMSO_FACTOR": _condition_number(conditions, "dmso_factor_celsius_per_percent"),
|
|
390
|
+
"PRIMER_FORMAMIDE_CONC": _condition_number(conditions, "formamide_molar"),
|
|
391
|
+
}
|
|
392
|
+
return sequence_args, global_args
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def _candidate(
|
|
396
|
+
raw: Mapping[str, object],
|
|
397
|
+
*,
|
|
398
|
+
rank: int,
|
|
399
|
+
template: str,
|
|
400
|
+
max_text_length: int,
|
|
401
|
+
) -> PrimerDesignCandidate:
|
|
402
|
+
template_length = len(template)
|
|
403
|
+
prefix = f"PRIMER_PAIR_{rank}"
|
|
404
|
+
left_position, left_length = _position_length(
|
|
405
|
+
raw.get(f"PRIMER_LEFT_{rank}"), f"left_{rank}"
|
|
406
|
+
)
|
|
407
|
+
right_anchor, right_length = _position_length(
|
|
408
|
+
raw.get(f"PRIMER_RIGHT_{rank}"), f"right_{rank}"
|
|
409
|
+
)
|
|
410
|
+
left_start = left_position
|
|
411
|
+
left_end = left_start + left_length
|
|
412
|
+
right_start = right_anchor - right_length + 1
|
|
413
|
+
right_end = right_anchor + 1
|
|
414
|
+
if not 0 <= left_start < left_end <= right_start < right_end <= template_length:
|
|
415
|
+
raise BackendExecutionError(
|
|
416
|
+
"Primer3 returned candidate coordinates outside the template.",
|
|
417
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
418
|
+
context={"candidate_rank": rank},
|
|
419
|
+
)
|
|
420
|
+
left_sequence = _bounded_text(
|
|
421
|
+
raw.get(f"PRIMER_LEFT_{rank}_SEQUENCE"),
|
|
422
|
+
f"left_sequence_{rank}",
|
|
423
|
+
max_length=max_text_length,
|
|
424
|
+
).upper()
|
|
425
|
+
right_sequence = _bounded_text(
|
|
426
|
+
raw.get(f"PRIMER_RIGHT_{rank}_SEQUENCE"),
|
|
427
|
+
f"right_sequence_{rank}",
|
|
428
|
+
max_length=max_text_length,
|
|
429
|
+
).upper()
|
|
430
|
+
expected_left = template[left_start:left_end]
|
|
431
|
+
expected_right = reverse_complement_text(template[right_start:right_end])
|
|
432
|
+
if (
|
|
433
|
+
len(left_sequence) != left_length
|
|
434
|
+
or len(right_sequence) != right_length
|
|
435
|
+
or left_sequence != expected_left
|
|
436
|
+
or right_sequence != expected_right
|
|
437
|
+
):
|
|
438
|
+
raise BackendExecutionError(
|
|
439
|
+
"Primer3 returned candidate sequences inconsistent with template coordinates.",
|
|
440
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
441
|
+
context={"candidate_rank": rank},
|
|
442
|
+
)
|
|
443
|
+
penalty_value = raw.get(f"{prefix}_PENALTY")
|
|
444
|
+
try:
|
|
445
|
+
product_size = _output_int(raw.get(f"{prefix}_PRODUCT_SIZE"), "product_size", minimum=1)
|
|
446
|
+
if product_size != right_end - left_start:
|
|
447
|
+
raise ConfigurationError(
|
|
448
|
+
"Primer3 product size does not match candidate coordinates."
|
|
449
|
+
)
|
|
450
|
+
return PrimerDesignCandidate(
|
|
451
|
+
rank=rank,
|
|
452
|
+
left_primer_sequence=left_sequence,
|
|
453
|
+
right_primer_sequence=right_sequence,
|
|
454
|
+
left_start=left_start,
|
|
455
|
+
left_end=left_end,
|
|
456
|
+
right_start=right_start,
|
|
457
|
+
right_end=right_end,
|
|
458
|
+
product_size=product_size,
|
|
459
|
+
left_tm_celsius=_output_float(raw.get(f"PRIMER_LEFT_{rank}_TM"), f"left_tm_{rank}"),
|
|
460
|
+
right_tm_celsius=_output_float(
|
|
461
|
+
raw.get(f"PRIMER_RIGHT_{rank}_TM"), f"right_tm_{rank}"
|
|
462
|
+
),
|
|
463
|
+
left_gc_fraction=_output_float(
|
|
464
|
+
raw.get(f"PRIMER_LEFT_{rank}_GC_PERCENT"), f"left_gc_{rank}"
|
|
465
|
+
)
|
|
466
|
+
/ 100.0,
|
|
467
|
+
right_gc_fraction=_output_float(
|
|
468
|
+
raw.get(f"PRIMER_RIGHT_{rank}_GC_PERCENT"), f"right_gc_{rank}"
|
|
469
|
+
)
|
|
470
|
+
/ 100.0,
|
|
471
|
+
pair_penalty=(
|
|
472
|
+
None
|
|
473
|
+
if penalty_value is None
|
|
474
|
+
else _output_float(penalty_value, f"pair_penalty_{rank}")
|
|
475
|
+
),
|
|
476
|
+
)
|
|
477
|
+
except ConfigurationError as exc:
|
|
478
|
+
raise BackendExecutionError(
|
|
479
|
+
"Primer3 returned an internally inconsistent candidate.",
|
|
480
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
481
|
+
context={"candidate_rank": rank},
|
|
482
|
+
) from exc
|
|
483
|
+
|
|
484
|
+
def _execute(
|
|
485
|
+
self,
|
|
486
|
+
record: str,
|
|
487
|
+
*,
|
|
488
|
+
max_result_keys: int,
|
|
489
|
+
max_text_length: int,
|
|
490
|
+
) -> dict[str, object]:
|
|
491
|
+
self.ensure_available()
|
|
492
|
+
assert self._path is not None
|
|
493
|
+
with tempfile.TemporaryDirectory(prefix="dnakit-primer3-design-") as workspace_text:
|
|
494
|
+
workspace = Path(workspace_text)
|
|
495
|
+
input_path = workspace / "input.boulder"
|
|
496
|
+
output_path = workspace / "output.boulder"
|
|
497
|
+
error_path = workspace / "error.txt"
|
|
498
|
+
try:
|
|
499
|
+
with input_path.open("x", encoding="utf-8", newline="\n") as handle:
|
|
500
|
+
handle.write(record)
|
|
501
|
+
handle.flush()
|
|
502
|
+
os.fsync(handle.fileno())
|
|
503
|
+
except OSError as exc:
|
|
504
|
+
raise BackendExecutionError(
|
|
505
|
+
"DNAKit could not stage the primer3_core input.",
|
|
506
|
+
code="PRIMER3_DESIGN_INPUT_STAGING_FAILED",
|
|
507
|
+
) from exc
|
|
508
|
+
arguments = (
|
|
509
|
+
"--strict_tags",
|
|
510
|
+
"--io_version=4",
|
|
511
|
+
"--output=output.boulder",
|
|
512
|
+
"--error=error.txt",
|
|
513
|
+
"input.boulder",
|
|
514
|
+
)
|
|
515
|
+
result = execute_bounded_command(
|
|
516
|
+
self._path,
|
|
517
|
+
arguments,
|
|
518
|
+
backend_id="primer3-cli",
|
|
519
|
+
cwd=workspace,
|
|
520
|
+
timeout_seconds=self._timeout_seconds,
|
|
521
|
+
max_output_bytes=min(self._max_output_bytes, 1_000_000),
|
|
522
|
+
monitored_output_paths=(output_path, error_path),
|
|
523
|
+
max_monitored_output_bytes=self._max_output_bytes,
|
|
524
|
+
)
|
|
525
|
+
error_text = ""
|
|
526
|
+
if error_path.exists():
|
|
527
|
+
error_text = _read_bounded(error_path, self._max_output_bytes)
|
|
528
|
+
if result.return_code != 0:
|
|
529
|
+
raise BackendExecutionError(
|
|
530
|
+
"primer3_core primer design failed.",
|
|
531
|
+
code="PRIMER3_CLI_EXECUTION_FAILED",
|
|
532
|
+
context={
|
|
533
|
+
"return_code": result.return_code,
|
|
534
|
+
"process_output_excerpt": result.output[:1_000],
|
|
535
|
+
"error_excerpt": error_text[:1_000],
|
|
536
|
+
},
|
|
537
|
+
)
|
|
538
|
+
output_text = _read_bounded(output_path, self._max_output_bytes)
|
|
539
|
+
return _parse_boulder_output(
|
|
540
|
+
output_text,
|
|
541
|
+
max_result_keys=max_result_keys,
|
|
542
|
+
max_text_length=max_text_length,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def design(
|
|
546
|
+
self,
|
|
547
|
+
prepared: PrimerDesignInterfaceResult,
|
|
548
|
+
*,
|
|
549
|
+
max_returned_candidates: int = 10_000,
|
|
550
|
+
max_result_keys: int = 100_000,
|
|
551
|
+
max_text_length: int = 100_000,
|
|
552
|
+
max_template_length: int = 1_000_000,
|
|
553
|
+
) -> PrimerDesignInterfaceResult:
|
|
554
|
+
"""Execute one prepared request; no discovery or execution happens earlier."""
|
|
555
|
+
|
|
556
|
+
if not isinstance(prepared, PrimerDesignInterfaceResult):
|
|
557
|
+
raise ConfigurationError("prepared must be a PrimerDesignInterfaceResult.")
|
|
558
|
+
if prepared.execution_performed:
|
|
559
|
+
raise ConfigurationError("A completed primer-design result cannot be executed again.")
|
|
560
|
+
validate_positive_int(max_returned_candidates, "max_returned_candidates", maximum=10_000)
|
|
561
|
+
validate_positive_int(max_result_keys, "max_result_keys", maximum=1_000_000)
|
|
562
|
+
validate_positive_int(max_text_length, "max_text_length", maximum=1_000_000)
|
|
563
|
+
validate_positive_int(
|
|
564
|
+
max_template_length,
|
|
565
|
+
"max_template_length",
|
|
566
|
+
maximum=10_000_000,
|
|
567
|
+
)
|
|
568
|
+
template_length = prepared.request.template.symbol_length
|
|
569
|
+
if template_length > max_template_length:
|
|
570
|
+
raise ConfigurationError(
|
|
571
|
+
"Primer design template exceeds max_template_length.",
|
|
572
|
+
code="PRIMER3_DESIGN_TEMPLATE_LIMIT_EXCEEDED",
|
|
573
|
+
context={
|
|
574
|
+
"template_length": template_length,
|
|
575
|
+
"max_template_length": max_template_length,
|
|
576
|
+
},
|
|
577
|
+
)
|
|
578
|
+
output_limit = min(prepared.request.candidate_count, max_returned_candidates)
|
|
579
|
+
sequence_args, global_args = self._arguments(prepared)
|
|
580
|
+
global_args["PRIMER_NUM_RETURN"] = output_limit
|
|
581
|
+
if self._parameters_path is not None:
|
|
582
|
+
global_args["PRIMER_THERMODYNAMIC_PARAMETERS_PATH"] = self._parameters_path + os.sep
|
|
583
|
+
raw = self._execute(
|
|
584
|
+
_boulder_record(sequence_args, global_args),
|
|
585
|
+
max_result_keys=max_result_keys,
|
|
586
|
+
max_text_length=max_text_length,
|
|
587
|
+
)
|
|
588
|
+
if len(raw) > max_result_keys or any(
|
|
589
|
+
not isinstance(key, str) or not key or len(key) > 256 for key in raw
|
|
590
|
+
):
|
|
591
|
+
raise BackendExecutionError(
|
|
592
|
+
"primer3_core returned an invalid or oversized result mapping.",
|
|
593
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
594
|
+
)
|
|
595
|
+
if any(isinstance(value, str) and len(value) > max_text_length for value in raw.values()):
|
|
596
|
+
raise BackendExecutionError(
|
|
597
|
+
"primer3_core returned oversized text output.",
|
|
598
|
+
code="INVALID_PRIMER3_DESIGN_OUTPUT",
|
|
599
|
+
)
|
|
600
|
+
backend_error = _bounded_text(
|
|
601
|
+
raw.get("PRIMER_ERROR", ""), "PRIMER_ERROR", max_length=max_text_length
|
|
602
|
+
)
|
|
603
|
+
if backend_error:
|
|
604
|
+
raise BackendExecutionError(
|
|
605
|
+
"primer3_core rejected the primer-design request.",
|
|
606
|
+
code="PRIMER3_CLI_DESIGN_REJECTED",
|
|
607
|
+
context={"backend_error": backend_error},
|
|
608
|
+
)
|
|
609
|
+
returned = _output_int(raw.get("PRIMER_PAIR_NUM_RETURNED", 0), "pair_count")
|
|
610
|
+
if returned > output_limit:
|
|
611
|
+
raise BackendExecutionError(
|
|
612
|
+
"primer3_core returned more candidates than the configured limit.",
|
|
613
|
+
code="PRIMER3_DESIGN_OUTPUT_LIMIT_EXCEEDED",
|
|
614
|
+
context={"returned": returned, "output_limit": output_limit},
|
|
615
|
+
)
|
|
616
|
+
candidates = tuple(
|
|
617
|
+
self._candidate(
|
|
618
|
+
raw,
|
|
619
|
+
rank=rank,
|
|
620
|
+
template=prepared.request.template.symbols,
|
|
621
|
+
max_text_length=max_text_length,
|
|
622
|
+
)
|
|
623
|
+
for rank in range(returned)
|
|
624
|
+
)
|
|
625
|
+
warning = _bounded_text(
|
|
626
|
+
raw.get("PRIMER_WARNING", ""), "PRIMER_WARNING", max_length=max_text_length
|
|
627
|
+
)
|
|
628
|
+
issues = (
|
|
629
|
+
(Issue("PRIMER3_DESIGN_WARNING", IssueSeverity.WARNING, warning),) if warning else ()
|
|
630
|
+
)
|
|
631
|
+
info = self.info
|
|
632
|
+
return PrimerDesignInterfaceResult(
|
|
633
|
+
request=prepared.request,
|
|
634
|
+
backend_name=info.name,
|
|
635
|
+
status=(
|
|
636
|
+
"execution-complete-with-candidates"
|
|
637
|
+
if candidates
|
|
638
|
+
else "execution-complete-no-candidates"
|
|
639
|
+
),
|
|
640
|
+
execution_performed=True,
|
|
641
|
+
candidates=candidates,
|
|
642
|
+
reason="The user explicitly invoked a separately installed primer3_core executable.",
|
|
643
|
+
method="primer3-cli-core-design-adapter",
|
|
644
|
+
algorithm_version="dnakit-primer3-cli-design-adapter-v1",
|
|
645
|
+
parameters=freeze_parameters(
|
|
646
|
+
{
|
|
647
|
+
"explicit_execution": True,
|
|
648
|
+
"automatic_path_search": False,
|
|
649
|
+
"automatic_install": False,
|
|
650
|
+
"automatic_probe": False,
|
|
651
|
+
"automatic_execution": False,
|
|
652
|
+
"requested_candidates": prepared.request.candidate_count,
|
|
653
|
+
"returned_candidates": len(candidates),
|
|
654
|
+
"max_returned_candidates": max_returned_candidates,
|
|
655
|
+
"max_result_keys": max_result_keys,
|
|
656
|
+
"max_text_length": max_text_length,
|
|
657
|
+
"max_template_length": max_template_length,
|
|
658
|
+
"primer3_core_path": self._path,
|
|
659
|
+
"thermodynamic_parameters_path": self._parameters_path,
|
|
660
|
+
}
|
|
661
|
+
),
|
|
662
|
+
provenance=_provenance(info),
|
|
663
|
+
issues=issues,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
__all__ = ["Primer3CLIDesignAdapter"]
|