reader-workbench 1.0.0__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.
- reader_workbench/__init__.py +22 -0
- reader_workbench/__main__.py +4 -0
- reader_workbench/_version.py +17 -0
- reader_workbench/api/__init__.py +74 -0
- reader_workbench/api/_record_reads.py +75 -0
- reader_workbench/api/artifacts.py +79 -0
- reader_workbench/api/facade.py +538 -0
- reader_workbench/api/models.py +285 -0
- reader_workbench/api/notebooks.py +63 -0
- reader_workbench/contracts/__init__.py +18 -0
- reader_workbench/contracts/builtins/__init__.py +36 -0
- reader_workbench/contracts/builtins/cytometry.py +140 -0
- reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
- reader_workbench/contracts/builtins/generic.py +21 -0
- reader_workbench/contracts/builtins/logic.py +149 -0
- reader_workbench/contracts/builtins/plate_reader.py +47 -0
- reader_workbench/contracts/catalog.py +257 -0
- reader_workbench/contracts/model.py +109 -0
- reader_workbench/domains/__init__.py +1 -0
- reader_workbench/domains/cytometry/__init__.py +3 -0
- reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
- reader_workbench/domains/cytometry/analysis/events.py +182 -0
- reader_workbench/domains/cytometry/analysis/gating.py +175 -0
- reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
- reader_workbench/domains/cytometry/io/__init__.py +3 -0
- reader_workbench/domains/cytometry/io/fcs.py +135 -0
- reader_workbench/domains/cytometry/plots/__init__.py +5 -0
- reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
- reader_workbench/domains/logic/__init__.py +3 -0
- reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
- reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
- reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
- reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
- reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
- reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
- reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
- reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
- reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
- reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
- reader_workbench/domains/logic/four_state_vector/config.py +214 -0
- reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
- reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
- reader_workbench/domains/logic/four_state_vector/math.py +191 -0
- reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
- reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
- reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
- reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
- reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
- reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
- reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
- reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
- reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
- reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
- reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
- reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
- reader_workbench/domains/logic/treatment_columns.py +42 -0
- reader_workbench/domains/plate_reader/__init__.py +1 -0
- reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
- reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
- reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
- reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
- reader_workbench/domains/plate_reader/io/__init__.py +6 -0
- reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
- reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
- reader_workbench/domains/plate_reader/ordering.py +59 -0
- reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
- reader_workbench/domains/plate_reader/plots/_data.py +29 -0
- reader_workbench/domains/plate_reader/plots/common.py +346 -0
- reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
- reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
- reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
- reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
- reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
- reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
- reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
- reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
- reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
- reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
- reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
- reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
- reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
- reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
- reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
- reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
- reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
- reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
- reader_workbench/domains/time_series/__init__.py +29 -0
- reader_workbench/domains/time_series/aggregation.py +60 -0
- reader_workbench/domains/time_series/contracts.py +368 -0
- reader_workbench/domains/time_series/reduction.py +395 -0
- reader_workbench/errors.py +55 -0
- reader_workbench/maintenance/__init__.py +6 -0
- reader_workbench/maintenance/docs.py +335 -0
- reader_workbench/maintenance/model.py +28 -0
- reader_workbench/maintenance/release.py +39 -0
- reader_workbench/maintenance/skills.py +124 -0
- reader_workbench/plotting/__init__.py +20 -0
- reader_workbench/plotting/mpl.py +56 -0
- reader_workbench/plotting/sinks.py +69 -0
- reader_workbench/plotting/style.py +175 -0
- reader_workbench/plotting/utils.py +27 -0
- reader_workbench/plugins/__init__.py +1 -0
- reader_workbench/plugins/catalog.py +33 -0
- reader_workbench/plugins/export/__init__.py +0 -0
- reader_workbench/plugins/export/_paths.py +21 -0
- reader_workbench/plugins/export/csv.py +41 -0
- reader_workbench/plugins/export/xlsx.py +44 -0
- reader_workbench/plugins/ingest/__init__.py +0 -0
- reader_workbench/plugins/ingest/_discovery.py +58 -0
- reader_workbench/plugins/ingest/discovery_policy.py +66 -0
- reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
- reader_workbench/plugins/ingest/synergy_h1.py +234 -0
- reader_workbench/plugins/manifests/__init__.py +1 -0
- reader_workbench/plugins/manifests/export.py +29 -0
- reader_workbench/plugins/manifests/ingest.py +29 -0
- reader_workbench/plugins/manifests/plot.py +161 -0
- reader_workbench/plugins/manifests/transform.py +172 -0
- reader_workbench/plugins/manifests/validator.py +18 -0
- reader_workbench/plugins/plot/__init__.py +0 -0
- reader_workbench/plugins/plot/_shared.py +55 -0
- reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
- reader_workbench/plugins/plot/distributions.py +58 -0
- reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
- reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
- reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
- reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
- reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
- reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
- reader_workbench/plugins/plot/logic_symmetry.py +56 -0
- reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
- reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
- reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
- reader_workbench/plugins/plot/time_series.py +114 -0
- reader_workbench/plugins/plot/ts_and_snap.py +210 -0
- reader_workbench/plugins/transform/__init__.py +0 -0
- reader_workbench/plugins/transform/_four_state_vector.py +204 -0
- reader_workbench/plugins/transform/_labeling.py +109 -0
- reader_workbench/plugins/transform/alias.py +70 -0
- reader_workbench/plugins/transform/assay_labels.py +62 -0
- reader_workbench/plugins/transform/blank.py +79 -0
- reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
- reader_workbench/plugins/transform/cytometry_gating.py +120 -0
- reader_workbench/plugins/transform/fold_change.py +79 -0
- reader_workbench/plugins/transform/four_state_event_window.py +93 -0
- reader_workbench/plugins/transform/four_state_vector.py +62 -0
- reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
- reader_workbench/plugins/transform/logic_symmetry.py +67 -0
- reader_workbench/plugins/transform/outlier_filter.py +60 -0
- reader_workbench/plugins/transform/overflow.py +197 -0
- reader_workbench/plugins/transform/ratio.py +237 -0
- reader_workbench/plugins/transform/sample_map.py +170 -0
- reader_workbench/plugins/transform/sample_metadata.py +94 -0
- reader_workbench/plugins/validator/__init__.py +1 -0
- reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
- reader_workbench/protocols/__init__.py +80 -0
- reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
- reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
- reader_workbench/protocols/builtins.py +1656 -0
- reader_workbench/protocols/compiler.py +22 -0
- reader_workbench/protocols/compilers/__init__.py +1 -0
- reader_workbench/protocols/compilers/common.py +100 -0
- reader_workbench/protocols/compilers/cytometry.py +87 -0
- reader_workbench/protocols/compilers/generic.py +14 -0
- reader_workbench/protocols/compilers/logic.py +245 -0
- reader_workbench/protocols/compilers/plate_reader.py +937 -0
- reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
- reader_workbench/protocols/model.py +1486 -0
- reader_workbench/protocols/semantic_coverage.py +234 -0
- reader_workbench/runtime/__init__.py +12 -0
- reader_workbench/runtime/builtin.py +23 -0
- reader_workbench/runtime/model.py +42 -0
- reader_workbench/workbench/__init__.py +60 -0
- reader_workbench/workbench/assets/__init__.py +22 -0
- reader_workbench/workbench/assets/types.py +118 -0
- reader_workbench/workbench/audit/__init__.py +5 -0
- reader_workbench/workbench/audit/experiments.py +307 -0
- reader_workbench/workbench/audit/staging.py +187 -0
- reader_workbench/workbench/cli/__init__.py +51 -0
- reader_workbench/workbench/cli/_lazy.py +9 -0
- reader_workbench/workbench/cli/_records_view.py +150 -0
- reader_workbench/workbench/cli/_surface_execution.py +443 -0
- reader_workbench/workbench/cli/audit.py +95 -0
- reader_workbench/workbench/cli/automation.py +229 -0
- reader_workbench/workbench/cli/demo.py +46 -0
- reader_workbench/workbench/cli/dop.py +91 -0
- reader_workbench/workbench/cli/experiments.py +635 -0
- reader_workbench/workbench/cli/helpers.py +232 -0
- reader_workbench/workbench/cli/main.py +59 -0
- reader_workbench/workbench/cli/maintenance.py +82 -0
- reader_workbench/workbench/cli/notebooks.py +260 -0
- reader_workbench/workbench/cli/pagination.py +117 -0
- reader_workbench/workbench/cli/protocols.py +336 -0
- reader_workbench/workbench/cli/shared.py +309 -0
- reader_workbench/workbench/cli/surfaces.py +534 -0
- reader_workbench/workbench/cli/verification.py +128 -0
- reader_workbench/workbench/commands.py +10 -0
- reader_workbench/workbench/config/__init__.py +47 -0
- reader_workbench/workbench/config/identity.py +13 -0
- reader_workbench/workbench/config/load.py +405 -0
- reader_workbench/workbench/config/model.py +274 -0
- reader_workbench/workbench/context.py +26 -0
- reader_workbench/workbench/decl/__init__.py +31 -0
- reader_workbench/workbench/decl/build.py +190 -0
- reader_workbench/workbench/decl/model.py +81 -0
- reader_workbench/workbench/dop/__init__.py +12 -0
- reader_workbench/workbench/dop/builtins.py +261 -0
- reader_workbench/workbench/dop/model.py +209 -0
- reader_workbench/workbench/engine/__init__.py +42 -0
- reader_workbench/workbench/engine/_shared.py +76 -0
- reader_workbench/workbench/engine/contracts.py +283 -0
- reader_workbench/workbench/engine/execution.py +326 -0
- reader_workbench/workbench/engine/file_outputs.py +260 -0
- reader_workbench/workbench/engine/inputs.py +161 -0
- reader_workbench/workbench/engine/invocations.py +507 -0
- reader_workbench/workbench/engine/planning.py +72 -0
- reader_workbench/workbench/engine/runtime.py +464 -0
- reader_workbench/workbench/engine/setup.py +149 -0
- reader_workbench/workbench/engine/validation.py +684 -0
- reader_workbench/workbench/experiment/__init__.py +47 -0
- reader_workbench/workbench/experiment/model.py +381 -0
- reader_workbench/workbench/experiments.py +133 -0
- reader_workbench/workbench/graph/__init__.py +47 -0
- reader_workbench/workbench/graph/nodes.py +102 -0
- reader_workbench/workbench/graph/normalize.py +177 -0
- reader_workbench/workbench/graph/refs.py +148 -0
- reader_workbench/workbench/input_discovery.py +19 -0
- reader_workbench/workbench/inspection/__init__.py +3 -0
- reader_workbench/workbench/inspection/catalogs.py +128 -0
- reader_workbench/workbench/inspection/common.py +92 -0
- reader_workbench/workbench/inspection/dop.py +64 -0
- reader_workbench/workbench/inspection/experiments.py +449 -0
- reader_workbench/workbench/inspection/inventory.py +68 -0
- reader_workbench/workbench/inspection/protocols.py +368 -0
- reader_workbench/workbench/inspection/readiness.py +333 -0
- reader_workbench/workbench/inspection/reports.py +367 -0
- reader_workbench/workbench/inspection/results.py +166 -0
- reader_workbench/workbench/inspection/runtime.py +287 -0
- reader_workbench/workbench/inspection/semantics.py +192 -0
- reader_workbench/workbench/inspection/validation.py +30 -0
- reader_workbench/workbench/notebooks/__init__.py +17 -0
- reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
- reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
- reader_workbench/workbench/notebooks/components/__init__.py +21 -0
- reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
- reader_workbench/workbench/notebooks/components/overview.py +119 -0
- reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
- reader_workbench/workbench/notebooks/launch.py +274 -0
- reader_workbench/workbench/notebooks/presentation.py +136 -0
- reader_workbench/workbench/notebooks/scaffold.py +60 -0
- reader_workbench/workbench/ontology.py +78 -0
- reader_workbench/workbench/paths.py +44 -0
- reader_workbench/workbench/ports/__init__.py +31 -0
- reader_workbench/workbench/ports/model.py +168 -0
- reader_workbench/workbench/records/__init__.py +44 -0
- reader_workbench/workbench/records/epoch.py +329 -0
- reader_workbench/workbench/records/evidence.py +247 -0
- reader_workbench/workbench/records/identity.py +87 -0
- reader_workbench/workbench/records/locking.py +185 -0
- reader_workbench/workbench/records/model.py +711 -0
- reader_workbench/workbench/records/sources.py +73 -0
- reader_workbench/workbench/records/store.py +1022 -0
- reader_workbench/workbench/records/verification.py +998 -0
- reader_workbench/workbench/registry.py +333 -0
- reader_workbench/workbench/spec_overrides.py +215 -0
- reader_workbench-1.0.0.dist-info/METADATA +91 -0
- reader_workbench-1.0.0.dist-info/RECORD +293 -0
- reader_workbench-1.0.0.dist-info/WHEEL +5 -0
- reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
- reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
- reader_workbench-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from .model import (
|
|
2
|
+
AnnotationCollections,
|
|
3
|
+
AnnotationCollectionSpec,
|
|
4
|
+
AnnotationLabels,
|
|
5
|
+
AnnotationLabelSpec,
|
|
6
|
+
AnnotationOrders,
|
|
7
|
+
AnnotationOrderSpec,
|
|
8
|
+
AnnotationSemantics,
|
|
9
|
+
ExperimentEvidence,
|
|
10
|
+
ExperimentSemantics,
|
|
11
|
+
FileResourceEntry,
|
|
12
|
+
OrderedStateSpaces,
|
|
13
|
+
OrderedStateSpaceSpec,
|
|
14
|
+
OutputLayout,
|
|
15
|
+
RecordResourceEntry,
|
|
16
|
+
ReplicateKind,
|
|
17
|
+
ResolvedAnnotationLabelSpec,
|
|
18
|
+
ResolvedOrderedStateSpace,
|
|
19
|
+
ResolvedPlotPartition,
|
|
20
|
+
ResourceCatalog,
|
|
21
|
+
ResourceEntry,
|
|
22
|
+
ResourceKind,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AnnotationCollectionSpec",
|
|
27
|
+
"AnnotationCollections",
|
|
28
|
+
"AnnotationLabelSpec",
|
|
29
|
+
"AnnotationLabels",
|
|
30
|
+
"AnnotationOrderSpec",
|
|
31
|
+
"AnnotationOrders",
|
|
32
|
+
"AnnotationSemantics",
|
|
33
|
+
"ExperimentEvidence",
|
|
34
|
+
"ExperimentSemantics",
|
|
35
|
+
"FileResourceEntry",
|
|
36
|
+
"OrderedStateSpaceSpec",
|
|
37
|
+
"OrderedStateSpaces",
|
|
38
|
+
"OutputLayout",
|
|
39
|
+
"ResolvedAnnotationLabelSpec",
|
|
40
|
+
"ResolvedOrderedStateSpace",
|
|
41
|
+
"ResolvedPlotPartition",
|
|
42
|
+
"ResourceCatalog",
|
|
43
|
+
"ResourceEntry",
|
|
44
|
+
"ResourceKind",
|
|
45
|
+
"RecordResourceEntry",
|
|
46
|
+
"ReplicateKind",
|
|
47
|
+
]
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from reader_workbench.protocols.model import ProtocolBinding, ProtocolSemanticProgram
|
|
9
|
+
|
|
10
|
+
ResourceKind = Literal["file", "record"]
|
|
11
|
+
ReplicateKind = Literal["biological", "technical", "mixed", "unknown", "not_applicable"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ExperimentEvidence:
|
|
16
|
+
data_class: str
|
|
17
|
+
data_class_reason: str
|
|
18
|
+
replicate_kind: ReplicateKind
|
|
19
|
+
replicate_identity_field: str | None = None
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if not isinstance(self.data_class, str):
|
|
23
|
+
raise TypeError("data_class must be a string")
|
|
24
|
+
if not isinstance(self.data_class_reason, str):
|
|
25
|
+
raise TypeError("data_class_reason must be a string")
|
|
26
|
+
if not isinstance(self.replicate_kind, str):
|
|
27
|
+
raise TypeError("replicate_kind must be a string")
|
|
28
|
+
if self.replicate_identity_field is not None and not isinstance(self.replicate_identity_field, str):
|
|
29
|
+
raise TypeError("replicate_identity_field must be a string when provided")
|
|
30
|
+
data_class = self.data_class.strip()
|
|
31
|
+
data_class_reason = self.data_class_reason.strip()
|
|
32
|
+
replicate_identity_field = (
|
|
33
|
+
self.replicate_identity_field.strip() if self.replicate_identity_field is not None else None
|
|
34
|
+
)
|
|
35
|
+
if not data_class:
|
|
36
|
+
raise ValueError("data_class must be a non-empty string")
|
|
37
|
+
if not data_class_reason:
|
|
38
|
+
raise ValueError("data_class_reason must be a non-empty string")
|
|
39
|
+
if self.replicate_kind not in {"biological", "technical", "mixed", "unknown", "not_applicable"}:
|
|
40
|
+
raise ValueError(f"unsupported replicate_kind: {self.replicate_kind!r}")
|
|
41
|
+
if self.replicate_identity_field is not None and not replicate_identity_field:
|
|
42
|
+
raise ValueError("replicate_identity_field must be a non-empty string when provided")
|
|
43
|
+
if self.replicate_kind == "not_applicable" and self.replicate_identity_field is not None:
|
|
44
|
+
raise ValueError("replicate_identity_field cannot be set when replicate_kind is not_applicable")
|
|
45
|
+
object.__setattr__(self, "data_class", data_class)
|
|
46
|
+
object.__setattr__(self, "data_class_reason", data_class_reason)
|
|
47
|
+
object.__setattr__(self, "replicate_identity_field", replicate_identity_field)
|
|
48
|
+
|
|
49
|
+
def to_payload(self) -> dict[str, str | None]:
|
|
50
|
+
return {
|
|
51
|
+
"data_class": self.data_class,
|
|
52
|
+
"data_class_reason": self.data_class_reason,
|
|
53
|
+
"replicate_kind": self.replicate_kind,
|
|
54
|
+
"replicate_identity_field": self.replicate_identity_field,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class AnnotationLabelSpec:
|
|
60
|
+
source: str
|
|
61
|
+
values: dict[str, str] = field(default_factory=dict)
|
|
62
|
+
output: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class ResolvedAnnotationLabelSpec:
|
|
67
|
+
ref: str
|
|
68
|
+
source: str
|
|
69
|
+
output: str | None
|
|
70
|
+
values: dict[str, str]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class AnnotationLabels:
|
|
75
|
+
by_id: dict[str, AnnotationLabelSpec] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
def resolve(self, refs: list[str] | None = None) -> list[ResolvedAnnotationLabelSpec]:
|
|
78
|
+
requested = list(self.by_id) if refs is None else refs
|
|
79
|
+
if not requested:
|
|
80
|
+
return []
|
|
81
|
+
resolved: list[ResolvedAnnotationLabelSpec] = []
|
|
82
|
+
for raw_ref in requested:
|
|
83
|
+
ref = str(raw_ref).strip()
|
|
84
|
+
if not ref:
|
|
85
|
+
raise ValueError("label refs must be non-empty strings")
|
|
86
|
+
spec = self.by_id.get(ref)
|
|
87
|
+
if spec is None:
|
|
88
|
+
raise ValueError(f"annotations.labels missing key '{ref}'")
|
|
89
|
+
resolved.append(
|
|
90
|
+
ResolvedAnnotationLabelSpec(
|
|
91
|
+
ref=ref,
|
|
92
|
+
source=spec.source,
|
|
93
|
+
output=spec.output,
|
|
94
|
+
values=dict(spec.values),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return resolved
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class AnnotationOrderSpec:
|
|
102
|
+
column: str
|
|
103
|
+
values: list[str] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class AnnotationOrders:
|
|
108
|
+
by_id: dict[str, AnnotationOrderSpec] = field(default_factory=dict)
|
|
109
|
+
|
|
110
|
+
def resolve(
|
|
111
|
+
self,
|
|
112
|
+
*,
|
|
113
|
+
order: list[str] | None,
|
|
114
|
+
order_ref: str | None,
|
|
115
|
+
column: str | None,
|
|
116
|
+
arg_name: str,
|
|
117
|
+
) -> list[str] | None:
|
|
118
|
+
if order is not None and order_ref is not None:
|
|
119
|
+
raise ValueError(f"{arg_name} and {arg_name}_ref are mutually exclusive")
|
|
120
|
+
if order is not None:
|
|
121
|
+
if not order:
|
|
122
|
+
raise ValueError(f"{arg_name} must not be empty when provided")
|
|
123
|
+
return [str(item) for item in order]
|
|
124
|
+
if order_ref is None:
|
|
125
|
+
return None
|
|
126
|
+
ref = str(order_ref).strip()
|
|
127
|
+
if not ref:
|
|
128
|
+
raise ValueError(f"{arg_name}_ref must be a non-empty string")
|
|
129
|
+
spec = self.by_id.get(ref)
|
|
130
|
+
if spec is None:
|
|
131
|
+
options = ", ".join(sorted(self.by_id)) if self.by_id else "—"
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Unknown {arg_name}_ref '{ref}'. Define it under annotations.orders.{ref}. (available: {options})"
|
|
134
|
+
)
|
|
135
|
+
if column and spec.column and str(column) != str(spec.column):
|
|
136
|
+
raise ValueError(f"{arg_name}_ref '{ref}' targets column {spec.column!r}, but plot uses column {column!r}")
|
|
137
|
+
if not spec.values:
|
|
138
|
+
raise ValueError(f"annotations.orders.{ref}.values must not be empty")
|
|
139
|
+
return [str(item) for item in spec.values]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class AnnotationCollectionSpec:
|
|
144
|
+
column: str
|
|
145
|
+
items: dict[str, list[str]] = field(default_factory=dict)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class AnnotationCollections:
|
|
150
|
+
by_id: dict[str, AnnotationCollectionSpec] = field(default_factory=dict)
|
|
151
|
+
|
|
152
|
+
def resolve(self, *, ref: str) -> dict[str, Any]:
|
|
153
|
+
collection_ref = str(ref).strip()
|
|
154
|
+
if not collection_ref:
|
|
155
|
+
raise ValueError("collection_ref must be a non-empty string")
|
|
156
|
+
spec = self.by_id.get(collection_ref)
|
|
157
|
+
if spec is None:
|
|
158
|
+
options = ", ".join(sorted(self.by_id)) if self.by_id else "—"
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Unknown collection_ref '{collection_ref}'. "
|
|
161
|
+
f"Define it under annotations.collections.{collection_ref}. (available: {options})"
|
|
162
|
+
)
|
|
163
|
+
if not spec.items:
|
|
164
|
+
raise ValueError(f"annotations.collections.{collection_ref}.items must be a non-empty mapping")
|
|
165
|
+
normalized_items: list[dict[str, list[str]]] = []
|
|
166
|
+
for label, values in spec.items.items():
|
|
167
|
+
if not values:
|
|
168
|
+
raise ValueError(f"annotations.collections.{collection_ref}.items.{label} must be a non-empty list")
|
|
169
|
+
normalized_items.append({str(label): [str(value) for value in values]})
|
|
170
|
+
return {"column": spec.column, "items": normalized_items}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class OrderedStateSpaceSpec:
|
|
175
|
+
column: str
|
|
176
|
+
state_order: tuple[str, ...]
|
|
177
|
+
source_values: dict[str, str]
|
|
178
|
+
case_sensitive: bool = True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class ResolvedOrderedStateSpace:
|
|
183
|
+
ref: str
|
|
184
|
+
column: str
|
|
185
|
+
state_ids: tuple[str, ...]
|
|
186
|
+
source_values: dict[str, str]
|
|
187
|
+
case_sensitive: bool
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class OrderedStateSpaces:
|
|
192
|
+
by_id: dict[str, OrderedStateSpaceSpec] = field(default_factory=dict)
|
|
193
|
+
|
|
194
|
+
def resolve(self, *, ref: str) -> ResolvedOrderedStateSpace:
|
|
195
|
+
state_map_ref = str(ref).strip()
|
|
196
|
+
if not state_map_ref:
|
|
197
|
+
raise ValueError("state_map_ref must be a non-empty string")
|
|
198
|
+
spec = self.by_id.get(state_map_ref)
|
|
199
|
+
if spec is None:
|
|
200
|
+
options = ", ".join(sorted(self.by_id)) if self.by_id else "—"
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f"Unknown state_map_ref '{state_map_ref}'. "
|
|
203
|
+
f"Define it under annotations.ordered_state_spaces.{state_map_ref}. (available: {options})"
|
|
204
|
+
)
|
|
205
|
+
if not isinstance(spec.column, str) or not spec.column.strip():
|
|
206
|
+
raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref}.column must be a non-empty string")
|
|
207
|
+
state_ids = tuple(str(state_id).strip() for state_id in spec.state_order)
|
|
208
|
+
if not state_ids:
|
|
209
|
+
raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref}.state_order must not be empty")
|
|
210
|
+
if any(not state_id for state_id in state_ids):
|
|
211
|
+
raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref} state ids must be non-empty")
|
|
212
|
+
if len(set(state_ids)) != len(state_ids):
|
|
213
|
+
raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref} state ids must be unique")
|
|
214
|
+
if set(spec.source_values) != set(state_ids):
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"annotations.ordered_state_spaces.{state_map_ref}.values must have exactly the ids in state_order"
|
|
217
|
+
)
|
|
218
|
+
source_values = tuple(str(spec.source_values[state_id]) for state_id in state_ids)
|
|
219
|
+
if any(not source_value.strip() for source_value in source_values):
|
|
220
|
+
raise ValueError(f"annotations.ordered_state_spaces.{state_map_ref} source values must be non-empty")
|
|
221
|
+
comparison_values = (
|
|
222
|
+
source_values if spec.case_sensitive else tuple(value.strip().casefold() for value in source_values)
|
|
223
|
+
)
|
|
224
|
+
if len(set(comparison_values)) != len(comparison_values):
|
|
225
|
+
sensitivity = "true" if spec.case_sensitive else "false"
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"annotations.ordered_state_spaces.{state_map_ref} source values must be unique "
|
|
228
|
+
f"under case_sensitive={sensitivity}"
|
|
229
|
+
)
|
|
230
|
+
return ResolvedOrderedStateSpace(
|
|
231
|
+
ref=state_map_ref,
|
|
232
|
+
column=spec.column,
|
|
233
|
+
state_ids=state_ids,
|
|
234
|
+
source_values=dict(zip(state_ids, source_values, strict=True)),
|
|
235
|
+
case_sensitive=bool(spec.case_sensitive),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass(frozen=True)
|
|
240
|
+
class ResolvedPlotPartition:
|
|
241
|
+
group_by: str | None
|
|
242
|
+
collection_items: list[dict[str, list[str]]] | None
|
|
243
|
+
match: str
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass(frozen=True)
|
|
247
|
+
class AnnotationSemantics:
|
|
248
|
+
labels: AnnotationLabels = field(default_factory=AnnotationLabels)
|
|
249
|
+
orders: AnnotationOrders = field(default_factory=AnnotationOrders)
|
|
250
|
+
collections: AnnotationCollections = field(default_factory=AnnotationCollections)
|
|
251
|
+
ordered_state_spaces: OrderedStateSpaces = field(default_factory=OrderedStateSpaces)
|
|
252
|
+
|
|
253
|
+
def resolve_label_specs(self, refs: list[str] | None = None) -> list[ResolvedAnnotationLabelSpec]:
|
|
254
|
+
return self.labels.resolve(refs)
|
|
255
|
+
|
|
256
|
+
def resolve_order_arg(
|
|
257
|
+
self,
|
|
258
|
+
*,
|
|
259
|
+
order: list[str] | None,
|
|
260
|
+
order_ref: str | None,
|
|
261
|
+
column: str | None,
|
|
262
|
+
arg_name: str,
|
|
263
|
+
) -> list[str] | None:
|
|
264
|
+
return self.orders.resolve(order=order, order_ref=order_ref, column=column, arg_name=arg_name)
|
|
265
|
+
|
|
266
|
+
def resolve_ordered_state_space(self, *, ref: str) -> ResolvedOrderedStateSpace:
|
|
267
|
+
return self.ordered_state_spaces.resolve(ref=ref)
|
|
268
|
+
|
|
269
|
+
def resolve_plot_partition(self, *, partition: dict[str, Any] | Any | None) -> ResolvedPlotPartition:
|
|
270
|
+
if partition is None:
|
|
271
|
+
return ResolvedPlotPartition(group_by=None, collection_items=None, match="exact")
|
|
272
|
+
if hasattr(partition, "model_dump"):
|
|
273
|
+
partition = partition.model_dump()
|
|
274
|
+
if not isinstance(partition, dict):
|
|
275
|
+
raise ValueError("partition must resolve to a mapping")
|
|
276
|
+
group_by_raw = partition.get("by")
|
|
277
|
+
if group_by_raw is not None and (not isinstance(group_by_raw, str) or not group_by_raw.strip()):
|
|
278
|
+
raise ValueError("partition.by must be a non-empty string when provided")
|
|
279
|
+
group_by = str(group_by_raw).strip() if isinstance(group_by_raw, str) else None
|
|
280
|
+
collection_ref_raw = partition.get("collection_ref")
|
|
281
|
+
if collection_ref_raw is not None and (
|
|
282
|
+
not isinstance(collection_ref_raw, str) or not collection_ref_raw.strip()
|
|
283
|
+
):
|
|
284
|
+
raise ValueError("partition.collection_ref must be a non-empty string when provided")
|
|
285
|
+
collection_ref = str(collection_ref_raw).strip() if isinstance(collection_ref_raw, str) else None
|
|
286
|
+
match = partition.get("match", "exact")
|
|
287
|
+
valid_match = {"exact", "contains", "startswith", "endswith", "regex"}
|
|
288
|
+
if not isinstance(match, str) or match not in valid_match:
|
|
289
|
+
raise ValueError(f"partition.match must be one of {sorted(valid_match)}")
|
|
290
|
+
if collection_ref is None:
|
|
291
|
+
return ResolvedPlotPartition(group_by=group_by, collection_items=None, match=match)
|
|
292
|
+
collection = self.collections.resolve(ref=collection_ref)
|
|
293
|
+
collection_column = collection["column"]
|
|
294
|
+
if group_by is not None and group_by != collection_column:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"partition.collection_ref '{collection_ref}' targets column {collection_column!r}, "
|
|
297
|
+
f"but partition.by uses column {group_by!r}"
|
|
298
|
+
)
|
|
299
|
+
return ResolvedPlotPartition(
|
|
300
|
+
group_by=collection_column,
|
|
301
|
+
collection_items=collection["items"],
|
|
302
|
+
match=match,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@dataclass(frozen=True)
|
|
307
|
+
class FileResourceEntry:
|
|
308
|
+
kind: Literal["file"]
|
|
309
|
+
path: Path
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@dataclass(frozen=True)
|
|
313
|
+
class RecordResourceEntry:
|
|
314
|
+
kind: Literal["record"]
|
|
315
|
+
experiment_id: str
|
|
316
|
+
record_id: str
|
|
317
|
+
experiment_root: Path
|
|
318
|
+
outputs_dir: Path
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
ResourceEntry = FileResourceEntry | RecordResourceEntry
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass(frozen=True)
|
|
325
|
+
class ResourceCatalog:
|
|
326
|
+
by_id: dict[str, ResourceEntry] = field(default_factory=dict)
|
|
327
|
+
|
|
328
|
+
def get(self, resource_id: str) -> ResourceEntry | None:
|
|
329
|
+
return self.by_id.get(resource_id)
|
|
330
|
+
|
|
331
|
+
def require(self, resource_id: str) -> ResourceEntry:
|
|
332
|
+
resource = self.get(resource_id)
|
|
333
|
+
if resource is None:
|
|
334
|
+
options = ", ".join(sorted(self.by_id)) if self.by_id else "—"
|
|
335
|
+
raise ValueError(f"Unknown resource '{resource_id}'. Declare it under resources. (available: {options})")
|
|
336
|
+
return resource
|
|
337
|
+
|
|
338
|
+
def require_file(self, resource_id: str) -> FileResourceEntry:
|
|
339
|
+
resource = self.require(resource_id)
|
|
340
|
+
if resource.kind != "file":
|
|
341
|
+
raise ValueError(f"Resource '{resource_id}' has kind '{resource.kind}', expected file")
|
|
342
|
+
return resource
|
|
343
|
+
|
|
344
|
+
def require_record(self, resource_id: str) -> RecordResourceEntry:
|
|
345
|
+
resource = self.require(resource_id)
|
|
346
|
+
if resource.kind != "record":
|
|
347
|
+
raise ValueError(f"Resource '{resource_id}' has kind '{resource.kind}', expected record")
|
|
348
|
+
return resource
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@dataclass(frozen=True)
|
|
352
|
+
class OutputLayout:
|
|
353
|
+
outputs_dir: Path
|
|
354
|
+
plots_subdir: str
|
|
355
|
+
exports_subdir: str
|
|
356
|
+
notebooks_subdir: str
|
|
357
|
+
|
|
358
|
+
def subdir_path(self, key: Literal["plots", "exports", "notebooks"]) -> Path:
|
|
359
|
+
raw = {
|
|
360
|
+
"plots": self.plots_subdir,
|
|
361
|
+
"exports": self.exports_subdir,
|
|
362
|
+
"notebooks": self.notebooks_subdir,
|
|
363
|
+
}[key]
|
|
364
|
+
return self.outputs_dir if raw in ("", ".", "./") else self.outputs_dir / raw
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@dataclass(frozen=True)
|
|
368
|
+
class ExperimentSemantics:
|
|
369
|
+
protocol: ProtocolBinding
|
|
370
|
+
annotations: AnnotationSemantics
|
|
371
|
+
resources: ResourceCatalog
|
|
372
|
+
layout: OutputLayout
|
|
373
|
+
protocol_program: ProtocolSemanticProgram
|
|
374
|
+
evidence: ExperimentEvidence | None = None
|
|
375
|
+
|
|
376
|
+
def __post_init__(self) -> None:
|
|
377
|
+
if self.protocol_program.protocol != self.protocol.id:
|
|
378
|
+
raise ValueError(
|
|
379
|
+
"ExperimentSemantics.protocol_program must target the bound protocol "
|
|
380
|
+
f"{self.protocol.id!r}, got {self.protocol_program.protocol!r}."
|
|
381
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from reader_workbench.errors import ConfigError
|
|
7
|
+
from reader_workbench.workbench.config import ReaderSpec
|
|
8
|
+
from reader_workbench.workbench.paths import resolve_path_within_root
|
|
9
|
+
|
|
10
|
+
_SCAFFOLD_NAMES = frozenset({"template", "templates"})
|
|
11
|
+
_SCAFFOLD_PREFIXES = ("template_", "scaffold", "_template")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_scaffold_dir(path: Path) -> bool:
|
|
15
|
+
name = path.name.strip().lower()
|
|
16
|
+
if not name:
|
|
17
|
+
return False
|
|
18
|
+
return name in _SCAFFOLD_NAMES or any(name.startswith(prefix) for prefix in _SCAFFOLD_PREFIXES)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def discover_experiment_dirs(root: Path, *, include_scaffolds: bool = False) -> list[Path]:
|
|
22
|
+
if not root.exists() or not root.is_dir():
|
|
23
|
+
return []
|
|
24
|
+
experiment_dirs: dict[Path, Path] = {}
|
|
25
|
+
for cfg in root.glob("**/config.yaml"):
|
|
26
|
+
relative = cfg.relative_to(root)
|
|
27
|
+
if "outputs" in relative.parts:
|
|
28
|
+
continue
|
|
29
|
+
exp_dir = cfg.parent.resolve()
|
|
30
|
+
try:
|
|
31
|
+
exp_dir.relative_to(root.resolve())
|
|
32
|
+
except ValueError:
|
|
33
|
+
continue
|
|
34
|
+
if not include_scaffolds and is_scaffold_dir(exp_dir):
|
|
35
|
+
continue
|
|
36
|
+
experiment_dirs[exp_dir] = cfg.resolve()
|
|
37
|
+
return sorted(experiment_dirs)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def discover_experiment_configs(root: Path, *, include_scaffolds: bool = False) -> list[Path]:
|
|
41
|
+
return [exp_dir / "config.yaml" for exp_dir in discover_experiment_dirs(root, include_scaffolds=include_scaffolds)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ExperimentLocation:
|
|
46
|
+
id: str
|
|
47
|
+
root: Path
|
|
48
|
+
config_path: Path
|
|
49
|
+
outputs_dir: Path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ExperimentCatalog:
|
|
53
|
+
"""Resolve experiment identities without assuming year or directory naming."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, root: Path) -> None:
|
|
56
|
+
self.root = Path(root).expanduser().resolve()
|
|
57
|
+
if not self.root.is_dir():
|
|
58
|
+
raise ConfigError("The experiments workspace is missing or is not a directory")
|
|
59
|
+
self._locations: dict[str, list[ExperimentLocation]] | None = None
|
|
60
|
+
self._invalid_config_count = 0
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_experiment_root(cls, experiment_root: Path) -> ExperimentCatalog:
|
|
64
|
+
return cls(find_experiments_root(experiment_root))
|
|
65
|
+
|
|
66
|
+
def resolve(self, experiment_id: str) -> ExperimentLocation:
|
|
67
|
+
identity = _safe_experiment_id(experiment_id)
|
|
68
|
+
if self._locations is None:
|
|
69
|
+
self._locations = self._build_index()
|
|
70
|
+
matches = self._locations.get(identity, [])
|
|
71
|
+
if len(matches) == 1:
|
|
72
|
+
return matches[0]
|
|
73
|
+
if len(matches) > 1:
|
|
74
|
+
rendered = ", ".join(str(item.config_path.relative_to(self.root)) for item in matches)
|
|
75
|
+
raise ConfigError(f"Experiment id {identity!r} is ambiguous in the experiments workspace: {rendered}")
|
|
76
|
+
suffix = (
|
|
77
|
+
f" ({self._invalid_config_count} invalid config(s) were ignored.)" if self._invalid_config_count else ""
|
|
78
|
+
)
|
|
79
|
+
raise ConfigError(f"Unknown experiment id {identity!r} in the experiments workspace.{suffix}")
|
|
80
|
+
|
|
81
|
+
def _build_index(self) -> dict[str, list[ExperimentLocation]]:
|
|
82
|
+
locations: dict[str, list[ExperimentLocation]] = {}
|
|
83
|
+
for config_path in discover_experiment_configs(self.root):
|
|
84
|
+
try:
|
|
85
|
+
spec = ReaderSpec.load(config_path)
|
|
86
|
+
except ConfigError:
|
|
87
|
+
self._invalid_config_count += 1
|
|
88
|
+
continue
|
|
89
|
+
experiment_root = config_path.parent.resolve()
|
|
90
|
+
try:
|
|
91
|
+
outputs_dir = resolve_path_within_root(spec.paths.outputs, root=experiment_root)
|
|
92
|
+
except ValueError:
|
|
93
|
+
self._invalid_config_count += 1
|
|
94
|
+
continue
|
|
95
|
+
locations.setdefault(spec.experiment.id, []).append(
|
|
96
|
+
ExperimentLocation(
|
|
97
|
+
id=spec.experiment.id,
|
|
98
|
+
root=experiment_root,
|
|
99
|
+
config_path=config_path.resolve(),
|
|
100
|
+
outputs_dir=outputs_dir,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
return locations
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_experiments_root(experiment_root: Path) -> Path:
|
|
107
|
+
resolved = Path(experiment_root).expanduser().resolve()
|
|
108
|
+
for candidate in (resolved, *resolved.parents):
|
|
109
|
+
if candidate.name == "experiments" and candidate.is_dir():
|
|
110
|
+
return candidate
|
|
111
|
+
raise ConfigError(
|
|
112
|
+
f"Experiment {resolved} is not inside a canonical experiments/ workspace; "
|
|
113
|
+
"cross-experiment record resources require that ownership boundary."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _safe_experiment_id(value: str) -> str:
|
|
118
|
+
if not isinstance(value, str) or not value.strip():
|
|
119
|
+
raise ConfigError("Experiment id must be a non-empty string")
|
|
120
|
+
identity = value.strip()
|
|
121
|
+
if Path(identity).name != identity or identity in {".", ".."}:
|
|
122
|
+
raise ConfigError(f"Experiment id must be one safe path segment: {identity!r}")
|
|
123
|
+
return identity
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = [
|
|
127
|
+
"ExperimentCatalog",
|
|
128
|
+
"ExperimentLocation",
|
|
129
|
+
"discover_experiment_configs",
|
|
130
|
+
"discover_experiment_dirs",
|
|
131
|
+
"find_experiments_root",
|
|
132
|
+
"is_scaffold_dir",
|
|
133
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from .nodes import PluginStep, RecipeSource, Workbench, ensure_unique_workbench_ids
|
|
2
|
+
from .normalize import materialize_workbench, normalize_input_binding, resolve_workbench, select_workbench_specs
|
|
3
|
+
from .refs import (
|
|
4
|
+
FileRef,
|
|
5
|
+
InputRef,
|
|
6
|
+
OutputRef,
|
|
7
|
+
ProvenanceInput,
|
|
8
|
+
RecordCollectionRef,
|
|
9
|
+
RecordRef,
|
|
10
|
+
ResourceRef,
|
|
11
|
+
SourceRecordRef,
|
|
12
|
+
input_ref_display,
|
|
13
|
+
input_ref_from_dict,
|
|
14
|
+
input_ref_to_dict,
|
|
15
|
+
output_ref_display,
|
|
16
|
+
output_ref_from_dict,
|
|
17
|
+
output_ref_to_dict,
|
|
18
|
+
provenance_input_from_dict,
|
|
19
|
+
provenance_input_to_dict,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"FileRef",
|
|
24
|
+
"InputRef",
|
|
25
|
+
"OutputRef",
|
|
26
|
+
"PluginStep",
|
|
27
|
+
"ProvenanceInput",
|
|
28
|
+
"RecordCollectionRef",
|
|
29
|
+
"RecipeSource",
|
|
30
|
+
"RecordRef",
|
|
31
|
+
"ResourceRef",
|
|
32
|
+
"SourceRecordRef",
|
|
33
|
+
"Workbench",
|
|
34
|
+
"ensure_unique_workbench_ids",
|
|
35
|
+
"input_ref_display",
|
|
36
|
+
"input_ref_from_dict",
|
|
37
|
+
"input_ref_to_dict",
|
|
38
|
+
"materialize_workbench",
|
|
39
|
+
"normalize_input_binding",
|
|
40
|
+
"output_ref_display",
|
|
41
|
+
"output_ref_from_dict",
|
|
42
|
+
"output_ref_to_dict",
|
|
43
|
+
"provenance_input_from_dict",
|
|
44
|
+
"provenance_input_to_dict",
|
|
45
|
+
"resolve_workbench",
|
|
46
|
+
"select_workbench_specs",
|
|
47
|
+
]
|