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
dnakit/core/feature.py ADDED
@@ -0,0 +1,90 @@
1
+ """Immutable DNA feature annotations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+
9
+ from dnakit.core._json import FrozenDict, freeze_mapping
10
+ from dnakit.core.coordinates import CompoundLocation, Interval, Location, UnresolvedLocation
11
+ from dnakit.core.enums import Strand
12
+ from dnakit.exceptions import FeatureError
13
+
14
+
15
+ @dataclass(frozen=True, init=False)
16
+ class DNAFeature:
17
+ """A typed annotation referring to a location without copying sequence data."""
18
+
19
+ type: str
20
+ location: Location
21
+ id: str | None
22
+ strand: Strand
23
+ label: str | None
24
+ score: float | None
25
+ phase: int | None
26
+ qualifiers: FrozenDict
27
+ source: str | None
28
+
29
+ __hash__ = None # type: ignore[assignment]
30
+
31
+ def __init__(
32
+ self,
33
+ type: str,
34
+ location: Location,
35
+ *,
36
+ id: str | None = None,
37
+ strand: Strand | str = Strand.UNKNOWN,
38
+ label: str | None = None,
39
+ score: float | None = None,
40
+ phase: int | None = None,
41
+ qualifiers: Mapping[str, object] | None = None,
42
+ source: str | None = None,
43
+ ) -> None:
44
+ if not isinstance(type, str) or not type.strip():
45
+ raise FeatureError("Feature type must be a non-empty string.")
46
+ if not isinstance(location, (Interval, CompoundLocation, UnresolvedLocation)):
47
+ raise FeatureError("Feature location must be an internal Location object.")
48
+ for field_name, value in (("id", id), ("label", label), ("source", source)):
49
+ if value is not None and (not isinstance(value, str) or not value.strip()):
50
+ raise FeatureError(
51
+ f"Feature {field_name} must be None or a non-empty string.",
52
+ context={field_name: value},
53
+ )
54
+ try:
55
+ resolved_strand = strand if isinstance(strand, Strand) else Strand(strand)
56
+ except (TypeError, ValueError) as exc:
57
+ raise FeatureError(
58
+ "Unknown feature strand.",
59
+ context={"strand": strand},
60
+ hint=f"Choose one of: {', '.join(item.value for item in Strand)}.",
61
+ ) from exc
62
+ if score is not None and (
63
+ isinstance(score, bool)
64
+ or not isinstance(score, (int, float))
65
+ or not math.isfinite(score)
66
+ ):
67
+ raise FeatureError(
68
+ "Feature score must be a finite number or None.",
69
+ context={"score": score},
70
+ )
71
+ if phase is not None and (
72
+ isinstance(phase, bool) or not isinstance(phase, int) or phase not in (0, 1, 2)
73
+ ):
74
+ raise FeatureError(
75
+ "Feature phase must be 0, 1, 2, or None.",
76
+ context={"phase": phase},
77
+ )
78
+
79
+ object.__setattr__(self, "type", type)
80
+ object.__setattr__(self, "location", location)
81
+ object.__setattr__(self, "id", id)
82
+ object.__setattr__(self, "strand", resolved_strand)
83
+ object.__setattr__(self, "label", label)
84
+ object.__setattr__(self, "score", None if score is None else float(score))
85
+ object.__setattr__(self, "phase", phase)
86
+ object.__setattr__(self, "qualifiers", freeze_mapping(qualifiers))
87
+ object.__setattr__(self, "source", source)
88
+
89
+
90
+ __all__ = ["DNAFeature"]
dnakit/core/gap.py ADDED
@@ -0,0 +1,82 @@
1
+ """Explicit known- and unknown-length sequence gaps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from dataclasses import dataclass
7
+
8
+ from dnakit.core._json import FrozenDict, freeze_mapping
9
+ from dnakit.core.enums import GapKind
10
+ from dnakit.exceptions import SequenceError
11
+
12
+
13
+ @dataclass(frozen=True, init=False)
14
+ class Gap:
15
+ """A gap between sequence fragments.
16
+
17
+ ``length=None`` denotes an unknown coordinate span. A gap never stores a
18
+ second copy of its flanking sequence; the flanks are determined by its
19
+ position in :class:`DNASequence.parts`.
20
+ """
21
+
22
+ length: int | None
23
+ kind: GapKind
24
+ crossable: bool | None
25
+ evidence: tuple[str, ...]
26
+ metadata: FrozenDict
27
+
28
+ def __init__(
29
+ self,
30
+ length: int | None,
31
+ kind: GapKind | str = GapKind.UNKNOWN,
32
+ crossable: bool | None = None,
33
+ evidence: Iterable[str] = (),
34
+ metadata: Mapping[str, object] | None = None,
35
+ ) -> None:
36
+ if length is not None and (isinstance(length, bool) or not isinstance(length, int)):
37
+ raise SequenceError(
38
+ "Gap length must be a positive integer or None.",
39
+ code="INVALID_GAP_LENGTH",
40
+ context={"length": length},
41
+ )
42
+ if length is not None and length <= 0:
43
+ raise SequenceError(
44
+ "Gap length must be greater than zero.",
45
+ code="INVALID_GAP_LENGTH",
46
+ context={"length": length},
47
+ hint="Use None when the gap length is unknown.",
48
+ )
49
+ try:
50
+ resolved_kind = kind if isinstance(kind, GapKind) else GapKind(kind)
51
+ except (TypeError, ValueError) as exc:
52
+ raise SequenceError(
53
+ "Unknown gap kind.",
54
+ code="INVALID_GAP_KIND",
55
+ context={"kind": kind},
56
+ hint=f"Choose one of: {', '.join(item.value for item in GapKind)}.",
57
+ ) from exc
58
+ if crossable is not None and not isinstance(crossable, bool):
59
+ raise SequenceError(
60
+ "Gap crossable policy must be True, False, or None.",
61
+ code="INVALID_GAP_POLICY",
62
+ context={"crossable": crossable},
63
+ )
64
+ evidence_tuple = tuple(evidence)
65
+ if any(not isinstance(item, str) or not item.strip() for item in evidence_tuple):
66
+ raise SequenceError(
67
+ "Gap evidence entries must be non-empty strings.",
68
+ code="INVALID_GAP_EVIDENCE",
69
+ )
70
+
71
+ object.__setattr__(self, "length", length)
72
+ object.__setattr__(self, "kind", resolved_kind)
73
+ object.__setattr__(self, "crossable", crossable)
74
+ object.__setattr__(self, "evidence", evidence_tuple)
75
+ object.__setattr__(self, "metadata", freeze_mapping(metadata))
76
+
77
+ @property
78
+ def is_known_length(self) -> bool:
79
+ return self.length is not None
80
+
81
+
82
+ __all__ = ["Gap"]
dnakit/core/issues.py ADDED
@@ -0,0 +1,62 @@
1
+ """Structured, serializable non-fatal diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from typing import Any, cast
8
+
9
+ from dnakit.core._json import FrozenDict, freeze_mapping, to_json_compatible
10
+ from dnakit.core.coordinates import CompoundLocation, Interval, Location, UnresolvedLocation
11
+ from dnakit.core.enums import IssueSeverity
12
+ from dnakit.exceptions import ConfigurationError
13
+
14
+
15
+ @dataclass(frozen=True, init=False)
16
+ class Issue:
17
+ """A stable machine-readable finding that does not require raising."""
18
+
19
+ code: str
20
+ severity: IssueSeverity
21
+ message: str
22
+ location: Location | None
23
+ details: FrozenDict
24
+
25
+ def __init__(
26
+ self,
27
+ code: str,
28
+ severity: IssueSeverity | str,
29
+ message: str,
30
+ location: Location | None = None,
31
+ details: Mapping[str, object] | None = None,
32
+ ) -> None:
33
+ if not isinstance(code, str) or not code.strip():
34
+ raise ConfigurationError("Issue code must be a non-empty string.")
35
+ if not isinstance(message, str) or not message.strip():
36
+ raise ConfigurationError("Issue message must be a non-empty string.")
37
+ try:
38
+ resolved_severity = (
39
+ severity if isinstance(severity, IssueSeverity) else IssueSeverity(severity)
40
+ )
41
+ except (TypeError, ValueError) as exc:
42
+ raise ConfigurationError(
43
+ "Unknown issue severity.",
44
+ context={"severity": severity},
45
+ hint=f"Choose one of: {', '.join(item.value for item in IssueSeverity)}.",
46
+ ) from exc
47
+ if location is not None and not isinstance(
48
+ location, (Interval, CompoundLocation, UnresolvedLocation)
49
+ ):
50
+ raise ConfigurationError("Issue location must be None or an internal Location.")
51
+
52
+ object.__setattr__(self, "code", code)
53
+ object.__setattr__(self, "severity", resolved_severity)
54
+ object.__setattr__(self, "message", message)
55
+ object.__setattr__(self, "location", location)
56
+ object.__setattr__(self, "details", freeze_mapping(details))
57
+
58
+ def to_dict(self) -> dict[str, Any]:
59
+ return cast(dict[str, Any], to_json_compatible(self))
60
+
61
+
62
+ __all__ = ["Issue"]
@@ -0,0 +1,305 @@
1
+ """Implementation, reference, artifact, and run provenance value objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform as platform_module
6
+ import re
7
+ from collections.abc import Iterable, Mapping
8
+ from dataclasses import dataclass
9
+ from importlib.metadata import PackageNotFoundError, version
10
+ from typing import Any, cast
11
+
12
+ from dnakit.core._json import FrozenDict, freeze_mapping, to_json_compatible
13
+ from dnakit.core.backend_info import BackendInfo
14
+ from dnakit.core.enums import ExecutionMode, ImplementationLabel, OriginClass
15
+ from dnakit.core.issues import Issue
16
+ from dnakit.exceptions import ConfigurationError
17
+
18
+ _SHA256_PATTERN = re.compile(r"^[0-9a-fA-F]{64}$")
19
+
20
+
21
+ def _installed_dnakit_version() -> str:
22
+ try:
23
+ return version("dnakit")
24
+ except PackageNotFoundError:
25
+ return "unknown"
26
+
27
+
28
+ def _non_empty_optional(value: str | None, name: str) -> None:
29
+ if value is not None and (not isinstance(value, str) or not value.strip()):
30
+ raise ConfigurationError(f"{name} must be None or a non-empty string.")
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Citation:
35
+ """A compact citation identifier with optional resolvable metadata."""
36
+
37
+ key: str
38
+ title: str | None = None
39
+ doi: str | None = None
40
+ url: str | None = None
41
+
42
+ def __post_init__(self) -> None:
43
+ if not isinstance(self.key, str) or not self.key.strip():
44
+ raise ConfigurationError("Citation key must be a non-empty string.")
45
+ for name in ("title", "doi", "url"):
46
+ _non_empty_optional(getattr(self, name), f"Citation {name}")
47
+
48
+
49
+ @dataclass(frozen=True, init=False)
50
+ class ImplementationInfo:
51
+ """The single authoritative classification of an implementation."""
52
+
53
+ label: ImplementationLabel
54
+ execution_mode: ExecutionMode
55
+ origin_class: OriginClass
56
+ license_expression: str | None
57
+ citations: tuple[Citation, ...]
58
+
59
+ def __init__(
60
+ self,
61
+ label: ImplementationLabel | str = ImplementationLabel.NATIVE,
62
+ execution_mode: ExecutionMode | str = ExecutionMode.INTERNAL,
63
+ origin_class: OriginClass | str = OriginClass.DNAKIT,
64
+ license_expression: str | None = None,
65
+ citations: Iterable[Citation] = (),
66
+ ) -> None:
67
+ try:
68
+ resolved_label = (
69
+ label if isinstance(label, ImplementationLabel) else ImplementationLabel(label)
70
+ )
71
+ resolved_execution = (
72
+ execution_mode
73
+ if isinstance(execution_mode, ExecutionMode)
74
+ else ExecutionMode(execution_mode)
75
+ )
76
+ resolved_origin = (
77
+ origin_class if isinstance(origin_class, OriginClass) else OriginClass(origin_class)
78
+ )
79
+ except (TypeError, ValueError) as exc:
80
+ raise ConfigurationError(
81
+ "Invalid implementation classification.",
82
+ context={
83
+ "label": label,
84
+ "execution_mode": execution_mode,
85
+ "origin_class": origin_class,
86
+ },
87
+ ) from exc
88
+ _non_empty_optional(license_expression, "Implementation license_expression")
89
+ citation_tuple = tuple(citations)
90
+ if any(not isinstance(citation, Citation) for citation in citation_tuple):
91
+ raise ConfigurationError("Implementation citations must all be Citation objects.")
92
+
93
+ object.__setattr__(self, "label", resolved_label)
94
+ object.__setattr__(self, "execution_mode", resolved_execution)
95
+ object.__setattr__(self, "origin_class", resolved_origin)
96
+ object.__setattr__(self, "license_expression", license_expression)
97
+ object.__setattr__(self, "citations", citation_tuple)
98
+
99
+
100
+ @dataclass(frozen=True, init=False)
101
+ class ReferenceInfo:
102
+ """Versioned reference database metadata."""
103
+
104
+ name: str
105
+ version: str | None
106
+ date: str | None
107
+ checksum: str | None
108
+ filters: FrozenDict
109
+
110
+ def __init__(
111
+ self,
112
+ name: str,
113
+ *,
114
+ version: str | None = None,
115
+ date: str | None = None,
116
+ checksum: str | None = None,
117
+ filters: Mapping[str, object] | None = None,
118
+ ) -> None:
119
+ if not isinstance(name, str) or not name.strip():
120
+ raise ConfigurationError("Reference name must be a non-empty string.")
121
+ for field_name, value in (
122
+ ("version", version),
123
+ ("date", date),
124
+ ("checksum", checksum),
125
+ ):
126
+ _non_empty_optional(value, f"Reference {field_name}")
127
+ object.__setattr__(self, "name", name)
128
+ object.__setattr__(self, "version", version)
129
+ object.__setattr__(self, "date", date)
130
+ object.__setattr__(self, "checksum", checksum)
131
+ object.__setattr__(self, "filters", freeze_mapping(filters))
132
+
133
+
134
+ @dataclass(frozen=True, init=False)
135
+ class Provenance:
136
+ """Environment and implementation identity attached to a result."""
137
+
138
+ dnakit_version: str
139
+ python_version: str
140
+ platform: str
141
+ dependency_versions: FrozenDict
142
+ implementation: ImplementationInfo
143
+ backend: BackendInfo | None
144
+ reference: ReferenceInfo | None
145
+
146
+ def __init__(
147
+ self,
148
+ dnakit_version: str | None = None,
149
+ python_version: str | None = None,
150
+ platform: str | None = None,
151
+ dependency_versions: Mapping[str, str] | None = None,
152
+ implementation: ImplementationInfo | None = None,
153
+ backend: BackendInfo | None = None,
154
+ reference: ReferenceInfo | None = None,
155
+ ) -> None:
156
+ resolved_dnakit = _installed_dnakit_version() if dnakit_version is None else dnakit_version
157
+ resolved_python = (
158
+ platform_module.python_version() if python_version is None else python_version
159
+ )
160
+ resolved_platform = platform_module.platform() if platform is None else platform
161
+ for field_name, value in (
162
+ ("dnakit_version", resolved_dnakit),
163
+ ("python_version", resolved_python),
164
+ ("platform", resolved_platform),
165
+ ):
166
+ if not isinstance(value, str) or not value.strip():
167
+ raise ConfigurationError(f"Provenance {field_name} must be non-empty.")
168
+ if dependency_versions is not None and any(
169
+ not isinstance(name, str)
170
+ or not name.strip()
171
+ or not isinstance(value, str)
172
+ or not value.strip()
173
+ for name, value in dependency_versions.items()
174
+ ):
175
+ raise ConfigurationError("Dependency versions must map non-empty strings to strings.")
176
+ resolved_implementation = implementation or ImplementationInfo()
177
+ if not isinstance(resolved_implementation, ImplementationInfo):
178
+ raise ConfigurationError("Provenance implementation must be ImplementationInfo.")
179
+ if backend is not None and not isinstance(backend, BackendInfo):
180
+ raise ConfigurationError("Provenance backend must be BackendInfo or None.")
181
+ if reference is not None and not isinstance(reference, ReferenceInfo):
182
+ raise ConfigurationError("Provenance reference must be ReferenceInfo or None.")
183
+
184
+ object.__setattr__(self, "dnakit_version", resolved_dnakit)
185
+ object.__setattr__(self, "python_version", resolved_python)
186
+ object.__setattr__(self, "platform", resolved_platform)
187
+ object.__setattr__(self, "dependency_versions", freeze_mapping(dependency_versions))
188
+ object.__setattr__(self, "implementation", resolved_implementation)
189
+ object.__setattr__(self, "backend", backend)
190
+ object.__setattr__(self, "reference", reference)
191
+
192
+ def to_dict(self) -> dict[str, Any]:
193
+ return cast(dict[str, Any], to_json_compatible(self))
194
+
195
+
196
+ @dataclass(frozen=True)
197
+ class ArtifactRef:
198
+ """Integrity-checked reference to a persisted result artifact."""
199
+
200
+ relative_path: str
201
+ media_type: str
202
+ schema_version: str
203
+ sha256: str
204
+ byte_size: int
205
+ created_at: str
206
+
207
+ def __post_init__(self) -> None:
208
+ for name in ("relative_path", "media_type", "schema_version", "created_at"):
209
+ value = getattr(self, name)
210
+ if not isinstance(value, str) or not value.strip():
211
+ raise ConfigurationError(f"Artifact {name} must be a non-empty string.")
212
+ if not isinstance(self.sha256, str) or not _SHA256_PATTERN.fullmatch(self.sha256):
213
+ raise ConfigurationError("Artifact sha256 must contain exactly 64 hexadecimal digits.")
214
+ if (
215
+ isinstance(self.byte_size, bool)
216
+ or not isinstance(self.byte_size, int)
217
+ or self.byte_size < 0
218
+ ):
219
+ raise ConfigurationError("Artifact byte_size must be a non-negative integer.")
220
+
221
+
222
+ @dataclass(frozen=True, init=False)
223
+ class RunManifest:
224
+ """Resolved parameters and artifacts for a reproducible execution."""
225
+
226
+ run_id: str
227
+ command: tuple[str, ...]
228
+ resolved_config: FrozenDict
229
+ seed: int | None
230
+ seed_derivation: str | None
231
+ inputs: tuple[ArtifactRef, ...]
232
+ outputs: tuple[ArtifactRef, ...]
233
+ provenance: Provenance
234
+ started_at: str
235
+ finished_at: str | None
236
+ status: str
237
+ issues: tuple[Issue, ...]
238
+
239
+ def __init__(
240
+ self,
241
+ run_id: str,
242
+ command: Iterable[str],
243
+ resolved_config: Mapping[str, object],
244
+ provenance: Provenance,
245
+ started_at: str,
246
+ status: str,
247
+ *,
248
+ seed: int | None = None,
249
+ seed_derivation: str | None = None,
250
+ inputs: Iterable[ArtifactRef] = (),
251
+ outputs: Iterable[ArtifactRef] = (),
252
+ finished_at: str | None = None,
253
+ issues: Iterable[Issue] = (),
254
+ ) -> None:
255
+ for field_name, value in (
256
+ ("run_id", run_id),
257
+ ("started_at", started_at),
258
+ ("status", status),
259
+ ):
260
+ if not isinstance(value, str) or not value.strip():
261
+ raise ConfigurationError(f"RunManifest {field_name} must be non-empty.")
262
+ _non_empty_optional(seed_derivation, "RunManifest seed_derivation")
263
+ _non_empty_optional(finished_at, "RunManifest finished_at")
264
+ if seed is not None and (isinstance(seed, bool) or not isinstance(seed, int)):
265
+ raise ConfigurationError("RunManifest seed must be an integer or None.")
266
+ command_tuple = tuple(command)
267
+ if not command_tuple or any(
268
+ not isinstance(argument, str) or not argument for argument in command_tuple
269
+ ):
270
+ raise ConfigurationError("RunManifest command must contain non-empty strings.")
271
+ input_tuple = tuple(inputs)
272
+ output_tuple = tuple(outputs)
273
+ issue_tuple = tuple(issues)
274
+ if any(not isinstance(item, ArtifactRef) for item in (*input_tuple, *output_tuple)):
275
+ raise ConfigurationError("RunManifest artifacts must be ArtifactRef objects.")
276
+ if any(not isinstance(issue, Issue) for issue in issue_tuple):
277
+ raise ConfigurationError("RunManifest issues must be Issue objects.")
278
+ if not isinstance(provenance, Provenance):
279
+ raise ConfigurationError("RunManifest provenance must be Provenance.")
280
+
281
+ object.__setattr__(self, "run_id", run_id)
282
+ object.__setattr__(self, "command", command_tuple)
283
+ object.__setattr__(self, "resolved_config", freeze_mapping(resolved_config))
284
+ object.__setattr__(self, "seed", seed)
285
+ object.__setattr__(self, "seed_derivation", seed_derivation)
286
+ object.__setattr__(self, "inputs", input_tuple)
287
+ object.__setattr__(self, "outputs", output_tuple)
288
+ object.__setattr__(self, "provenance", provenance)
289
+ object.__setattr__(self, "started_at", started_at)
290
+ object.__setattr__(self, "finished_at", finished_at)
291
+ object.__setattr__(self, "status", status)
292
+ object.__setattr__(self, "issues", issue_tuple)
293
+
294
+ def to_dict(self) -> dict[str, Any]:
295
+ return cast(dict[str, Any], to_json_compatible(self))
296
+
297
+
298
+ __all__ = [
299
+ "ArtifactRef",
300
+ "Citation",
301
+ "ImplementationInfo",
302
+ "Provenance",
303
+ "ReferenceInfo",
304
+ "RunManifest",
305
+ ]