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,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from reader_workbench.errors import ConfigError
|
|
9
|
+
from reader_workbench.workbench.graph.refs import (
|
|
10
|
+
InputRef,
|
|
11
|
+
OutputRef,
|
|
12
|
+
input_ref_to_dict,
|
|
13
|
+
output_ref_to_dict,
|
|
14
|
+
)
|
|
15
|
+
from reader_workbench.workbench.ontology import (
|
|
16
|
+
WorkbenchItemKind,
|
|
17
|
+
WorkbenchPluginStepKind,
|
|
18
|
+
WorkbenchSurfaceSemantics,
|
|
19
|
+
get_workbench_surface_semantics,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class RecipeSource:
|
|
25
|
+
recipe: str
|
|
26
|
+
with_: dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class PluginStep:
|
|
31
|
+
kind: WorkbenchPluginStepKind
|
|
32
|
+
id: str
|
|
33
|
+
plugin: str
|
|
34
|
+
reads: dict[str, InputRef] = field(default_factory=dict)
|
|
35
|
+
with_: dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
writes: dict[str, OutputRef] = field(default_factory=dict)
|
|
37
|
+
source_recipe: RecipeSource | None = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def semantics(self) -> WorkbenchSurfaceSemantics:
|
|
41
|
+
return get_workbench_surface_semantics(self.kind)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def plugin_category(self) -> str:
|
|
45
|
+
return self.plugin.split("/", 1)[0] if "/" in self.plugin else self.plugin
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
|
48
|
+
payload = {
|
|
49
|
+
"id": self.id,
|
|
50
|
+
"plugin": self.plugin,
|
|
51
|
+
"reads": {key: input_ref_to_dict(value) for key, value in (self.reads or {}).items()},
|
|
52
|
+
"with": dict(self.with_ or {}),
|
|
53
|
+
"writes": {key: output_ref_to_dict(value) for key, value in (self.writes or {}).items()},
|
|
54
|
+
}
|
|
55
|
+
if self.source_recipe is not None:
|
|
56
|
+
payload["source_recipe"] = {
|
|
57
|
+
"recipe": self.source_recipe.recipe,
|
|
58
|
+
"with": dict(self.source_recipe.with_ or {}),
|
|
59
|
+
}
|
|
60
|
+
return payload
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
WorkbenchItem = PluginStep
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Workbench:
|
|
68
|
+
pipeline: tuple[PluginStep, ...] = ()
|
|
69
|
+
plots: tuple[PluginStep, ...] = ()
|
|
70
|
+
exports: tuple[PluginStep, ...] = ()
|
|
71
|
+
|
|
72
|
+
def by_kind(self, kind: WorkbenchItemKind) -> tuple[WorkbenchItem, ...]:
|
|
73
|
+
if kind == "pipeline":
|
|
74
|
+
return self.pipeline
|
|
75
|
+
if kind == "plot":
|
|
76
|
+
return self.plots
|
|
77
|
+
if kind == "export":
|
|
78
|
+
return self.exports
|
|
79
|
+
raise ValueError(f"Unknown workbench item kind {kind!r}")
|
|
80
|
+
|
|
81
|
+
def all_specs(self) -> tuple[WorkbenchItem, ...]:
|
|
82
|
+
return self.pipeline + self.plots + self.exports
|
|
83
|
+
|
|
84
|
+
def plugin_steps(self) -> tuple[PluginStep, ...]:
|
|
85
|
+
return self.pipeline + self.plots + self.exports
|
|
86
|
+
|
|
87
|
+
def plugin_categories(self) -> set[str]:
|
|
88
|
+
return {item.plugin_category for item in self.plugin_steps()}
|
|
89
|
+
|
|
90
|
+
def counts(self) -> dict[WorkbenchItemKind, int]:
|
|
91
|
+
return {
|
|
92
|
+
"pipeline": len(self.pipeline),
|
|
93
|
+
"plot": len(self.plots),
|
|
94
|
+
"export": len(self.exports),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def ensure_unique_workbench_ids(*collections: Sequence[WorkbenchItem]) -> None:
|
|
99
|
+
ids: list[str] = [item.id for collection in collections for item in collection if item.id]
|
|
100
|
+
dupes = sorted(item_id for item_id, count in Counter(ids).items() if count > 1)
|
|
101
|
+
if dupes:
|
|
102
|
+
raise ConfigError(f"Duplicate step/spec id(s) across pipeline/plots/exports: {dupes}")
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from reader_workbench.errors import ConfigError
|
|
7
|
+
from reader_workbench.workbench.decl import (
|
|
8
|
+
FileInputDecl,
|
|
9
|
+
PluginStepDecl,
|
|
10
|
+
RecordCollectionInputDecl,
|
|
11
|
+
RecordInputDecl,
|
|
12
|
+
RecordOutputDecl,
|
|
13
|
+
ResourceInputDecl,
|
|
14
|
+
WorkbenchDecl,
|
|
15
|
+
)
|
|
16
|
+
from reader_workbench.workbench.experiment import ResourceCatalog
|
|
17
|
+
from reader_workbench.workbench.graph.nodes import PluginStep, Workbench, ensure_unique_workbench_ids
|
|
18
|
+
from reader_workbench.workbench.graph.refs import (
|
|
19
|
+
FileRef,
|
|
20
|
+
InputRef,
|
|
21
|
+
OutputRef,
|
|
22
|
+
RecordCollectionRef,
|
|
23
|
+
RecordRef,
|
|
24
|
+
ResourceRef,
|
|
25
|
+
SourceRecordRef,
|
|
26
|
+
)
|
|
27
|
+
from reader_workbench.workbench.ontology import WorkbenchPluginStepKind, get_workbench_surface_semantics
|
|
28
|
+
from reader_workbench.workbench.paths import resolve_path_within_root
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_workbench(decl: WorkbenchDecl) -> Workbench:
|
|
32
|
+
pipeline = tuple(_resolve_plugin_steps(decl, kind="pipeline", specs=decl.pipeline.steps))
|
|
33
|
+
plots = tuple(_resolve_plugin_steps(decl, kind="plot", specs=decl.plots.specs))
|
|
34
|
+
exports = tuple(_resolve_plugin_steps(decl, kind="export", specs=decl.exports.specs))
|
|
35
|
+
ensure_unique_workbench_ids(pipeline, plots, exports)
|
|
36
|
+
return Workbench(pipeline=pipeline, plots=plots, exports=exports)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def materialize_workbench(decl: WorkbenchDecl) -> dict[str, list[dict[str, Any]]]:
|
|
40
|
+
workbench = resolve_workbench(decl)
|
|
41
|
+
return {
|
|
42
|
+
"pipeline": [item.to_dict() for item in workbench.pipeline],
|
|
43
|
+
"plots": [item.to_dict() for item in workbench.plots],
|
|
44
|
+
"exports": [item.to_dict() for item in workbench.exports],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def select_workbench_specs(
|
|
49
|
+
specs: list[PluginStep],
|
|
50
|
+
*,
|
|
51
|
+
only: list[str],
|
|
52
|
+
exclude: list[str],
|
|
53
|
+
kind_label: str,
|
|
54
|
+
) -> list[PluginStep]:
|
|
55
|
+
ids = [item.id for item in specs]
|
|
56
|
+
available = set(ids)
|
|
57
|
+
if only:
|
|
58
|
+
only_ids = set(only)
|
|
59
|
+
missing = sorted(only_ids - available)
|
|
60
|
+
if missing:
|
|
61
|
+
raise ConfigError(f"Unknown {kind_label} id(s): {missing}.")
|
|
62
|
+
selected = [item for item in specs if item.id in only_ids]
|
|
63
|
+
else:
|
|
64
|
+
selected = list(specs)
|
|
65
|
+
if exclude:
|
|
66
|
+
exclude_ids = set(exclude)
|
|
67
|
+
missing = sorted(exclude_ids - available)
|
|
68
|
+
if missing:
|
|
69
|
+
raise ConfigError(f"Unknown {kind_label} id(s): {missing}.")
|
|
70
|
+
selected = [item for item in selected if item.id not in exclude_ids]
|
|
71
|
+
return selected
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def normalize_input_binding(
|
|
75
|
+
binding: Any,
|
|
76
|
+
*,
|
|
77
|
+
root: Path,
|
|
78
|
+
resources: ResourceCatalog,
|
|
79
|
+
section: str,
|
|
80
|
+
step_id: str,
|
|
81
|
+
key: str,
|
|
82
|
+
) -> InputRef:
|
|
83
|
+
if isinstance(binding, RecordInputDecl):
|
|
84
|
+
return RecordRef(record_id=binding.record_id)
|
|
85
|
+
if isinstance(binding, FileInputDecl):
|
|
86
|
+
try:
|
|
87
|
+
path = resolve_path_within_root(binding.path, root=root)
|
|
88
|
+
except ValueError as err:
|
|
89
|
+
raise ConfigError(
|
|
90
|
+
f"{section} {step_id}: reads '{key}' must stay under the experiment root after resolving symlinks."
|
|
91
|
+
) from err
|
|
92
|
+
return FileRef(path=path)
|
|
93
|
+
if isinstance(binding, ResourceInputDecl):
|
|
94
|
+
try:
|
|
95
|
+
resource = resources.require_file(binding.resource_id)
|
|
96
|
+
except ValueError as err:
|
|
97
|
+
raise ConfigError(
|
|
98
|
+
f"{section} {step_id}: reads '{key}' references unknown resource '{binding.resource_id}'."
|
|
99
|
+
) from err
|
|
100
|
+
return ResourceRef(resource_id=binding.resource_id, path=resource.path)
|
|
101
|
+
if isinstance(binding, RecordCollectionInputDecl):
|
|
102
|
+
records: list[SourceRecordRef] = []
|
|
103
|
+
for resource_id in binding.resource_ids:
|
|
104
|
+
try:
|
|
105
|
+
resource = resources.require_record(resource_id)
|
|
106
|
+
except ValueError as err:
|
|
107
|
+
raise ConfigError(
|
|
108
|
+
f"{section} {step_id}: reads '{key}' references unknown record resource '{resource_id}'."
|
|
109
|
+
) from err
|
|
110
|
+
records.append(
|
|
111
|
+
SourceRecordRef(
|
|
112
|
+
resource_id=resource_id,
|
|
113
|
+
experiment_id=resource.experiment_id,
|
|
114
|
+
record_id=resource.record_id,
|
|
115
|
+
experiment_root=resource.experiment_root,
|
|
116
|
+
outputs_dir=resource.outputs_dir,
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return RecordCollectionRef(records=tuple(records))
|
|
120
|
+
raise ConfigError(f"{section} {step_id}: reads '{key}' uses unsupported binding type {type(binding).__name__}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def normalize_output_binding(binding: RecordOutputDecl) -> OutputRef:
|
|
124
|
+
return OutputRef(record_id=binding.record_id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _resolve_plugin_steps(
|
|
128
|
+
decl: WorkbenchDecl,
|
|
129
|
+
*,
|
|
130
|
+
kind: WorkbenchPluginStepKind,
|
|
131
|
+
specs: tuple[PluginStepDecl, ...],
|
|
132
|
+
) -> list[PluginStep]:
|
|
133
|
+
semantics = get_workbench_surface_semantics(kind)
|
|
134
|
+
root = decl.experiment.root
|
|
135
|
+
resources = decl.experiment_semantics.resources
|
|
136
|
+
seen: set[str] = set()
|
|
137
|
+
resolved: list[PluginStep] = []
|
|
138
|
+
for step in specs or ():
|
|
139
|
+
step_id = step.id
|
|
140
|
+
if not step_id or not isinstance(step_id, str):
|
|
141
|
+
raise ConfigError(f"Every {semantics.section} spec must include an id.")
|
|
142
|
+
if step_id in seen:
|
|
143
|
+
raise ConfigError(f"{semantics.section} contains duplicate spec id(s): {step_id}")
|
|
144
|
+
seen.add(step_id)
|
|
145
|
+
plugin = step.plugin
|
|
146
|
+
if not plugin or not isinstance(plugin, str):
|
|
147
|
+
raise ConfigError(f"{semantics.section} {step_id}: plugin must be a non-empty string")
|
|
148
|
+
if "/" not in plugin:
|
|
149
|
+
raise ConfigError(f"{semantics.section} {step_id}: plugin must be 'category/key'")
|
|
150
|
+
category = plugin.split("/", 1)[0]
|
|
151
|
+
expected_category = semantics.expected_plugin_category
|
|
152
|
+
if kind == "pipeline" and category in {"plot", "export"}:
|
|
153
|
+
raise ConfigError(f"pipeline {step_id}: plot/export plugins are not allowed in pipeline.")
|
|
154
|
+
if expected_category is not None and category != expected_category:
|
|
155
|
+
raise ConfigError(f"{semantics.section} {step_id}: plugin must be {expected_category}/*")
|
|
156
|
+
resolved.append(
|
|
157
|
+
PluginStep(
|
|
158
|
+
kind=kind,
|
|
159
|
+
id=step_id,
|
|
160
|
+
plugin=plugin,
|
|
161
|
+
reads={
|
|
162
|
+
key: normalize_input_binding(
|
|
163
|
+
value,
|
|
164
|
+
root=root,
|
|
165
|
+
resources=resources,
|
|
166
|
+
section=semantics.section,
|
|
167
|
+
step_id=step_id,
|
|
168
|
+
key=key,
|
|
169
|
+
)
|
|
170
|
+
for key, value in (step.reads or {}).items()
|
|
171
|
+
},
|
|
172
|
+
with_=dict(step.with_ or {}),
|
|
173
|
+
writes={key: normalize_output_binding(value) for key, value in (step.writes or {}).items()},
|
|
174
|
+
source_recipe=step.source_recipe,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
return resolved
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class RecordRef:
|
|
10
|
+
record_id: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class FileRef:
|
|
15
|
+
path: Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ResourceRef:
|
|
20
|
+
resource_id: str
|
|
21
|
+
path: Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SourceRecordRef:
|
|
26
|
+
resource_id: str
|
|
27
|
+
experiment_id: str
|
|
28
|
+
record_id: str
|
|
29
|
+
experiment_root: Path
|
|
30
|
+
outputs_dir: Path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class RecordCollectionRef:
|
|
35
|
+
records: tuple[SourceRecordRef, ...]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
InputRef = RecordRef | FileRef | ResourceRef | SourceRecordRef | RecordCollectionRef
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class OutputRef:
|
|
43
|
+
record_id: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class ProvenanceInput:
|
|
48
|
+
label: str
|
|
49
|
+
ref: InputRef
|
|
50
|
+
discovery_policy: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def input_ref_display(ref: InputRef) -> str:
|
|
54
|
+
if isinstance(ref, RecordRef):
|
|
55
|
+
return ref.record_id
|
|
56
|
+
if isinstance(ref, ResourceRef):
|
|
57
|
+
return f"resource({ref.resource_id})"
|
|
58
|
+
if isinstance(ref, SourceRecordRef):
|
|
59
|
+
return f"record_resource({ref.resource_id}:{ref.experiment_id}:{ref.record_id})"
|
|
60
|
+
if isinstance(ref, RecordCollectionRef):
|
|
61
|
+
return "record_collection(" + ", ".join(item.resource_id for item in ref.records) + ")"
|
|
62
|
+
return f"file({ref.path})"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def output_ref_display(ref: OutputRef) -> str:
|
|
66
|
+
return ref.record_id
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def input_ref_to_dict(ref: InputRef) -> dict[str, Any]:
|
|
70
|
+
if isinstance(ref, RecordRef):
|
|
71
|
+
return {"record": ref.record_id}
|
|
72
|
+
if isinstance(ref, ResourceRef):
|
|
73
|
+
return {"resource": ref.resource_id, "path": str(ref.path)}
|
|
74
|
+
if isinstance(ref, SourceRecordRef):
|
|
75
|
+
return {
|
|
76
|
+
"record_resource": ref.resource_id,
|
|
77
|
+
"experiment": ref.experiment_id,
|
|
78
|
+
"record": ref.record_id,
|
|
79
|
+
}
|
|
80
|
+
if isinstance(ref, RecordCollectionRef):
|
|
81
|
+
return {
|
|
82
|
+
"record_collection": [
|
|
83
|
+
{
|
|
84
|
+
"resource": item.resource_id,
|
|
85
|
+
"experiment": item.experiment_id,
|
|
86
|
+
"record": item.record_id,
|
|
87
|
+
}
|
|
88
|
+
for item in ref.records
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
return {"file": str(ref.path)}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def output_ref_to_dict(ref: OutputRef) -> dict[str, str]:
|
|
95
|
+
return {"record": ref.record_id}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def provenance_input_to_dict(binding: ProvenanceInput) -> dict[str, Any]:
|
|
99
|
+
return {"label": binding.label, **input_ref_to_dict(binding.ref)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def input_ref_from_dict(payload: dict[str, Any]) -> InputRef:
|
|
103
|
+
if not isinstance(payload, dict):
|
|
104
|
+
raise TypeError("input binding payload must be a mapping")
|
|
105
|
+
keys = set(payload)
|
|
106
|
+
if keys == {"record"}:
|
|
107
|
+
record = payload["record"]
|
|
108
|
+
if isinstance(record, str) and record:
|
|
109
|
+
return RecordRef(record_id=record)
|
|
110
|
+
raise ValueError("record binding must include a non-empty record id")
|
|
111
|
+
if keys == {"file"}:
|
|
112
|
+
file_path = payload["file"]
|
|
113
|
+
if isinstance(file_path, str) and file_path:
|
|
114
|
+
return FileRef(path=Path(file_path))
|
|
115
|
+
raise ValueError("file binding must include a non-empty path")
|
|
116
|
+
if keys == {"resource", "path"}:
|
|
117
|
+
resource_id = payload["resource"]
|
|
118
|
+
path = payload["path"]
|
|
119
|
+
if not isinstance(resource_id, str) or not resource_id:
|
|
120
|
+
raise ValueError("resource binding must include a non-empty resource id")
|
|
121
|
+
if not isinstance(path, str) or not path:
|
|
122
|
+
raise ValueError(f"resource binding {resource_id!r} must include a non-empty path")
|
|
123
|
+
return ResourceRef(resource_id=resource_id, path=Path(path))
|
|
124
|
+
raise ValueError(
|
|
125
|
+
"input binding payload must contain exactly one reference shape: {'record'}, {'file'}, or {'resource', 'path'}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def output_ref_from_dict(payload: dict[str, Any]) -> OutputRef:
|
|
130
|
+
if not isinstance(payload, dict):
|
|
131
|
+
raise TypeError("output binding payload must be a mapping")
|
|
132
|
+
if set(payload) != {"record"}:
|
|
133
|
+
raise ValueError("output binding payload must contain only 'record'")
|
|
134
|
+
record = payload.get("record")
|
|
135
|
+
if not isinstance(record, str) or not record:
|
|
136
|
+
raise ValueError("output binding payload must include record")
|
|
137
|
+
return OutputRef(record_id=record)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def provenance_input_from_dict(payload: dict[str, Any]) -> ProvenanceInput:
|
|
141
|
+
if not isinstance(payload, dict):
|
|
142
|
+
raise TypeError("provenance input payload must be a mapping")
|
|
143
|
+
label = payload.get("label")
|
|
144
|
+
if not isinstance(label, str) or not label:
|
|
145
|
+
raise ValueError("provenance input payload must include label")
|
|
146
|
+
ref_payload = dict(payload)
|
|
147
|
+
ref_payload.pop("label", None)
|
|
148
|
+
return ProvenanceInput(label=label, ref=input_ref_from_dict(ref_payload))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Neutral defaults shared by protocol authoring and ingest discovery mechanics."""
|
|
2
|
+
|
|
3
|
+
DEFAULT_INPUT_ROOTS = ("./inputs", "./raw", "./raw_data")
|
|
4
|
+
DEFAULT_WORKBOOK_INCLUDE = ("*.xlsx", "*.XLSX")
|
|
5
|
+
DEFAULT_INPUT_EXCLUDE = (
|
|
6
|
+
"~$*",
|
|
7
|
+
"._*",
|
|
8
|
+
"#*#",
|
|
9
|
+
"*.tmp",
|
|
10
|
+
"*.temp",
|
|
11
|
+
"*.bak",
|
|
12
|
+
"metadata.*",
|
|
13
|
+
"metadata_filtered.*",
|
|
14
|
+
"sample_map.*",
|
|
15
|
+
"sample_metadata.*",
|
|
16
|
+
"plate_map.*",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = ["DEFAULT_INPUT_EXCLUDE", "DEFAULT_INPUT_ROOTS", "DEFAULT_WORKBOOK_INCLUDE"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
|
|
6
|
+
from reader_workbench.workbench.graph import resolve_workbench
|
|
7
|
+
|
|
8
|
+
from .experiments import experiment_identity_payload
|
|
9
|
+
from .runtime import export_output_summaries, plot_output_summaries, record_producer_map, spec_step_payload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _sorted_counter(values: list[str]) -> dict[str, int]:
|
|
13
|
+
counts = Counter(value for value in values if value)
|
|
14
|
+
return dict(sorted(counts.items()))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def workbench_surface_catalog_payload(
|
|
18
|
+
*,
|
|
19
|
+
experiment: dict[str, object],
|
|
20
|
+
protocol: str,
|
|
21
|
+
kind: str,
|
|
22
|
+
only: list[str],
|
|
23
|
+
exclude: list[str],
|
|
24
|
+
entries: list[dict[str, object]],
|
|
25
|
+
) -> dict[str, object]:
|
|
26
|
+
payload_key = f"{kind}s"
|
|
27
|
+
summary_key = payload_key
|
|
28
|
+
plugin_ids = [str(entry.get("plugin") or "") for entry in entries]
|
|
29
|
+
domains = [
|
|
30
|
+
str(semantics.get("domain") or "")
|
|
31
|
+
for semantics in (entry.get("semantics") for entry in entries)
|
|
32
|
+
if isinstance(semantics, dict)
|
|
33
|
+
]
|
|
34
|
+
families = [
|
|
35
|
+
str(semantics.get("family") or "")
|
|
36
|
+
for semantics in (entry.get("semantics") for entry in entries)
|
|
37
|
+
if isinstance(semantics, dict)
|
|
38
|
+
]
|
|
39
|
+
return {
|
|
40
|
+
"experiment": deepcopy(experiment),
|
|
41
|
+
"catalog": {
|
|
42
|
+
"kind": kind,
|
|
43
|
+
"protocol": protocol,
|
|
44
|
+
},
|
|
45
|
+
"selection": {
|
|
46
|
+
"only": list(only),
|
|
47
|
+
"exclude": list(exclude),
|
|
48
|
+
},
|
|
49
|
+
"summary": {
|
|
50
|
+
summary_key: len(entries),
|
|
51
|
+
"by_plugin": _sorted_counter(plugin_ids),
|
|
52
|
+
"by_domain": _sorted_counter(domains),
|
|
53
|
+
"by_family": _sorted_counter(families),
|
|
54
|
+
},
|
|
55
|
+
payload_key: deepcopy(entries),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def plugin_registry_payload(
|
|
60
|
+
*,
|
|
61
|
+
descriptors,
|
|
62
|
+
category: str | None,
|
|
63
|
+
domain: str | None,
|
|
64
|
+
family: str | None,
|
|
65
|
+
protocol: str | None,
|
|
66
|
+
) -> dict[str, object]:
|
|
67
|
+
plugins = [
|
|
68
|
+
{
|
|
69
|
+
"category": descriptor.category,
|
|
70
|
+
"domain": descriptor.domain,
|
|
71
|
+
"family": descriptor.family,
|
|
72
|
+
"key": descriptor.key,
|
|
73
|
+
"plugin": descriptor.plugin,
|
|
74
|
+
"summary": descriptor.summary,
|
|
75
|
+
"class": f"{descriptor.cls.__module__}.{descriptor.cls.__name__}",
|
|
76
|
+
}
|
|
77
|
+
for descriptor in descriptors
|
|
78
|
+
]
|
|
79
|
+
return {
|
|
80
|
+
"selection": {
|
|
81
|
+
"category": category,
|
|
82
|
+
"domain": domain,
|
|
83
|
+
"family": family,
|
|
84
|
+
"protocol": protocol,
|
|
85
|
+
},
|
|
86
|
+
"summary": {
|
|
87
|
+
"plugins": len(plugins),
|
|
88
|
+
"by_category": _sorted_counter([str(item["category"]) for item in plugins]),
|
|
89
|
+
"by_domain": _sorted_counter([str(item["domain"]) for item in plugins]),
|
|
90
|
+
"by_family": _sorted_counter([str(item["family"]) for item in plugins]),
|
|
91
|
+
},
|
|
92
|
+
"plugins": plugins,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def workbench_surface_specs_payload(
|
|
97
|
+
*,
|
|
98
|
+
job_path,
|
|
99
|
+
decl,
|
|
100
|
+
runtime,
|
|
101
|
+
bound_protocol,
|
|
102
|
+
selected,
|
|
103
|
+
kind: str,
|
|
104
|
+
only: list[str],
|
|
105
|
+
exclude: list[str],
|
|
106
|
+
) -> dict[str, object]:
|
|
107
|
+
workbench = resolve_workbench(decl)
|
|
108
|
+
record_producers = record_producer_map(workbench.plugin_steps(), runtime=runtime)
|
|
109
|
+
summary_lookup = (
|
|
110
|
+
plot_output_summaries(bound_protocol) if kind == "plot" else export_output_summaries(bound_protocol)
|
|
111
|
+
)
|
|
112
|
+
entries = [
|
|
113
|
+
spec_step_payload(
|
|
114
|
+
step,
|
|
115
|
+
summary=summary_lookup.get(step.id, "—"),
|
|
116
|
+
runtime=runtime,
|
|
117
|
+
record_producers=record_producers,
|
|
118
|
+
)
|
|
119
|
+
for step in selected
|
|
120
|
+
]
|
|
121
|
+
return workbench_surface_catalog_payload(
|
|
122
|
+
experiment=experiment_identity_payload(job_path=job_path, decl=decl, protocol_id=bound_protocol.id),
|
|
123
|
+
protocol=bound_protocol.id,
|
|
124
|
+
kind=kind,
|
|
125
|
+
only=only,
|
|
126
|
+
exclude=exclude,
|
|
127
|
+
entries=entries,
|
|
128
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def count_visible_files(path: Path) -> int:
|
|
8
|
+
if not path.exists():
|
|
9
|
+
return 0
|
|
10
|
+
return sum(1 for item in path.rglob("*") if item.is_file() and not item.name.startswith("."))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def count_visible_glob(path: Path, pattern: str) -> int:
|
|
14
|
+
if not path.exists():
|
|
15
|
+
return 0
|
|
16
|
+
return sum(1 for item in path.rglob(pattern) if item.is_file() and not item.name.startswith("."))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def visible_relative_files(path: Path, *, base: Path, limit: int = 8) -> list[str]:
|
|
20
|
+
if not path.exists():
|
|
21
|
+
return []
|
|
22
|
+
files = sorted(item for item in path.rglob("*") if item.is_file() and not item.name.startswith("."))
|
|
23
|
+
return [format_relative_path(item, base=base) for item in files[:limit]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_relative_path(path: Path, *, base: Path) -> str:
|
|
27
|
+
try:
|
|
28
|
+
return str(path.relative_to(base))
|
|
29
|
+
except ValueError:
|
|
30
|
+
return str(path)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_output_subdir(outputs_dir: Path, subdir: str) -> Path:
|
|
34
|
+
return outputs_dir if subdir in ("", ".", "./") else outputs_dir / subdir
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def summarize_outputs_dir(
|
|
38
|
+
outputs_dir: Path,
|
|
39
|
+
*,
|
|
40
|
+
plots_subdir: str = "plots",
|
|
41
|
+
exports_subdir: str = "exports",
|
|
42
|
+
notebooks_subdir: str = "notebooks",
|
|
43
|
+
) -> dict[str, int]:
|
|
44
|
+
plots_dir = resolve_output_subdir(outputs_dir, plots_subdir)
|
|
45
|
+
exports_dir = resolve_output_subdir(outputs_dir, exports_subdir)
|
|
46
|
+
notebooks_dir = resolve_output_subdir(outputs_dir, notebooks_subdir)
|
|
47
|
+
artifacts_dir = outputs_dir / "artifacts"
|
|
48
|
+
return {
|
|
49
|
+
"records": count_visible_glob(artifacts_dir, "*.parquet"),
|
|
50
|
+
"plots": count_visible_files(plots_dir),
|
|
51
|
+
"exports": count_visible_files(exports_dir),
|
|
52
|
+
"notebooks": count_visible_files(notebooks_dir),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def preview_output_files(path: Path, *, base: Path, limit: int = 4) -> str:
|
|
57
|
+
files = visible_relative_files(path, base=base, limit=limit)
|
|
58
|
+
if not files:
|
|
59
|
+
return "—"
|
|
60
|
+
remaining = count_visible_files(path) - len(files)
|
|
61
|
+
preview = ", ".join(files)
|
|
62
|
+
if remaining > 0:
|
|
63
|
+
preview += f", … (+{remaining} more)"
|
|
64
|
+
return preview
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def render_compact_value(value) -> str:
|
|
68
|
+
if value is None:
|
|
69
|
+
return "null"
|
|
70
|
+
if isinstance(value, bool):
|
|
71
|
+
return "true" if value else "false"
|
|
72
|
+
if isinstance(value, (int, float)):
|
|
73
|
+
return str(value)
|
|
74
|
+
if isinstance(value, str):
|
|
75
|
+
return value
|
|
76
|
+
if isinstance(value, tuple):
|
|
77
|
+
value = list(value)
|
|
78
|
+
try:
|
|
79
|
+
return json.dumps(value, ensure_ascii=True, sort_keys=True)
|
|
80
|
+
except TypeError:
|
|
81
|
+
return str(value)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def flatten_binding_rows(value, *, prefix: str = "") -> list[tuple[str, str]]:
|
|
85
|
+
rows: list[tuple[str, str]] = []
|
|
86
|
+
if isinstance(value, dict):
|
|
87
|
+
for key in sorted(value):
|
|
88
|
+
child_path = f"{prefix}.{key}" if prefix else str(key)
|
|
89
|
+
rows.extend(flatten_binding_rows(value[key], prefix=child_path))
|
|
90
|
+
return rows
|
|
91
|
+
rows.append((prefix or "value", render_compact_value(value)))
|
|
92
|
+
return rows
|