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.
Files changed (210) hide show
  1. dnakit/__init__.py +108 -0
  2. dnakit/_version.py +5 -0
  3. dnakit/alignment/__init__.py +6 -0
  4. dnakit/alignment/pairwise.py +512 -0
  5. dnakit/alignment/results.py +188 -0
  6. dnakit/backends/__init__.py +80 -0
  7. dnakit/backends/external.py +517 -0
  8. dnakit/backends/registry.py +207 -0
  9. dnakit/batch.py +452 -0
  10. dnakit/cache/__init__.py +5 -0
  11. dnakit/cache/store.py +424 -0
  12. dnakit/chunking.py +920 -0
  13. dnakit/cli/__init__.py +5 -0
  14. dnakit/cli/__main__.py +6 -0
  15. dnakit/cli/app.py +814 -0
  16. dnakit/cli/workflow.py +94 -0
  17. dnakit/config/__init__.py +5 -0
  18. dnakit/config/loader.py +381 -0
  19. dnakit/core/__init__.py +80 -0
  20. dnakit/core/_json.py +206 -0
  21. dnakit/core/backend_info.py +70 -0
  22. dnakit/core/collection.py +163 -0
  23. dnakit/core/coordinates.py +295 -0
  24. dnakit/core/enums.py +95 -0
  25. dnakit/core/facade.py +719 -0
  26. dnakit/core/feature.py +90 -0
  27. dnakit/core/gap.py +82 -0
  28. dnakit/core/issues.py +62 -0
  29. dnakit/core/provenance.py +305 -0
  30. dnakit/core/record.py +161 -0
  31. dnakit/core/results.py +134 -0
  32. dnakit/core/sequence.py +327 -0
  33. dnakit/datasets/__init__.py +100 -0
  34. dnakit/datasets/_advanced_shared.py +198 -0
  35. dnakit/datasets/_metadata.py +33 -0
  36. dnakit/datasets/advanced_deduplicate.py +183 -0
  37. dnakit/datasets/advanced_split.py +302 -0
  38. dnakit/datasets/clustering.py +286 -0
  39. dnakit/datasets/config.py +531 -0
  40. dnakit/datasets/deduplicate.py +303 -0
  41. dnakit/datasets/evaluation.py +244 -0
  42. dnakit/datasets/filter.py +135 -0
  43. dnakit/datasets/neural_clustering.py +221 -0
  44. dnakit/datasets/results.py +520 -0
  45. dnakit/datasets/split.py +450 -0
  46. dnakit/descriptors/__init__.py +102 -0
  47. dnakit/descriptors/_dinucleotide.py +345 -0
  48. dnakit/descriptors/_shared.py +180 -0
  49. dnakit/descriptors/all.py +917 -0
  50. dnakit/descriptors/basic.py +193 -0
  51. dnakit/descriptors/codon.py +195 -0
  52. dnakit/descriptors/complexity.py +201 -0
  53. dnakit/descriptors/entropy.py +101 -0
  54. dnakit/descriptors/homopolymer.py +105 -0
  55. dnakit/descriptors/kmer.py +88 -0
  56. dnakit/descriptors/results.py +286 -0
  57. dnakit/descriptors/schema.py +414 -0
  58. dnakit/descriptors/window.py +203 -0
  59. dnakit/download/__init__.py +54 -0
  60. dnakit/download/catalogs.py +166 -0
  61. dnakit/download/ena.py +131 -0
  62. dnakit/download/encode.py +103 -0
  63. dnakit/download/files.py +469 -0
  64. dnakit/download/indexes.py +233 -0
  65. dnakit/download/metadata.py +164 -0
  66. dnakit/download/models.py +227 -0
  67. dnakit/download/ncbi.py +333 -0
  68. dnakit/download/sequence.py +133 -0
  69. dnakit/download/ucsc.py +99 -0
  70. dnakit/evaluation/__init__.py +81 -0
  71. dnakit/evaluation/_shared.py +288 -0
  72. dnakit/evaluation/collection.py +270 -0
  73. dnakit/evaluation/config.py +591 -0
  74. dnakit/evaluation/distribution.py +287 -0
  75. dnakit/evaluation/frechet.py +223 -0
  76. dnakit/evaluation/generative.py +381 -0
  77. dnakit/evaluation/reference.py +409 -0
  78. dnakit/evaluation/results.py +137 -0
  79. dnakit/evaluation/scorecard.py +211 -0
  80. dnakit/evaluation/sequence.py +403 -0
  81. dnakit/evaluation/synthesis.py +249 -0
  82. dnakit/exceptions.py +189 -0
  83. dnakit/fingerprints/__init__.py +109 -0
  84. dnakit/fingerprints/_shared.py +116 -0
  85. dnakit/fingerprints/advanced.py +829 -0
  86. dnakit/fingerprints/bit.py +229 -0
  87. dnakit/fingerprints/encoding.py +351 -0
  88. dnakit/fingerprints/kmer.py +170 -0
  89. dnakit/fingerprints/results.py +531 -0
  90. dnakit/fingerprints/sketch.py +246 -0
  91. dnakit/io/__init__.py +112 -0
  92. dnakit/io/_advanced_common.py +105 -0
  93. dnakit/io/_formats.py +994 -0
  94. dnakit/io/annotations.py +1179 -0
  95. dnakit/io/api.py +759 -0
  96. dnakit/io/config.py +315 -0
  97. dnakit/io/genbank.py +523 -0
  98. dnakit/io/indexed.py +1146 -0
  99. dnakit/io/metadata.py +452 -0
  100. dnakit/io/parquet.py +56 -0
  101. dnakit/io/results.py +131 -0
  102. dnakit/io/source.py +138 -0
  103. dnakit/io/tables.py +1237 -0
  104. dnakit/molbio/__init__.py +101 -0
  105. dnakit/molbio/_shared.py +266 -0
  106. dnakit/molbio/assembly.py +279 -0
  107. dnakit/molbio/crispr.py +181 -0
  108. dnakit/molbio/optimization.py +897 -0
  109. dnakit/molbio/primer3_design.py +667 -0
  110. dnakit/molbio/primers.py +899 -0
  111. dnakit/molbio/restriction.py +573 -0
  112. dnakit/molbio/results.py +583 -0
  113. dnakit/ops/__init__.py +588 -0
  114. dnakit/ops/_common.py +201 -0
  115. dnakit/ops/circular.py +193 -0
  116. dnakit/ops/concat.py +204 -0
  117. dnakit/ops/direction.py +27 -0
  118. dnakit/ops/edit.py +443 -0
  119. dnakit/ops/evolution.py +991 -0
  120. dnakit/ops/mutation.py +151 -0
  121. dnakit/ops/records.py +1012 -0
  122. dnakit/ops/translation.py +257 -0
  123. dnakit/patterns/__init__.py +81 -0
  124. dnakit/patterns/_shared.py +282 -0
  125. dnakit/patterns/coding.py +360 -0
  126. dnakit/patterns/crispr.py +337 -0
  127. dnakit/patterns/motif.py +721 -0
  128. dnakit/patterns/regions.py +341 -0
  129. dnakit/patterns/repeats.py +480 -0
  130. dnakit/patterns/restriction.py +281 -0
  131. dnakit/patterns/results.py +411 -0
  132. dnakit/py.typed +1 -0
  133. dnakit/references/__init__.py +27 -0
  134. dnakit/references/models.py +155 -0
  135. dnakit/references/ncbi.py +785 -0
  136. dnakit/representations/__init__.py +29 -0
  137. dnakit/representations/backends.py +928 -0
  138. dnakit/representations/checkpoints.py +356 -0
  139. dnakit/representations/extraction.py +220 -0
  140. dnakit/representations/models.py +395 -0
  141. dnakit/representations/results.py +79 -0
  142. dnakit/search/__init__.py +132 -0
  143. dnakit/search/_http.py +271 -0
  144. dnakit/search/_shared.py +108 -0
  145. dnakit/search/blast.py +516 -0
  146. dnakit/search/ena.py +245 -0
  147. dnakit/search/encode.py +102 -0
  148. dnakit/search/ensembl.py +874 -0
  149. dnakit/search/models.py +159 -0
  150. dnakit/search/ncbi.py +756 -0
  151. dnakit/search/ucsc.py +388 -0
  152. dnakit/secondary_structure/__init__.py +38 -0
  153. dnakit/secondary_structure/dotbracket.py +410 -0
  154. dnakit/secondary_structure/nupack.py +589 -0
  155. dnakit/secondary_structure/results.py +142 -0
  156. dnakit/similarity/__init__.py +125 -0
  157. dnakit/similarity/_shared.py +152 -0
  158. dnakit/similarity/approximate.py +258 -0
  159. dnakit/similarity/compare.py +162 -0
  160. dnakit/similarity/dashing.py +936 -0
  161. dnakit/similarity/distance.py +279 -0
  162. dnakit/similarity/index.py +365 -0
  163. dnakit/similarity/matrix.py +254 -0
  164. dnakit/similarity/results.py +676 -0
  165. dnakit/similarity/search.py +286 -0
  166. dnakit/similarity/sketch.py +96 -0
  167. dnakit/similarity/vector.py +662 -0
  168. dnakit/standardize/__init__.py +77 -0
  169. dnakit/standardize/_shared.py +100 -0
  170. dnakit/standardize/config.py +411 -0
  171. dnakit/standardize/gaps.py +1060 -0
  172. dnakit/standardize/normalize.py +565 -0
  173. dnakit/standardize/results.py +247 -0
  174. dnakit/standardize/validate.py +487 -0
  175. dnakit/structure3d/__init__.py +47 -0
  176. dnakit/structure3d/_shared.py +47 -0
  177. dnakit/structure3d/geometry.py +773 -0
  178. dnakit/structure3d/pdb.py +253 -0
  179. dnakit/structure3d/results.py +233 -0
  180. dnakit/structure3d/threedna.py +174 -0
  181. dnakit/thermodynamics/__init__.py +112 -0
  182. dnakit/thermodynamics/_shared.py +92 -0
  183. dnakit/thermodynamics/backends.py +653 -0
  184. dnakit/thermodynamics/calculations.py +837 -0
  185. dnakit/thermodynamics/config.py +175 -0
  186. dnakit/thermodynamics/equilibrium.py +340 -0
  187. dnakit/thermodynamics/optics.py +385 -0
  188. dnakit/thermodynamics/parameters.py +79 -0
  189. dnakit/thermodynamics/results.py +445 -0
  190. dnakit/visualization/__init__.py +43 -0
  191. dnakit/visualization/_svg.py +185 -0
  192. dnakit/visualization/advanced.py +314 -0
  193. dnakit/visualization/config.py +255 -0
  194. dnakit/visualization/export.py +349 -0
  195. dnakit/visualization/matrix.py +279 -0
  196. dnakit/visualization/report.py +431 -0
  197. dnakit/visualization/results.py +236 -0
  198. dnakit/visualization/sequence.py +459 -0
  199. dnakit/workflows/__init__.py +45 -0
  200. dnakit/workflows/manifest.py +365 -0
  201. dnakit/workflows/runner.py +1548 -0
  202. dnakit/workflows/schema.py +898 -0
  203. dnakit-0.1.0.dev0.dist-info/METADATA +126 -0
  204. dnakit-0.1.0.dev0.dist-info/RECORD +210 -0
  205. dnakit-0.1.0.dev0.dist-info/WHEEL +5 -0
  206. dnakit-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  207. dnakit-0.1.0.dev0.dist-info/licenses/DISCLAIMER.md +26 -0
  208. dnakit-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
  209. dnakit-0.1.0.dev0.dist-info/licenses/THIRD_PARTY_NOTICES.md +73 -0
  210. dnakit-0.1.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,459 @@
1
+ """Dependency-free gap-aware DNA sequence text visualization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass
8
+
9
+ from dnakit.core.enums import IssueSeverity, Topology
10
+ from dnakit.core.facade import DNA, resolve_single_dna
11
+ from dnakit.core.gap import Gap
12
+ from dnakit.core.issues import Issue
13
+ from dnakit.core.record import DNARecord
14
+ from dnakit.core.sequence import DNASequence
15
+ from dnakit.exceptions import ConfigurationError
16
+
17
+ from ._svg import SVGBuilder, validate_xml_text
18
+ from .config import LimitPolicy, SequencePlotConfig, _color
19
+ from .results import SVGArtifact
20
+
21
+ _COMPLEMENT = str.maketrans(
22
+ {
23
+ "A": "T",
24
+ "C": "G",
25
+ "G": "C",
26
+ "T": "A",
27
+ "R": "Y",
28
+ "Y": "R",
29
+ "S": "S",
30
+ "W": "W",
31
+ "K": "M",
32
+ "M": "K",
33
+ "B": "V",
34
+ "D": "H",
35
+ "H": "D",
36
+ "V": "B",
37
+ "N": "N",
38
+ }
39
+ )
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class Highlight:
44
+ """A zero-based, half-open highlight in nucleotide-symbol coordinates."""
45
+
46
+ start: int
47
+ end: int
48
+ label: str | None = None
49
+ color: str = "#fde68a"
50
+ foreground: str | None = None
51
+ priority: int = 0
52
+ opacity: float = 0.7
53
+
54
+ def __post_init__(self) -> None:
55
+ for name, value in (("start", self.start), ("end", self.end)):
56
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
57
+ raise ConfigurationError(
58
+ f"Highlight {name} must be a non-negative integer.",
59
+ code="INVALID_HIGHLIGHT",
60
+ )
61
+ if self.end <= self.start:
62
+ raise ConfigurationError(
63
+ "Highlight end must be greater than start.", code="INVALID_HIGHLIGHT"
64
+ )
65
+ if self.label is not None:
66
+ if not isinstance(self.label, str) or not self.label.strip():
67
+ raise ConfigurationError(
68
+ "Highlight label must be non-empty or None.", code="INVALID_HIGHLIGHT"
69
+ )
70
+ try:
71
+ validate_xml_text(self.label, name="highlight label")
72
+ except ValueError as exc:
73
+ raise ConfigurationError(
74
+ "Highlight label contains a character forbidden by XML.",
75
+ code="INVALID_HIGHLIGHT",
76
+ ) from exc
77
+ _color(self.color, "highlight color")
78
+ if self.foreground is not None:
79
+ _color(self.foreground, "highlight foreground")
80
+ if isinstance(self.priority, bool) or not isinstance(self.priority, int):
81
+ raise ConfigurationError(
82
+ "Highlight priority must be an integer.", code="INVALID_HIGHLIGHT"
83
+ )
84
+ if (
85
+ isinstance(self.opacity, bool)
86
+ or not isinstance(self.opacity, (int, float))
87
+ or not math.isfinite(self.opacity)
88
+ or not 0.0 <= self.opacity <= 1.0
89
+ ):
90
+ raise ConfigurationError(
91
+ "Highlight opacity must be finite and between zero and one.",
92
+ code="INVALID_HIGHLIGHT",
93
+ )
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class _Cell:
98
+ text: str
99
+ columns: int
100
+ symbol_index: int | None
101
+ coordinate_start: int | None
102
+ coordinate_end: int | None
103
+ is_gap: bool
104
+
105
+
106
+ @dataclass(frozen=True, slots=True)
107
+ class _Row:
108
+ cells: tuple[_Cell, ...]
109
+ columns: int
110
+
111
+
112
+ def _coerce_input(value: DNA | DNASequence | DNARecord) -> tuple[DNASequence, str | None, int]:
113
+ if isinstance(value, (DNA, DNARecord, DNASequence)):
114
+ sequence, record = resolve_single_dna(value)
115
+ return (
116
+ sequence,
117
+ None if record is None else record.id,
118
+ 0 if record is None else len(record.features),
119
+ )
120
+ raise TypeError("plot_sequence() accepts DNA, DNASequence, or DNARecord.")
121
+
122
+
123
+ def _gap_label(gap: Gap) -> str:
124
+ return "[… bp]" if gap.length is None else f"[{gap.length} bp]"
125
+
126
+
127
+ def _cells(sequence: DNASequence, display_limit: int) -> tuple[_Cell, ...]:
128
+ cells: list[_Cell] = []
129
+ symbol_index = 0
130
+ coordinate: int | None = 0
131
+ for part in sequence.parts:
132
+ if isinstance(part, Gap):
133
+ if symbol_index >= display_limit and sequence.symbol_length > display_limit:
134
+ break
135
+ label = _gap_label(part)
136
+ coordinate_end = (
137
+ coordinate + part.length
138
+ if coordinate is not None and part.length is not None
139
+ else None
140
+ )
141
+ cells.append(
142
+ _Cell(
143
+ label,
144
+ max(4, len(label) + 1),
145
+ None,
146
+ coordinate,
147
+ coordinate_end,
148
+ True,
149
+ )
150
+ )
151
+ coordinate = coordinate_end
152
+ continue
153
+ for symbol in part:
154
+ if symbol_index >= display_limit:
155
+ return tuple(cells)
156
+ coordinate_end = None if coordinate is None else coordinate + 1
157
+ cells.append(_Cell(symbol, 1, symbol_index, coordinate, coordinate_end, False))
158
+ symbol_index += 1
159
+ coordinate = coordinate_end
160
+ return tuple(cells)
161
+
162
+
163
+ def _rows(cells: tuple[_Cell, ...], columns_per_line: int) -> tuple[_Row, ...]:
164
+ rows: list[_Row] = []
165
+ current: list[_Cell] = []
166
+ columns = 0
167
+ for cell in cells:
168
+ if current and columns + cell.columns > columns_per_line:
169
+ rows.append(_Row(tuple(current), columns))
170
+ current = []
171
+ columns = 0
172
+ current.append(cell)
173
+ columns += cell.columns
174
+ if current:
175
+ rows.append(_Row(tuple(current), columns))
176
+ return tuple(rows)
177
+
178
+
179
+ def _active_highlight(
180
+ symbol_index: int, highlights: tuple[Highlight, ...]
181
+ ) -> tuple[int, Highlight] | None:
182
+ candidates = tuple(
183
+ (index, highlight)
184
+ for index, highlight in enumerate(highlights)
185
+ if highlight.start <= symbol_index < highlight.end
186
+ )
187
+ if not candidates:
188
+ return None
189
+ return max(candidates, key=lambda item: (item[1].priority, item[0]))
190
+
191
+
192
+ def _coordinate_label(value: int | None, offset: int) -> str:
193
+ return "?" if value is None else str(value + offset)
194
+
195
+
196
+ def plot_sequence(
197
+ value: DNA | DNASequence | DNARecord,
198
+ *,
199
+ highlights: Iterable[Highlight] = (),
200
+ config: SequencePlotConfig | None = None,
201
+ ) -> SVGArtifact:
202
+ """Render symbols, explicit gaps, coordinates, and optional highlights.
203
+
204
+ Highlight coordinates always refer to ``DNASequence.symbols`` and therefore
205
+ exclude :class:`~dnakit.core.Gap` spans. Record features are deliberately
206
+ not rendered implicitly; linear feature maps remain an advanced module.
207
+ """
208
+
209
+ sequence, sequence_id, feature_count = _coerce_input(value)
210
+ resolved = SequencePlotConfig() if config is None else config
211
+ if not isinstance(resolved, SequencePlotConfig):
212
+ raise TypeError("config must be SequencePlotConfig or None.")
213
+ highlight_tuple = tuple(highlights)
214
+ if any(not isinstance(item, Highlight) for item in highlight_tuple):
215
+ raise TypeError("highlights must contain only Highlight objects.")
216
+ if any(item.end > sequence.symbol_length for item in highlight_tuple):
217
+ raise ConfigurationError(
218
+ "A highlight exceeds sequence symbol_length.",
219
+ code="HIGHLIGHT_OUT_OF_RANGE",
220
+ context={"symbol_length": sequence.symbol_length},
221
+ )
222
+
223
+ gaps = tuple(part for part in sequence.parts if isinstance(part, Gap))
224
+ if len(gaps) > resolved.max_gaps:
225
+ raise ConfigurationError(
226
+ "Sequence plot exceeds max_gaps.",
227
+ code="VISUALIZATION_SIZE_LIMIT",
228
+ context={"gap_count": len(gaps), "max_gaps": resolved.max_gaps},
229
+ )
230
+ truncated = sequence.symbol_length > resolved.max_symbols
231
+ if truncated and resolved.limit_policy is LimitPolicy.ERROR:
232
+ raise ConfigurationError(
233
+ "Sequence plot exceeds max_symbols.",
234
+ code="VISUALIZATION_SIZE_LIMIT",
235
+ context={
236
+ "symbol_length": sequence.symbol_length,
237
+ "max_symbols": resolved.max_symbols,
238
+ },
239
+ hint="Raise max_symbols or explicitly choose limit_policy='truncate'.",
240
+ )
241
+ display_limit = min(sequence.symbol_length, resolved.max_symbols)
242
+ cells = _cells(sequence, display_limit)
243
+ rows = _rows(cells, resolved.bases_per_line)
244
+
245
+ issues: list[Issue] = []
246
+ if truncated:
247
+ issues.append(
248
+ Issue(
249
+ "VIZ_SEQUENCE_TRUNCATED",
250
+ IssueSeverity.WARNING,
251
+ "The sequence text plot was explicitly truncated.",
252
+ details={
253
+ "symbol_length": sequence.symbol_length,
254
+ "displayed_symbols": display_limit,
255
+ },
256
+ )
257
+ )
258
+ clipped_highlights = sum(item.end > display_limit for item in highlight_tuple)
259
+ if clipped_highlights:
260
+ issues.append(
261
+ Issue(
262
+ "VIZ_HIGHLIGHT_CLIPPED",
263
+ IssueSeverity.WARNING,
264
+ "At least one highlight extends beyond the displayed sequence prefix.",
265
+ details={"count": clipped_highlights},
266
+ )
267
+ )
268
+ if sequence.topology is Topology.CIRCULAR:
269
+ issues.append(
270
+ Issue(
271
+ "VIZ_CIRCULAR_LINEARIZED",
272
+ IssueSeverity.INFO,
273
+ "The circular sequence is shown as a linear text view.",
274
+ )
275
+ )
276
+
277
+ title = resolved.title or (
278
+ f"DNA sequence: {sequence_id}" if sequence_id is not None else "DNA sequence"
279
+ )
280
+ coordinate_gutter = 72 if resolved.show_coordinates else 0
281
+ max_columns = max((row.columns for row in rows), default=20)
282
+ width = max(
283
+ 420,
284
+ resolved.margin * 2
285
+ + coordinate_gutter * (2 if resolved.show_coordinates else 0)
286
+ + max_columns * resolved.cell_width,
287
+ )
288
+ row_height = resolved.line_height + (resolved.font_size + 8 if resolved.show_complement else 0)
289
+ title_height = resolved.font_size + 32
290
+ height = max(
291
+ 120,
292
+ resolved.margin * 2 + title_height + max(1, len(rows)) * row_height,
293
+ )
294
+ metadata = {
295
+ "coordinate_space": "symbol-highlights; sequence-span-labels",
296
+ "display_start_coordinate": resolved.start_coordinate,
297
+ "displayed_symbols": display_limit,
298
+ "feature_count_not_rendered": feature_count,
299
+ "gap_count": len(gaps),
300
+ "highlight_count": len(highlight_tuple),
301
+ "sequence_id": sequence_id,
302
+ "symbol_length": sequence.symbol_length,
303
+ "topology": sequence.topology.value,
304
+ "truncated": truncated,
305
+ "unknown_gap_count": sum(gap.length is None for gap in gaps),
306
+ }
307
+ builder = SVGBuilder(
308
+ width,
309
+ height,
310
+ title=title,
311
+ description="Gap-aware DNA symbol text plot in input order.",
312
+ kind="sequence",
313
+ theme=resolved.theme,
314
+ metadata=metadata,
315
+ )
316
+ builder.text(
317
+ resolved.margin,
318
+ resolved.margin + resolved.font_size,
319
+ title,
320
+ fill=resolved.theme.foreground,
321
+ font_family=resolved.theme.font_family,
322
+ font_size=resolved.font_size + 2,
323
+ font_weight="bold",
324
+ class_="plot-title",
325
+ )
326
+
327
+ if not rows:
328
+ builder.text(
329
+ width / 2,
330
+ height / 2,
331
+ "Empty sequence",
332
+ fill=resolved.theme.muted,
333
+ font_family=resolved.theme.font_family,
334
+ font_size=resolved.font_size,
335
+ text_anchor="middle",
336
+ class_="empty-state",
337
+ )
338
+ return builder.artifact(issues=issues)
339
+
340
+ base_colors = {
341
+ "A": "#16a34a",
342
+ "C": "#2563eb",
343
+ "G": "#d97706",
344
+ "T": "#dc2626",
345
+ }
346
+ for row_index, row in enumerate(rows):
347
+ y = resolved.margin + title_height + row_index * row_height + resolved.font_size
348
+ x = resolved.margin + coordinate_gutter
349
+ if resolved.show_coordinates:
350
+ builder.text(
351
+ resolved.margin,
352
+ y,
353
+ _coordinate_label(row.cells[0].coordinate_start, resolved.start_coordinate),
354
+ fill=resolved.theme.muted,
355
+ font_family=resolved.theme.font_family,
356
+ font_size=max(9, resolved.font_size - 2),
357
+ class_="coordinate coordinate-start",
358
+ )
359
+ for cell in row.cells:
360
+ cell_width = cell.columns * resolved.cell_width
361
+ if cell.is_gap:
362
+ builder.rect(
363
+ x,
364
+ y - resolved.font_size,
365
+ cell_width - 2,
366
+ resolved.font_size + 6,
367
+ fill=resolved.theme.missing,
368
+ stroke=resolved.theme.grid,
369
+ stroke_dasharray="3 2",
370
+ rx=3,
371
+ class_="gap",
372
+ data_coordinate_start=_coordinate_label(
373
+ cell.coordinate_start, resolved.start_coordinate
374
+ ),
375
+ data_coordinate_end=_coordinate_label(
376
+ cell.coordinate_end, resolved.start_coordinate
377
+ ),
378
+ )
379
+ builder.text(
380
+ x + cell_width / 2,
381
+ y,
382
+ cell.text,
383
+ fill=resolved.theme.muted,
384
+ font_family=resolved.theme.font_family,
385
+ font_size=max(9, resolved.font_size - 2),
386
+ text_anchor="middle",
387
+ class_="gap-label",
388
+ )
389
+ else:
390
+ assert cell.symbol_index is not None
391
+ active = _active_highlight(cell.symbol_index, highlight_tuple)
392
+ foreground = base_colors.get(cell.text, resolved.theme.muted)
393
+ if active is not None:
394
+ highlight_index, highlight = active
395
+ builder.rect(
396
+ x,
397
+ y - resolved.font_size,
398
+ cell_width,
399
+ resolved.font_size + 6,
400
+ fill=highlight.color,
401
+ fill_opacity=highlight.opacity,
402
+ class_="highlight",
403
+ data_highlight_index=highlight_index,
404
+ data_priority=highlight.priority,
405
+ )
406
+ if highlight.foreground is not None:
407
+ foreground = highlight.foreground
408
+ if highlight.label is not None and cell.symbol_index == highlight.start:
409
+ builder.text(
410
+ x,
411
+ y - resolved.font_size - 3,
412
+ highlight.label,
413
+ fill=resolved.theme.foreground,
414
+ font_family=resolved.theme.font_family,
415
+ font_size=max(8, resolved.font_size - 4),
416
+ class_="highlight-label",
417
+ )
418
+ builder.text(
419
+ x + cell_width / 2,
420
+ y,
421
+ cell.text,
422
+ fill=foreground,
423
+ font_family=resolved.theme.font_family,
424
+ font_size=resolved.font_size,
425
+ font_weight="bold",
426
+ text_anchor="middle",
427
+ class_="base",
428
+ data_symbol_index=cell.symbol_index,
429
+ data_coordinate=_coordinate_label(
430
+ cell.coordinate_start, resolved.start_coordinate
431
+ ),
432
+ )
433
+ if resolved.show_complement:
434
+ builder.text(
435
+ x + cell_width / 2,
436
+ y + resolved.font_size + 8,
437
+ cell.text.translate(_COMPLEMENT),
438
+ fill=foreground,
439
+ font_family=resolved.theme.font_family,
440
+ font_size=resolved.font_size,
441
+ text_anchor="middle",
442
+ class_="complement",
443
+ data_symbol_index=cell.symbol_index,
444
+ )
445
+ x += cell_width
446
+ if resolved.show_coordinates:
447
+ builder.text(
448
+ x + 8,
449
+ y,
450
+ _coordinate_label(row.cells[-1].coordinate_end, resolved.start_coordinate),
451
+ fill=resolved.theme.muted,
452
+ font_family=resolved.theme.font_family,
453
+ font_size=max(9, resolved.font_size - 2),
454
+ class_="coordinate coordinate-end",
455
+ )
456
+ return builder.artifact(issues=issues)
457
+
458
+
459
+ __all__ = ["Highlight", "plot_sequence"]
@@ -0,0 +1,45 @@
1
+ """Strict local workflows, manifests, and auditable execution helpers."""
2
+
3
+ from dnakit.workflows.manifest import (
4
+ RunManifestBuilder,
5
+ artifact_from_path,
6
+ load_manifest,
7
+ save_manifest,
8
+ )
9
+ from dnakit.workflows.runner import (
10
+ ProgressCallback,
11
+ WorkflowProgress,
12
+ WorkflowRunResult,
13
+ WorkflowStepResult,
14
+ run_workflow,
15
+ )
16
+ from dnakit.workflows.schema import (
17
+ LoadedWorkflow,
18
+ WorkflowErrorPolicy,
19
+ WorkflowInput,
20
+ WorkflowLimits,
21
+ WorkflowOperation,
22
+ WorkflowSpec,
23
+ WorkflowStep,
24
+ load_workflow,
25
+ )
26
+
27
+ __all__ = [
28
+ "LoadedWorkflow",
29
+ "ProgressCallback",
30
+ "RunManifestBuilder",
31
+ "WorkflowErrorPolicy",
32
+ "WorkflowInput",
33
+ "WorkflowLimits",
34
+ "WorkflowOperation",
35
+ "WorkflowProgress",
36
+ "WorkflowRunResult",
37
+ "WorkflowSpec",
38
+ "WorkflowStep",
39
+ "WorkflowStepResult",
40
+ "artifact_from_path",
41
+ "load_manifest",
42
+ "load_workflow",
43
+ "run_workflow",
44
+ "save_manifest",
45
+ ]