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,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from reader_workbench.contracts import ContractCatalog
|
|
10
|
+
from reader_workbench.errors import ContractError, ExecutionError
|
|
11
|
+
from reader_workbench.workbench.graph import OutputRef
|
|
12
|
+
from reader_workbench.workbench.ports import InputPortSpec, OutputPortSpec
|
|
13
|
+
from reader_workbench.workbench.records import PathDescription, SourceRecordCollection
|
|
14
|
+
from reader_workbench.workbench.registry import Plugin
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _assert_input_ports(
|
|
18
|
+
plugin: Plugin,
|
|
19
|
+
inputs: dict[str, Any],
|
|
20
|
+
*,
|
|
21
|
+
contracts: ContractCatalog,
|
|
22
|
+
where: str,
|
|
23
|
+
) -> None:
|
|
24
|
+
declared = plugin.input_ports()
|
|
25
|
+
allowed = set(declared)
|
|
26
|
+
for name, port in declared.items():
|
|
27
|
+
if name not in inputs:
|
|
28
|
+
if port.optional:
|
|
29
|
+
continue
|
|
30
|
+
raise ExecutionError(f"[{where}] input '{name}' is required by plugin but not provided in 'reads'")
|
|
31
|
+
value = inputs[name]
|
|
32
|
+
_assert_input_port_value(
|
|
33
|
+
name=name,
|
|
34
|
+
port=port,
|
|
35
|
+
value=value,
|
|
36
|
+
contracts=contracts,
|
|
37
|
+
where=where,
|
|
38
|
+
)
|
|
39
|
+
extra = sorted(set(inputs) - allowed)
|
|
40
|
+
if extra:
|
|
41
|
+
raise ExecutionError(f"[{where}] unexpected inputs provided: {extra} (allowed: {sorted(allowed)})")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _assert_output_ports(
|
|
45
|
+
output_ports: Mapping[str, OutputPortSpec],
|
|
46
|
+
outputs: dict[str, Any],
|
|
47
|
+
*,
|
|
48
|
+
contracts: ContractCatalog,
|
|
49
|
+
where: str,
|
|
50
|
+
) -> None:
|
|
51
|
+
if set(outputs) != set(output_ports):
|
|
52
|
+
raise ExecutionError(f"[{where}] plugin must emit outputs {sorted(output_ports)} but emitted {sorted(outputs)}")
|
|
53
|
+
for name, port in output_ports.items():
|
|
54
|
+
value = outputs[name]
|
|
55
|
+
if port.kind == "dataframe":
|
|
56
|
+
if not isinstance(value, pd.DataFrame):
|
|
57
|
+
raise ExecutionError(
|
|
58
|
+
f"[{where}] output '{name}' must be a DataFrame for dataframe port kind, got {type(value).__name__}"
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
contracts.validate(value, contract_id=port.contract or "", where=where)
|
|
62
|
+
except ContractError as err:
|
|
63
|
+
raise ExecutionError(str(err)) from err
|
|
64
|
+
continue
|
|
65
|
+
if isinstance(value, pd.DataFrame):
|
|
66
|
+
msg = f"[{where}] output '{name}' is declared as {port.kind} but returned a DataFrame"
|
|
67
|
+
raise ExecutionError(msg)
|
|
68
|
+
_coerce_file_output(port=port, value=value, where=where, name=name)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_runtime_output_ports(
|
|
72
|
+
plugin: Plugin,
|
|
73
|
+
*,
|
|
74
|
+
inputs: Mapping[str, Any],
|
|
75
|
+
outputs: Mapping[str, Any],
|
|
76
|
+
cfg: Any,
|
|
77
|
+
contracts: ContractCatalog,
|
|
78
|
+
where: str,
|
|
79
|
+
) -> dict[str, OutputPortSpec]:
|
|
80
|
+
declared = dict(plugin.output_ports())
|
|
81
|
+
resolved = dict(plugin.resolve_output_ports(inputs=inputs, outputs=outputs, cfg=cfg, where=where))
|
|
82
|
+
if set(resolved) != set(declared):
|
|
83
|
+
raise ExecutionError(
|
|
84
|
+
f"[{where}] runtime output ports must match declared outputs: "
|
|
85
|
+
f"declared={sorted(declared)} resolved={sorted(resolved)}"
|
|
86
|
+
)
|
|
87
|
+
for name, declared_port in declared.items():
|
|
88
|
+
resolved_port = resolved[name]
|
|
89
|
+
if resolved_port.kind != declared_port.kind:
|
|
90
|
+
raise ExecutionError(
|
|
91
|
+
f"[{where}] runtime output port '{name}' changed kind from {declared_port.kind!r} "
|
|
92
|
+
f"to {resolved_port.kind!r}"
|
|
93
|
+
)
|
|
94
|
+
if declared_port.kind != "dataframe":
|
|
95
|
+
if resolved_port.contract is not None:
|
|
96
|
+
raise ExecutionError(
|
|
97
|
+
f"[{where}] runtime output port '{name}' of kind {declared_port.kind!r} "
|
|
98
|
+
"must not resolve a dataframe contract"
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
if declared_port.contract is None or resolved_port.contract is None:
|
|
102
|
+
raise ExecutionError(f"[{where}] dataframe output port '{name}' must resolve a non-empty contract id")
|
|
103
|
+
try:
|
|
104
|
+
if contracts.satisfies(actual=resolved_port.contract, expected=declared_port.contract):
|
|
105
|
+
continue
|
|
106
|
+
except ContractError as err:
|
|
107
|
+
raise ExecutionError(str(err)) from err
|
|
108
|
+
raise ExecutionError(
|
|
109
|
+
f"[{where}] runtime contract for output '{name}' must satisfy declared contract "
|
|
110
|
+
f"{declared_port.contract!r}, got {resolved_port.contract!r}"
|
|
111
|
+
)
|
|
112
|
+
return resolved
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _resolve_output_labels(
|
|
116
|
+
*,
|
|
117
|
+
step_id: str,
|
|
118
|
+
output_ports: Mapping[str, OutputPortSpec],
|
|
119
|
+
writes: dict[str, OutputRef],
|
|
120
|
+
) -> dict[str, OutputRef]:
|
|
121
|
+
unknown = sorted(set(writes) - set(output_ports))
|
|
122
|
+
if unknown:
|
|
123
|
+
raise ExecutionError(
|
|
124
|
+
f"[{step_id}] writes includes unknown outputs: {unknown} (expected: {sorted(output_ports)})"
|
|
125
|
+
)
|
|
126
|
+
labels: dict[str, OutputRef] = {}
|
|
127
|
+
for out_name, port in output_ports.items():
|
|
128
|
+
if port.kind != "dataframe":
|
|
129
|
+
if out_name in writes:
|
|
130
|
+
raise ExecutionError(
|
|
131
|
+
f"[{step_id}] writes cannot target output '{out_name}' (port kind is {port.kind!r})."
|
|
132
|
+
)
|
|
133
|
+
continue
|
|
134
|
+
label = writes[out_name] if out_name in writes else OutputRef(record_id=f"{step_id}/{out_name}")
|
|
135
|
+
if not isinstance(label, OutputRef) or not label.record_id.strip():
|
|
136
|
+
raise ExecutionError(f"[{step_id}] writes for '{out_name}' must be a non-empty record ref.")
|
|
137
|
+
labels[out_name] = label
|
|
138
|
+
if len({ref.record_id for ref in labels.values()}) != len(labels):
|
|
139
|
+
rendered = {name: ref.record_id for name, ref in labels.items()}
|
|
140
|
+
raise ExecutionError(f"[{step_id}] writes produce duplicate output labels: {rendered}")
|
|
141
|
+
return labels
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def collect_file_output_paths(
|
|
145
|
+
*,
|
|
146
|
+
output_ports: Mapping[str, OutputPortSpec],
|
|
147
|
+
outputs: Mapping[str, Any],
|
|
148
|
+
where: str,
|
|
149
|
+
) -> list[Path]:
|
|
150
|
+
collected: list[Path] = []
|
|
151
|
+
for name, port in output_ports.items():
|
|
152
|
+
if port.kind == "dataframe":
|
|
153
|
+
continue
|
|
154
|
+
collected.extend(_coerce_file_output(port=port, value=outputs[name], where=where, name=name))
|
|
155
|
+
return collected
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def collect_file_output_descriptions(
|
|
159
|
+
*,
|
|
160
|
+
output_ports: Mapping[str, OutputPortSpec],
|
|
161
|
+
outputs: Mapping[str, Any],
|
|
162
|
+
) -> list[PathDescription]:
|
|
163
|
+
descriptions: list[PathDescription] = []
|
|
164
|
+
for name, port in output_ports.items():
|
|
165
|
+
if port.kind == "dataframe":
|
|
166
|
+
continue
|
|
167
|
+
value = outputs[name]
|
|
168
|
+
if isinstance(value, PathDescription):
|
|
169
|
+
descriptions.append(value)
|
|
170
|
+
elif isinstance(value, Sequence) and not isinstance(value, str | bytes):
|
|
171
|
+
descriptions.extend(item for item in value if isinstance(item, PathDescription))
|
|
172
|
+
return descriptions
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _assert_input_port_value(
|
|
176
|
+
*,
|
|
177
|
+
name: str,
|
|
178
|
+
port: InputPortSpec,
|
|
179
|
+
value: Any,
|
|
180
|
+
contracts: ContractCatalog,
|
|
181
|
+
where: str,
|
|
182
|
+
) -> None:
|
|
183
|
+
if port.kind == "dataframe":
|
|
184
|
+
actual_contract = getattr(value, "contract_id", None)
|
|
185
|
+
if actual_contract is None:
|
|
186
|
+
raise ExecutionError(
|
|
187
|
+
f"[{where}] input '{name}' expects a dataframe artifact but got {type(value).__name__}"
|
|
188
|
+
)
|
|
189
|
+
if port.contract is None:
|
|
190
|
+
return
|
|
191
|
+
try:
|
|
192
|
+
if contracts.satisfies(actual=actual_contract, expected=port.contract):
|
|
193
|
+
return
|
|
194
|
+
except ContractError as err:
|
|
195
|
+
msg = str(err)
|
|
196
|
+
else:
|
|
197
|
+
msg = f"[{where}] input '{name}' must be contract {port.contract} but got {actual_contract}"
|
|
198
|
+
raise ExecutionError(msg)
|
|
199
|
+
if port.kind == "record_collection":
|
|
200
|
+
if not isinstance(value, SourceRecordCollection):
|
|
201
|
+
raise ExecutionError(
|
|
202
|
+
f"[{where}] input '{name}' expects a source record collection but got {type(value).__name__}"
|
|
203
|
+
)
|
|
204
|
+
for item in value:
|
|
205
|
+
actual_contract = getattr(item.record, "contract_id", None)
|
|
206
|
+
if actual_contract is None:
|
|
207
|
+
raise ExecutionError(
|
|
208
|
+
f"[{where}] input '{name}' source {item.ref.resource_id!r} is not a dataframe artifact"
|
|
209
|
+
)
|
|
210
|
+
if port.contract is None:
|
|
211
|
+
continue
|
|
212
|
+
try:
|
|
213
|
+
compatible = contracts.satisfies(actual=actual_contract, expected=port.contract)
|
|
214
|
+
except ContractError as err:
|
|
215
|
+
raise ExecutionError(str(err)) from err
|
|
216
|
+
if not compatible:
|
|
217
|
+
raise ExecutionError(
|
|
218
|
+
f"[{where}] input '{name}' source {item.ref.resource_id!r} must satisfy "
|
|
219
|
+
f"contract {port.contract!r} but got {actual_contract!r}"
|
|
220
|
+
)
|
|
221
|
+
return
|
|
222
|
+
if port.kind == "file_path":
|
|
223
|
+
if isinstance(value, Path):
|
|
224
|
+
return
|
|
225
|
+
raise ExecutionError(f"[{where}] input '{name}' expects a file path but got {type(value).__name__}")
|
|
226
|
+
if port.kind == "file_set":
|
|
227
|
+
if isinstance(value, tuple) and value and all(isinstance(item, Path) for item in value):
|
|
228
|
+
return
|
|
229
|
+
raise ExecutionError(
|
|
230
|
+
f"[{where}] input '{name}' expects a non-empty tuple of file paths but got {type(value).__name__}"
|
|
231
|
+
)
|
|
232
|
+
if port.kind == "file_bundle":
|
|
233
|
+
files = getattr(value, "files", None)
|
|
234
|
+
if files is None:
|
|
235
|
+
raise ExecutionError(f"[{where}] input '{name}' expects a file bundle but got {type(value).__name__}")
|
|
236
|
+
return
|
|
237
|
+
raise ExecutionError(f"[{where}] input '{name}' uses unknown port kind {port.kind!r}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _coerce_file_output(
|
|
241
|
+
*,
|
|
242
|
+
port: OutputPortSpec,
|
|
243
|
+
value: Any,
|
|
244
|
+
where: str,
|
|
245
|
+
name: str,
|
|
246
|
+
) -> list[Path]:
|
|
247
|
+
if port.kind == "file_path":
|
|
248
|
+
if isinstance(value, PathDescription):
|
|
249
|
+
return [value.path]
|
|
250
|
+
if isinstance(value, str | Path):
|
|
251
|
+
return [_validated_output_path(value, where=where, name=name)]
|
|
252
|
+
raise ExecutionError(f"[{where}] output '{name}' must be a path-like value for file_path ports")
|
|
253
|
+
if port.kind == "file_bundle":
|
|
254
|
+
if isinstance(value, PathDescription):
|
|
255
|
+
return [value.path]
|
|
256
|
+
if isinstance(value, str | Path):
|
|
257
|
+
return [_validated_output_path(value, where=where, name=name)]
|
|
258
|
+
if isinstance(value, Sequence) and not isinstance(value, str | bytes):
|
|
259
|
+
if not value:
|
|
260
|
+
raise ExecutionError(f"[{where}] output '{name}' file bundle must contain at least one file")
|
|
261
|
+
paths: list[Path] = []
|
|
262
|
+
for item in value:
|
|
263
|
+
if isinstance(item, PathDescription):
|
|
264
|
+
paths.append(item.path)
|
|
265
|
+
continue
|
|
266
|
+
if not isinstance(item, str | Path):
|
|
267
|
+
raise ExecutionError(
|
|
268
|
+
f"[{where}] output '{name}' file bundle entries must be path-like or PathDescription, "
|
|
269
|
+
f"got {type(item).__name__}"
|
|
270
|
+
)
|
|
271
|
+
paths.append(_validated_output_path(item, where=where, name=name))
|
|
272
|
+
return paths
|
|
273
|
+
raise ExecutionError(f"[{where}] output '{name}' must be a list of path-like values for file_bundle ports")
|
|
274
|
+
raise ExecutionError(f"[{where}] output '{name}' uses unknown non-dataframe port kind {port.kind!r}")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _validated_output_path(value: str | Path, *, where: str, name: str) -> Path:
|
|
278
|
+
if isinstance(value, str) and not value.strip():
|
|
279
|
+
raise ExecutionError(f"[{where}] output '{name}' paths must be non-empty")
|
|
280
|
+
path = Path(value)
|
|
281
|
+
if path == Path("."):
|
|
282
|
+
raise ExecutionError(f"[{where}] output '{name}' paths must identify files")
|
|
283
|
+
return path
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import nullcontext
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
10
|
+
|
|
11
|
+
from reader_workbench.errors import ExecutionError, ReaderError
|
|
12
|
+
from reader_workbench.workbench.context import RunContext
|
|
13
|
+
from reader_workbench.workbench.graph import (
|
|
14
|
+
FileRef,
|
|
15
|
+
ProvenanceInput,
|
|
16
|
+
RecordCollectionRef,
|
|
17
|
+
RecordRef,
|
|
18
|
+
ResourceRef,
|
|
19
|
+
SourceRecordRef,
|
|
20
|
+
)
|
|
21
|
+
from reader_workbench.workbench.records import PathDescription, RecordInputEvidence, RecordStore
|
|
22
|
+
|
|
23
|
+
from ._shared import digest_cfg
|
|
24
|
+
from .contracts import (
|
|
25
|
+
_assert_input_ports,
|
|
26
|
+
_assert_output_ports,
|
|
27
|
+
_resolve_output_labels,
|
|
28
|
+
_resolve_runtime_output_ports,
|
|
29
|
+
collect_file_output_descriptions,
|
|
30
|
+
collect_file_output_paths,
|
|
31
|
+
)
|
|
32
|
+
from .file_outputs import FileOutputTransaction
|
|
33
|
+
from .inputs import _resolve_inputs, resolve_missing_file_inputs
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _runtime_provenance_inputs(*, step: Any, inputs: dict[str, Any]) -> list[ProvenanceInput]:
|
|
37
|
+
provenance: list[ProvenanceInput] = []
|
|
38
|
+
declared_reads = dict(step.reads or {})
|
|
39
|
+
for label in sorted(inputs):
|
|
40
|
+
ref = declared_reads.get(label)
|
|
41
|
+
value = inputs[label]
|
|
42
|
+
if isinstance(ref, RecordCollectionRef):
|
|
43
|
+
provenance.extend(
|
|
44
|
+
ProvenanceInput(
|
|
45
|
+
label=f"{label}[{item.resource_id}]",
|
|
46
|
+
ref=item,
|
|
47
|
+
discovery_policy="source_record",
|
|
48
|
+
)
|
|
49
|
+
for item in ref.records
|
|
50
|
+
)
|
|
51
|
+
continue
|
|
52
|
+
if isinstance(value, tuple) and value and all(isinstance(item, Path) for item in value):
|
|
53
|
+
for index, path in enumerate(value):
|
|
54
|
+
item_ref = ref if ref is not None and len(value) == 1 else FileRef(path=path)
|
|
55
|
+
if isinstance(item_ref, ResourceRef):
|
|
56
|
+
policy = "declared_resource"
|
|
57
|
+
elif ref is not None:
|
|
58
|
+
policy = "declared_file"
|
|
59
|
+
else:
|
|
60
|
+
policy = "plugin_discovery"
|
|
61
|
+
provenance.append(
|
|
62
|
+
ProvenanceInput(
|
|
63
|
+
label=f"{label}[{index}]",
|
|
64
|
+
ref=item_ref,
|
|
65
|
+
discovery_policy=policy,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
continue
|
|
69
|
+
if ref is None:
|
|
70
|
+
if not isinstance(value, Path):
|
|
71
|
+
raise ExecutionError(
|
|
72
|
+
f"{step.id}: resolved input {label!r} has no declared reference and is not a file path"
|
|
73
|
+
)
|
|
74
|
+
ref = FileRef(path=value)
|
|
75
|
+
discovery_policy = "plugin_discovery"
|
|
76
|
+
elif isinstance(ref, RecordRef):
|
|
77
|
+
discovery_policy = "record"
|
|
78
|
+
elif isinstance(ref, SourceRecordRef):
|
|
79
|
+
discovery_policy = "source_record"
|
|
80
|
+
elif isinstance(ref, ResourceRef):
|
|
81
|
+
discovery_policy = "declared_resource"
|
|
82
|
+
else:
|
|
83
|
+
discovery_policy = "declared_file"
|
|
84
|
+
provenance.append(ProvenanceInput(label=label, ref=ref, discovery_policy=discovery_policy))
|
|
85
|
+
return provenance
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _persist_dataframe_outputs(
|
|
89
|
+
*,
|
|
90
|
+
store: RecordStore,
|
|
91
|
+
ctx: RunContext,
|
|
92
|
+
step: Any,
|
|
93
|
+
cfg: Any,
|
|
94
|
+
input_evidence: tuple[RecordInputEvidence, ...],
|
|
95
|
+
outputs: dict[str, Any],
|
|
96
|
+
resolved_output_ports: dict[str, Any],
|
|
97
|
+
output_labels: dict[str, Any],
|
|
98
|
+
phase: str,
|
|
99
|
+
) -> None:
|
|
100
|
+
for out_name, obj in outputs.items():
|
|
101
|
+
port = resolved_output_ports[out_name]
|
|
102
|
+
if port.kind != "dataframe":
|
|
103
|
+
continue
|
|
104
|
+
if isinstance(obj, pd.DataFrame):
|
|
105
|
+
store.persist_dataframe(
|
|
106
|
+
producer_id=step.id,
|
|
107
|
+
producer_kind="pipeline" if phase == "pipeline" else ("plot" if phase == "plots" else "export"),
|
|
108
|
+
producer_plugin=step.plugin,
|
|
109
|
+
out_name=out_name,
|
|
110
|
+
record_id=output_labels[out_name].record_id,
|
|
111
|
+
df=obj,
|
|
112
|
+
contract_id=port.contract or "",
|
|
113
|
+
inputs=input_evidence,
|
|
114
|
+
config_digest=ctx.config_digest,
|
|
115
|
+
producer_config_digest=digest_cfg(cfg),
|
|
116
|
+
source_recipe=step.source_recipe,
|
|
117
|
+
)
|
|
118
|
+
continue
|
|
119
|
+
raise ExecutionError(f"{phase} {step.id}: unsupported output type for {out_name}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _persist_file_bundle_record(
|
|
123
|
+
*,
|
|
124
|
+
store: RecordStore,
|
|
125
|
+
ctx: RunContext,
|
|
126
|
+
step: Any,
|
|
127
|
+
cfg: Any,
|
|
128
|
+
input_evidence: tuple[RecordInputEvidence, ...],
|
|
129
|
+
outputs: dict[str, Any],
|
|
130
|
+
resolved_output_ports: dict[str, Any],
|
|
131
|
+
phase: str,
|
|
132
|
+
description: str,
|
|
133
|
+
protocol_figure_description: str | None,
|
|
134
|
+
) -> None:
|
|
135
|
+
explicit_files = collect_file_output_paths(
|
|
136
|
+
output_ports=resolved_output_ports,
|
|
137
|
+
outputs=outputs,
|
|
138
|
+
where=f"{phase}:{step.id}",
|
|
139
|
+
)
|
|
140
|
+
if not explicit_files:
|
|
141
|
+
raise ExecutionError(f"{phase} {step.id}: must emit at least one explicit file output")
|
|
142
|
+
producer_kind = "plot" if phase == "plots" else "export"
|
|
143
|
+
record_files = sorted(
|
|
144
|
+
{_record_output_path(path, outputs_dir=ctx.outputs_dir) for path in explicit_files},
|
|
145
|
+
key=str,
|
|
146
|
+
)
|
|
147
|
+
path_descriptions: tuple[PathDescription, ...] = ()
|
|
148
|
+
if phase == "plots":
|
|
149
|
+
explicit_descriptions = collect_file_output_descriptions(
|
|
150
|
+
output_ports=resolved_output_ports,
|
|
151
|
+
outputs=outputs,
|
|
152
|
+
)
|
|
153
|
+
explicit_by_path: dict[Path, str] = {}
|
|
154
|
+
for item in explicit_descriptions:
|
|
155
|
+
record_path = _record_output_path(item.path, outputs_dir=ctx.outputs_dir)
|
|
156
|
+
if record_path in explicit_by_path:
|
|
157
|
+
raise ExecutionError(f"plots {step.id}: duplicate descriptions for {record_path}")
|
|
158
|
+
explicit_by_path[record_path] = item.description
|
|
159
|
+
fallback_description = protocol_figure_description or description
|
|
160
|
+
path_descriptions = tuple(
|
|
161
|
+
PathDescription(path=path, description=explicit_by_path.get(path, fallback_description))
|
|
162
|
+
for path in record_files
|
|
163
|
+
)
|
|
164
|
+
store.append_file_bundle(
|
|
165
|
+
producer_kind=producer_kind,
|
|
166
|
+
producer_id=step.id,
|
|
167
|
+
producer_plugin=step.plugin,
|
|
168
|
+
record_id=f"{producer_kind}:{step.id}",
|
|
169
|
+
inputs=input_evidence,
|
|
170
|
+
config_digest=ctx.config_digest,
|
|
171
|
+
producer_config_digest=digest_cfg(cfg),
|
|
172
|
+
files=record_files,
|
|
173
|
+
description=description,
|
|
174
|
+
path_descriptions=path_descriptions,
|
|
175
|
+
source_recipe=step.source_recipe,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _record_output_path(path: Path, *, outputs_dir: Path) -> Path:
|
|
180
|
+
try:
|
|
181
|
+
return path.relative_to(outputs_dir)
|
|
182
|
+
except ValueError:
|
|
183
|
+
return path
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _protocol_figure_description(*, ctx: RunContext, step: Any) -> str | None:
|
|
187
|
+
for figure in ctx.protocol.descriptor.figures:
|
|
188
|
+
if figure.id == step.id:
|
|
189
|
+
return figure.summary
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def execute_step(*, step: Any, phase: str, store: RecordStore, ctx: RunContext, registry: Any) -> None:
|
|
194
|
+
descriptor = registry.resolve_descriptor(step.plugin)
|
|
195
|
+
plugin_cls = descriptor.cls
|
|
196
|
+
if ctx.protocol is None:
|
|
197
|
+
raise ExecutionError(f"{phase} {step.id}: run context is missing a bound protocol")
|
|
198
|
+
effective_with = ctx.protocol.effective_plugin_config(plugin_id=step.plugin, step_with=(step.with_ or {}))
|
|
199
|
+
cfg = plugin_cls.ConfigModel.model_validate(effective_with)
|
|
200
|
+
plugin = plugin_cls()
|
|
201
|
+
plugin.bind_runtime(descriptor=descriptor, contracts=registry.contracts)
|
|
202
|
+
input_ports = plugin.input_ports()
|
|
203
|
+
output_ports = plugin.output_ports()
|
|
204
|
+
output_labels = _resolve_output_labels(
|
|
205
|
+
step_id=step.id,
|
|
206
|
+
output_ports=output_ports,
|
|
207
|
+
writes=(step.writes or {}),
|
|
208
|
+
)
|
|
209
|
+
inputs = _resolve_inputs(store, step.reads or {}, input_ports=input_ports, exp_dir=ctx.exp_dir)
|
|
210
|
+
inputs = resolve_missing_file_inputs(
|
|
211
|
+
plugin=plugin,
|
|
212
|
+
exp_dir=ctx.exp_dir,
|
|
213
|
+
cfg=cfg,
|
|
214
|
+
inputs=inputs,
|
|
215
|
+
input_ports=input_ports,
|
|
216
|
+
)
|
|
217
|
+
_assert_input_ports(
|
|
218
|
+
plugin,
|
|
219
|
+
inputs,
|
|
220
|
+
contracts=registry.contracts,
|
|
221
|
+
where=step.id,
|
|
222
|
+
)
|
|
223
|
+
provenance_inputs = _runtime_provenance_inputs(step=step, inputs=inputs)
|
|
224
|
+
input_evidence = store.capture_inputs(provenance_inputs, resolved_inputs=inputs)
|
|
225
|
+
|
|
226
|
+
plug_inputs: dict[str, Any] = {}
|
|
227
|
+
for key, value in inputs.items():
|
|
228
|
+
if hasattr(value, "load_dataframe"):
|
|
229
|
+
plug_inputs[key] = value.load_dataframe()
|
|
230
|
+
else:
|
|
231
|
+
plug_inputs[key] = value
|
|
232
|
+
|
|
233
|
+
transaction_context = (
|
|
234
|
+
FileOutputTransaction(context=ctx, step_id=step.id, phase=phase)
|
|
235
|
+
if phase in {"plots", "exports"}
|
|
236
|
+
else nullcontext()
|
|
237
|
+
)
|
|
238
|
+
with transaction_context as file_transaction:
|
|
239
|
+
plugin_context = file_transaction.context if file_transaction is not None else ctx
|
|
240
|
+
try:
|
|
241
|
+
outputs = plugin.run(plugin_context, plug_inputs, cfg)
|
|
242
|
+
except ReaderError:
|
|
243
|
+
raise
|
|
244
|
+
except Exception as err:
|
|
245
|
+
raise ExecutionError(f"{phase} {step.id} crashed: {err}") from err
|
|
246
|
+
|
|
247
|
+
resolved_output_ports = _resolve_runtime_output_ports(
|
|
248
|
+
plugin,
|
|
249
|
+
inputs=inputs,
|
|
250
|
+
outputs=outputs,
|
|
251
|
+
cfg=cfg,
|
|
252
|
+
contracts=registry.contracts,
|
|
253
|
+
where=step.id,
|
|
254
|
+
)
|
|
255
|
+
_assert_output_ports(
|
|
256
|
+
resolved_output_ports,
|
|
257
|
+
outputs,
|
|
258
|
+
contracts=registry.contracts,
|
|
259
|
+
where=step.id,
|
|
260
|
+
)
|
|
261
|
+
_persist_dataframe_outputs(
|
|
262
|
+
store=store,
|
|
263
|
+
ctx=ctx,
|
|
264
|
+
step=step,
|
|
265
|
+
cfg=cfg,
|
|
266
|
+
input_evidence=input_evidence,
|
|
267
|
+
outputs=outputs,
|
|
268
|
+
resolved_output_ports=resolved_output_ports,
|
|
269
|
+
output_labels=output_labels,
|
|
270
|
+
phase=phase,
|
|
271
|
+
)
|
|
272
|
+
if phase in {"plots", "exports"}:
|
|
273
|
+
outputs = file_transaction.promote(
|
|
274
|
+
outputs=outputs,
|
|
275
|
+
output_ports=resolved_output_ports,
|
|
276
|
+
where=f"{phase}:{step.id}",
|
|
277
|
+
)
|
|
278
|
+
protocol_figure_description = _protocol_figure_description(ctx=ctx, step=step) if phase == "plots" else None
|
|
279
|
+
description = protocol_figure_description or descriptor.summary
|
|
280
|
+
_persist_file_bundle_record(
|
|
281
|
+
store=store,
|
|
282
|
+
ctx=ctx,
|
|
283
|
+
step=step,
|
|
284
|
+
cfg=cfg,
|
|
285
|
+
input_evidence=input_evidence,
|
|
286
|
+
outputs=outputs,
|
|
287
|
+
resolved_output_ports=resolved_output_ports,
|
|
288
|
+
phase=phase,
|
|
289
|
+
description=description,
|
|
290
|
+
protocol_figure_description=protocol_figure_description,
|
|
291
|
+
)
|
|
292
|
+
file_transaction.commit()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def run_steps(
|
|
296
|
+
*,
|
|
297
|
+
items: list[Any],
|
|
298
|
+
phase: str,
|
|
299
|
+
verbose: bool,
|
|
300
|
+
console: Console,
|
|
301
|
+
ctx: RunContext,
|
|
302
|
+
store: RecordStore,
|
|
303
|
+
registry: Any,
|
|
304
|
+
) -> None:
|
|
305
|
+
if not items:
|
|
306
|
+
return
|
|
307
|
+
if verbose:
|
|
308
|
+
for ordinal, step in enumerate(items, 1):
|
|
309
|
+
ctx.logger.info("→ %s %s [%d/%d] plugin=%s", phase, step.id, ordinal, len(items), step.plugin)
|
|
310
|
+
execute_step(step=step, phase=phase, store=store, ctx=ctx, registry=registry)
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
with Progress(
|
|
314
|
+
SpinnerColumn(style="accent"),
|
|
315
|
+
TextColumn("[progress.description]{task.description}"),
|
|
316
|
+
BarColumn(),
|
|
317
|
+
TextColumn("{task.completed}/{task.total}"),
|
|
318
|
+
TimeElapsedColumn(),
|
|
319
|
+
console=console,
|
|
320
|
+
transient=True,
|
|
321
|
+
) as progress:
|
|
322
|
+
task = progress.add_task(f"{phase.title()} ({len(items)} steps)", total=len(items))
|
|
323
|
+
for step in items:
|
|
324
|
+
progress.update(task, description=f"{phase}: {step.id}")
|
|
325
|
+
execute_step(step=step, phase=phase, store=store, ctx=ctx, registry=registry)
|
|
326
|
+
progress.advance(task)
|