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
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
|
+
]
|