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,333 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.metadata as md
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from reader_workbench.contracts import ContractCatalog, ContractId, OutputContractSurface
|
|
14
|
+
from reader_workbench.errors import ContractError, RegistryError
|
|
15
|
+
from reader_workbench.workbench.assets import AssetCatalog, AssetDescriptor, plugin_category_from_id
|
|
16
|
+
from reader_workbench.workbench.ports import (
|
|
17
|
+
InputPortSpec,
|
|
18
|
+
OutputPortSpec,
|
|
19
|
+
validate_input_ports,
|
|
20
|
+
validate_output_ports,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PluginConfig(BaseModel):
|
|
25
|
+
"""Base class for per-plugin configs (pydantic v2)."""
|
|
26
|
+
|
|
27
|
+
model_config = {"extra": "forbid"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class PreflightIssue:
|
|
32
|
+
kind: str
|
|
33
|
+
message: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Plugin(ABC):
|
|
37
|
+
"""Contract-driven plugin interface."""
|
|
38
|
+
|
|
39
|
+
ConfigModel = PluginConfig
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self._descriptor: AssetDescriptor | None = None
|
|
43
|
+
self._contracts: ContractCatalog | None = None
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def input_ports(cls) -> Mapping[str, InputPortSpec]:
|
|
48
|
+
"""Typed input port declarations."""
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def output_ports(cls) -> Mapping[str, OutputPortSpec]:
|
|
53
|
+
"""Typed output port declarations."""
|
|
54
|
+
|
|
55
|
+
def resolve_output_ports(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
inputs: Mapping[str, Any],
|
|
59
|
+
outputs: Mapping[str, Any],
|
|
60
|
+
cfg: PluginConfig,
|
|
61
|
+
where: str,
|
|
62
|
+
) -> Mapping[str, OutputPortSpec]:
|
|
63
|
+
"""Resolve runtime output ports; default is the declared minimum."""
|
|
64
|
+
del inputs, outputs, cfg, where
|
|
65
|
+
return dict(type(self).output_ports())
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def output_port_surfaces(cls) -> Mapping[str, OutputContractSurface]:
|
|
69
|
+
surfaces: dict[str, OutputContractSurface] = {}
|
|
70
|
+
for name, port in cls.output_ports().items():
|
|
71
|
+
surface = port.contract_surface
|
|
72
|
+
if surface is not None:
|
|
73
|
+
surfaces[name] = surface
|
|
74
|
+
return surfaces
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def preflight_readiness(
|
|
78
|
+
cls,
|
|
79
|
+
*,
|
|
80
|
+
exp_dir: Path,
|
|
81
|
+
cfg: PluginConfig,
|
|
82
|
+
reads: Mapping[str, Any],
|
|
83
|
+
) -> tuple[PreflightIssue, ...]:
|
|
84
|
+
del exp_dir, cfg, reads
|
|
85
|
+
return ()
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def validate_semantic_references(cls, *, experiment: Any, cfg: PluginConfig) -> None:
|
|
89
|
+
"""Validate experiment annotation references owned by this plugin's config."""
|
|
90
|
+
|
|
91
|
+
del experiment, cfg
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def resolve_missing_file_inputs(
|
|
95
|
+
cls,
|
|
96
|
+
*,
|
|
97
|
+
exp_dir: Path,
|
|
98
|
+
cfg: PluginConfig,
|
|
99
|
+
inputs: Mapping[str, Any],
|
|
100
|
+
) -> Mapping[str, Path]:
|
|
101
|
+
"""Resolve optional file inputs that were not bound in the graph."""
|
|
102
|
+
del exp_dir, cfg, inputs
|
|
103
|
+
return {}
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def passthrough_output_ports(
|
|
107
|
+
cls,
|
|
108
|
+
*,
|
|
109
|
+
outputs: Mapping[str, OutputPortSpec],
|
|
110
|
+
passthrough: Mapping[str, str],
|
|
111
|
+
promoted_examples: Mapping[str, tuple[ContractId, ...]] | None = None,
|
|
112
|
+
note: str | None = None,
|
|
113
|
+
) -> dict[str, OutputPortSpec]:
|
|
114
|
+
ports = dict(outputs)
|
|
115
|
+
promoted_examples = promoted_examples or {}
|
|
116
|
+
for out_name in passthrough:
|
|
117
|
+
port = ports.get(out_name)
|
|
118
|
+
if port is None or port.kind != "dataframe" or port.contract is None:
|
|
119
|
+
continue
|
|
120
|
+
ports[out_name] = replace(
|
|
121
|
+
port,
|
|
122
|
+
surface=OutputContractSurface(
|
|
123
|
+
minimum=port.contract,
|
|
124
|
+
runtime_mode="passthrough",
|
|
125
|
+
promoted=tuple(promoted_examples.get(out_name, ())),
|
|
126
|
+
note=note,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
return ports
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def promoted_output_ports(
|
|
133
|
+
cls,
|
|
134
|
+
*,
|
|
135
|
+
outputs: Mapping[str, OutputPortSpec],
|
|
136
|
+
promotions: Mapping[str, tuple[ContractId, ...]],
|
|
137
|
+
note: str | None = None,
|
|
138
|
+
) -> dict[str, OutputPortSpec]:
|
|
139
|
+
ports = dict(outputs)
|
|
140
|
+
for out_name, promoted in promotions.items():
|
|
141
|
+
port = ports.get(out_name)
|
|
142
|
+
if port is None or port.kind != "dataframe" or port.contract is None:
|
|
143
|
+
continue
|
|
144
|
+
ports[out_name] = replace(
|
|
145
|
+
port,
|
|
146
|
+
surface=OutputContractSurface(
|
|
147
|
+
minimum=port.contract,
|
|
148
|
+
runtime_mode="promoted",
|
|
149
|
+
promoted=tuple(promoted),
|
|
150
|
+
note=note,
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
return ports
|
|
154
|
+
|
|
155
|
+
def inherit_dataframe_output_ports(
|
|
156
|
+
self,
|
|
157
|
+
*,
|
|
158
|
+
inputs: Mapping[str, Any],
|
|
159
|
+
outputs: Mapping[str, Any],
|
|
160
|
+
passthrough: Mapping[str, str],
|
|
161
|
+
where: str,
|
|
162
|
+
) -> dict[str, OutputPortSpec]:
|
|
163
|
+
"""
|
|
164
|
+
Preserve stricter dataframe contracts across pass-through transforms when
|
|
165
|
+
the emitted dataframe still validates against the input contract.
|
|
166
|
+
"""
|
|
167
|
+
resolved = dict(type(self).output_ports())
|
|
168
|
+
for out_name, in_name in passthrough.items():
|
|
169
|
+
if out_name not in resolved or in_name not in inputs or out_name not in outputs:
|
|
170
|
+
continue
|
|
171
|
+
port = resolved[out_name]
|
|
172
|
+
if port.kind != "dataframe" or port.contract is None:
|
|
173
|
+
continue
|
|
174
|
+
actual = getattr(inputs[in_name], "contract_id", None)
|
|
175
|
+
if actual in (None, "none"):
|
|
176
|
+
continue
|
|
177
|
+
if not self.contracts.satisfies(actual=actual, expected=port.contract):
|
|
178
|
+
continue
|
|
179
|
+
if not isinstance(outputs[out_name], pd.DataFrame):
|
|
180
|
+
continue
|
|
181
|
+
try:
|
|
182
|
+
self.contracts.validate(outputs[out_name], contract_id=actual, where=f"{where}:{out_name}")
|
|
183
|
+
except ContractError:
|
|
184
|
+
continue
|
|
185
|
+
resolved[out_name] = replace(port, contract=actual)
|
|
186
|
+
return resolved
|
|
187
|
+
|
|
188
|
+
@abstractmethod
|
|
189
|
+
def run(self, ctx, inputs: dict[str, Any], cfg: PluginConfig) -> dict[str, Any]:
|
|
190
|
+
"""Execute and return dict of outputs by label."""
|
|
191
|
+
|
|
192
|
+
def bind_runtime(self, *, descriptor: AssetDescriptor, contracts: ContractCatalog) -> None:
|
|
193
|
+
if descriptor.cls is not type(self):
|
|
194
|
+
raise RegistryError(
|
|
195
|
+
f"Descriptor {descriptor.plugin_id!r} points to {descriptor.cls.__module__}.{descriptor.cls.__name__}, "
|
|
196
|
+
f"not {type(self).__module__}.{type(self).__name__}"
|
|
197
|
+
)
|
|
198
|
+
self._descriptor = descriptor
|
|
199
|
+
self._contracts = contracts
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def descriptor(self) -> AssetDescriptor:
|
|
203
|
+
if self._descriptor is None:
|
|
204
|
+
raise RegistryError(
|
|
205
|
+
f"Plugin instance {type(self).__module__}.{type(self).__name__} is missing a bound descriptor"
|
|
206
|
+
)
|
|
207
|
+
return self._descriptor
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def contracts(self) -> ContractCatalog:
|
|
211
|
+
if self._contracts is None:
|
|
212
|
+
raise RegistryError(
|
|
213
|
+
f"Plugin instance {type(self).__module__}.{type(self).__name__} is missing a bound contract catalog"
|
|
214
|
+
)
|
|
215
|
+
return self._contracts
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def plugin_id(self) -> str:
|
|
219
|
+
return self.descriptor.plugin_id
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def plugin_key(self) -> str:
|
|
223
|
+
return self.descriptor.key
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def plugin_category(self) -> str:
|
|
227
|
+
return self.descriptor.category
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class Registry:
|
|
231
|
+
"""Descriptor-driven plugin registry with explicit built-ins and explicit entry points."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, *, contracts: ContractCatalog) -> None:
|
|
234
|
+
self._descriptors: dict[str, AssetDescriptor] = {}
|
|
235
|
+
self.contracts = contracts
|
|
236
|
+
|
|
237
|
+
def register(self, descriptor: AssetDescriptor) -> None:
|
|
238
|
+
if not issubclass(descriptor.cls, Plugin):
|
|
239
|
+
raise RegistryError(
|
|
240
|
+
f"Plugin descriptor {descriptor.plugin_id!r} must point to a Plugin subclass, "
|
|
241
|
+
f"got {descriptor.cls.__module__}.{descriptor.cls.__name__}"
|
|
242
|
+
)
|
|
243
|
+
if descriptor.plugin in self._descriptors:
|
|
244
|
+
raise RegistryError(f"Duplicate plugin {descriptor.plugin!r}")
|
|
245
|
+
validate_input_ports(descriptor.cls.input_ports(), where=descriptor.plugin)
|
|
246
|
+
outputs = validate_output_ports(descriptor.cls.output_ports(), where=descriptor.plugin)
|
|
247
|
+
surfaces = descriptor.cls.output_port_surfaces()
|
|
248
|
+
for name, surface in surfaces.items():
|
|
249
|
+
port = outputs.get(name)
|
|
250
|
+
if port is None:
|
|
251
|
+
raise RegistryError(f"{descriptor.plugin}: output port surface {name!r} does not match a declared port")
|
|
252
|
+
if port.kind != "dataframe":
|
|
253
|
+
raise RegistryError(f"{descriptor.plugin}: output port surface {name!r} requires a dataframe port")
|
|
254
|
+
if surface.minimum != port.contract:
|
|
255
|
+
raise RegistryError(
|
|
256
|
+
f"{descriptor.plugin}: output port surface {name!r} minimum {surface.minimum!r} "
|
|
257
|
+
f"must match declared contract {port.contract!r}"
|
|
258
|
+
)
|
|
259
|
+
self._descriptors[descriptor.plugin] = descriptor
|
|
260
|
+
|
|
261
|
+
def categories(self) -> Mapping[str, Mapping[str, type[Plugin]]]:
|
|
262
|
+
grouped: dict[str, dict[str, type[Plugin]]] = {
|
|
263
|
+
"ingest": {},
|
|
264
|
+
"transform": {},
|
|
265
|
+
"plot": {},
|
|
266
|
+
"export": {},
|
|
267
|
+
"validator": {},
|
|
268
|
+
}
|
|
269
|
+
for descriptor in self._descriptors.values():
|
|
270
|
+
category = descriptor.category
|
|
271
|
+
grouped[category][descriptor.key] = descriptor.cls
|
|
272
|
+
return grouped
|
|
273
|
+
|
|
274
|
+
def catalog(self) -> AssetCatalog:
|
|
275
|
+
return AssetCatalog(list(self._descriptors.values()))
|
|
276
|
+
|
|
277
|
+
def resolve_descriptor(self, plugin: str) -> AssetDescriptor:
|
|
278
|
+
try:
|
|
279
|
+
return self._descriptors[plugin]
|
|
280
|
+
except KeyError:
|
|
281
|
+
available = ", ".join(sorted(self._descriptors))
|
|
282
|
+
raise RegistryError(f"Unknown plugin '{plugin}'. Installed: {available}") from None
|
|
283
|
+
|
|
284
|
+
def resolve(self, plugin: str) -> type[Plugin]:
|
|
285
|
+
return self.resolve_descriptor(plugin).cls
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _coerce_external_descriptor(loaded: Any, *, ep_name: str) -> AssetDescriptor:
|
|
289
|
+
if isinstance(loaded, AssetDescriptor):
|
|
290
|
+
descriptor = loaded
|
|
291
|
+
elif isinstance(loaded, type):
|
|
292
|
+
raise RegistryError(
|
|
293
|
+
f"Entry point {ep_name!r} must expose a plugin descriptor or descriptor factory, not a plugin class"
|
|
294
|
+
)
|
|
295
|
+
elif isinstance(loaded, Callable):
|
|
296
|
+
descriptor = loaded()
|
|
297
|
+
else:
|
|
298
|
+
descriptor = loaded
|
|
299
|
+
if not isinstance(descriptor, AssetDescriptor):
|
|
300
|
+
raise RegistryError(
|
|
301
|
+
f"Entry point {ep_name!r} must load an AssetDescriptor or a zero-arg callable returning one"
|
|
302
|
+
)
|
|
303
|
+
if ep_name != descriptor.plugin_id:
|
|
304
|
+
raise RegistryError(f"Entry point {ep_name!r} must match descriptor plugin id {descriptor.plugin_id!r}")
|
|
305
|
+
return descriptor
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def load_plugin_catalog(
|
|
309
|
+
*,
|
|
310
|
+
contracts: ContractCatalog,
|
|
311
|
+
builtin_descriptors: Iterable[AssetDescriptor],
|
|
312
|
+
categories: set[str] | None = None,
|
|
313
|
+
) -> Registry:
|
|
314
|
+
"""Register supplied built-ins and coordinated external plugin entry points."""
|
|
315
|
+
reg = Registry(contracts=contracts)
|
|
316
|
+
wanted = set(categories) if categories else None
|
|
317
|
+
|
|
318
|
+
selected_builtin_descriptors = tuple(
|
|
319
|
+
descriptor for descriptor in builtin_descriptors if wanted is None or descriptor.category in wanted
|
|
320
|
+
)
|
|
321
|
+
if not selected_builtin_descriptors:
|
|
322
|
+
raise RegistryError("No built-in plugin descriptors were supplied for the requested categories.")
|
|
323
|
+
for descriptor in selected_builtin_descriptors:
|
|
324
|
+
reg.register(descriptor)
|
|
325
|
+
|
|
326
|
+
for ep in md.entry_points(group="reader_workbench.plugins"):
|
|
327
|
+
entry_point_category = plugin_category_from_id(ep.name)
|
|
328
|
+
if wanted is not None and entry_point_category not in wanted:
|
|
329
|
+
continue
|
|
330
|
+
descriptor = _coerce_external_descriptor(ep.load(), ep_name=ep.name)
|
|
331
|
+
reg.register(descriptor)
|
|
332
|
+
|
|
333
|
+
return reg
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from reader_workbench.errors import ConfigError
|
|
9
|
+
from reader_workbench.workbench.experiment import ResourceCatalog
|
|
10
|
+
from reader_workbench.workbench.graph import (
|
|
11
|
+
FileRef,
|
|
12
|
+
InputRef,
|
|
13
|
+
OutputRef,
|
|
14
|
+
RecordRef,
|
|
15
|
+
ResourceRef,
|
|
16
|
+
select_workbench_specs,
|
|
17
|
+
)
|
|
18
|
+
from reader_workbench.workbench.paths import resolve_path_within_root
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_surface_command(
|
|
22
|
+
command: str,
|
|
23
|
+
job_path: Path,
|
|
24
|
+
*,
|
|
25
|
+
only: list[str] | None,
|
|
26
|
+
exclude: list[str] | None,
|
|
27
|
+
list_only: bool = False,
|
|
28
|
+
dry_run: bool = False,
|
|
29
|
+
log_level: str = "INFO",
|
|
30
|
+
inputs: list[str] | None = None,
|
|
31
|
+
sets: list[str] | None = None,
|
|
32
|
+
) -> list[str]:
|
|
33
|
+
parts = ["uv", "run", *command.split(), str(job_path)]
|
|
34
|
+
if list_only:
|
|
35
|
+
parts += ["--list"]
|
|
36
|
+
if only:
|
|
37
|
+
for value in only:
|
|
38
|
+
parts += ["--only", value]
|
|
39
|
+
if exclude:
|
|
40
|
+
for value in exclude:
|
|
41
|
+
parts += ["--exclude", value]
|
|
42
|
+
if dry_run:
|
|
43
|
+
parts += ["--dry-run"]
|
|
44
|
+
if log_level and log_level != "INFO":
|
|
45
|
+
parts += ["--log-level", log_level]
|
|
46
|
+
for raw in inputs or []:
|
|
47
|
+
parts += ["--input", raw]
|
|
48
|
+
for raw in sets or []:
|
|
49
|
+
parts += ["--set", raw]
|
|
50
|
+
return parts
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def select_surface_specs(steps, *, only: list[str], exclude: list[str], kind: str):
|
|
54
|
+
try:
|
|
55
|
+
return select_workbench_specs(steps, only=only, exclude=exclude, kind_label=kind)
|
|
56
|
+
except ConfigError as err:
|
|
57
|
+
raise typer.BadParameter(f"{err} Use --list to see valid ids.") from err
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_input_overrides(
|
|
61
|
+
raw_inputs: list[str],
|
|
62
|
+
*,
|
|
63
|
+
root: Path,
|
|
64
|
+
resources: ResourceCatalog,
|
|
65
|
+
) -> dict[str, InputRef]:
|
|
66
|
+
overrides: dict[str, InputRef] = {}
|
|
67
|
+
for raw in raw_inputs:
|
|
68
|
+
if "=" not in raw:
|
|
69
|
+
raise typer.BadParameter("--input expects KEY=VALUE")
|
|
70
|
+
key, value = raw.split("=", 1)
|
|
71
|
+
key = key.strip()
|
|
72
|
+
value = value.strip()
|
|
73
|
+
if not key:
|
|
74
|
+
raise typer.BadParameter("--input key cannot be empty")
|
|
75
|
+
if not value:
|
|
76
|
+
raise typer.BadParameter("--input value cannot be empty")
|
|
77
|
+
overrides[key] = _coerce_cli_input_ref(yaml.safe_load(value), root=root, resources=resources)
|
|
78
|
+
return overrides
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_set_overrides(raw_sets: list[str]) -> list[tuple[str, object]]:
|
|
82
|
+
overrides: list[tuple[str, object]] = []
|
|
83
|
+
for raw in raw_sets:
|
|
84
|
+
if "=" not in raw:
|
|
85
|
+
raise typer.BadParameter("--set expects PATH=VALUE")
|
|
86
|
+
path, value_raw = raw.split("=", 1)
|
|
87
|
+
path = path.strip()
|
|
88
|
+
if not path:
|
|
89
|
+
raise typer.BadParameter("--set path cannot be empty")
|
|
90
|
+
overrides.append((path, yaml.safe_load(value_raw)))
|
|
91
|
+
return overrides
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def apply_step_overrides(
|
|
95
|
+
steps,
|
|
96
|
+
*,
|
|
97
|
+
input_overrides: dict[str, InputRef],
|
|
98
|
+
set_overrides: list[tuple[str, object]],
|
|
99
|
+
root: Path,
|
|
100
|
+
resources: ResourceCatalog,
|
|
101
|
+
):
|
|
102
|
+
updated = []
|
|
103
|
+
for step in steps:
|
|
104
|
+
if hasattr(step, "model_copy"):
|
|
105
|
+
cloned = step.model_copy(deep=True)
|
|
106
|
+
reads = dict(cloned.reads or {})
|
|
107
|
+
with_block = dict(cloned.with_ or {})
|
|
108
|
+
writes = dict(cloned.writes or {})
|
|
109
|
+
else:
|
|
110
|
+
reads = dict(step.reads or {})
|
|
111
|
+
with_block = dict(step.with_ or {})
|
|
112
|
+
writes = dict(step.writes or {})
|
|
113
|
+
if input_overrides:
|
|
114
|
+
reads.update(input_overrides)
|
|
115
|
+
for path, value in set_overrides:
|
|
116
|
+
parts = [item for item in path.split(".") if item]
|
|
117
|
+
if not parts:
|
|
118
|
+
raise typer.BadParameter("--set path cannot be empty")
|
|
119
|
+
section = parts[0]
|
|
120
|
+
if section not in {"reads", "with", "writes"}:
|
|
121
|
+
raise typer.BadParameter("--set path must start with reads., with., or writes.")
|
|
122
|
+
if section in {"reads", "writes"}:
|
|
123
|
+
if len(parts) != 2:
|
|
124
|
+
raise typer.BadParameter(f"--set {section} expects a single key (e.g., {section}.foo=bar)")
|
|
125
|
+
target = reads if section == "reads" else writes
|
|
126
|
+
target[parts[1]] = (
|
|
127
|
+
_coerce_cli_input_ref(value, root=root, resources=resources)
|
|
128
|
+
if section == "reads"
|
|
129
|
+
else _coerce_cli_output_ref(value)
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
if len(parts) < 2:
|
|
133
|
+
raise typer.BadParameter("--set with.* requires a key (e.g., with.foo=bar)")
|
|
134
|
+
_set_nested(with_block, parts[1:], value)
|
|
135
|
+
if hasattr(step, "model_copy"):
|
|
136
|
+
cloned.reads = reads
|
|
137
|
+
cloned.with_ = with_block
|
|
138
|
+
cloned.writes = writes
|
|
139
|
+
updated.append(cloned)
|
|
140
|
+
continue
|
|
141
|
+
payload = {
|
|
142
|
+
"id": step.id,
|
|
143
|
+
"plugin": step.plugin,
|
|
144
|
+
"reads": reads,
|
|
145
|
+
"with_": with_block,
|
|
146
|
+
"writes": writes,
|
|
147
|
+
"source_recipe": getattr(step, "source_recipe", None),
|
|
148
|
+
}
|
|
149
|
+
if hasattr(step, "kind"):
|
|
150
|
+
payload["kind"] = step.kind
|
|
151
|
+
updated.append(step.__class__(**payload))
|
|
152
|
+
return updated
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _coerce_cli_input_ref(value, *, root: Path, resources: ResourceCatalog) -> InputRef:
|
|
156
|
+
if isinstance(value, (RecordRef, FileRef, ResourceRef)):
|
|
157
|
+
return value
|
|
158
|
+
if isinstance(value, dict):
|
|
159
|
+
record = value.get("record")
|
|
160
|
+
file_path = value.get("file")
|
|
161
|
+
resource_id = value.get("resource")
|
|
162
|
+
populated = [item for item in (record, file_path, resource_id) if item is not None]
|
|
163
|
+
if len(populated) != 1:
|
|
164
|
+
raise typer.BadParameter("reads.* must declare exactly one of record, file, or resource")
|
|
165
|
+
if isinstance(record, str) and record.strip():
|
|
166
|
+
return RecordRef(record_id=record.strip())
|
|
167
|
+
if isinstance(file_path, str) and file_path.strip():
|
|
168
|
+
path = Path(file_path.strip()).expanduser()
|
|
169
|
+
try:
|
|
170
|
+
path = resolve_path_within_root(path, root=root)
|
|
171
|
+
except ValueError as err:
|
|
172
|
+
raise typer.BadParameter(
|
|
173
|
+
"reads.* file bindings must stay under the experiment root after resolving symlinks"
|
|
174
|
+
) from err
|
|
175
|
+
return FileRef(path=path)
|
|
176
|
+
if isinstance(resource_id, str) and resource_id.strip():
|
|
177
|
+
return _resolve_cli_resource_ref(resource_id.strip(), resources=resources)
|
|
178
|
+
raise typer.BadParameter("reads.* binding values must be non-empty strings")
|
|
179
|
+
if isinstance(value, str) and value.strip():
|
|
180
|
+
return RecordRef(record_id=value.strip())
|
|
181
|
+
raise typer.BadParameter("reads.* expects a YAML/JSON mapping like {record: ...}, {file: ...}, or {resource: ...}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _resolve_cli_resource_ref(resource_id: str, *, resources: ResourceCatalog) -> ResourceRef:
|
|
185
|
+
if not resource_id:
|
|
186
|
+
raise typer.BadParameter("resource bindings require a non-empty resource id")
|
|
187
|
+
try:
|
|
188
|
+
resource = resources.require_file(resource_id)
|
|
189
|
+
except ValueError as err:
|
|
190
|
+
raise typer.BadParameter(str(err)) from err
|
|
191
|
+
return ResourceRef(resource_id=resource_id, path=resource.path.resolve())
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _coerce_cli_output_ref(value) -> OutputRef:
|
|
195
|
+
if isinstance(value, OutputRef):
|
|
196
|
+
return value
|
|
197
|
+
if isinstance(value, dict):
|
|
198
|
+
record = value.get("record")
|
|
199
|
+
if isinstance(record, str) and record.strip():
|
|
200
|
+
return OutputRef(record_id=record.strip())
|
|
201
|
+
raise typer.BadParameter("writes.* must declare {record: ...}")
|
|
202
|
+
if isinstance(value, str) and value.strip():
|
|
203
|
+
return OutputRef(record_id=value.strip())
|
|
204
|
+
raise typer.BadParameter("writes.* expects a record id or a {record: ...} mapping")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _set_nested(mapping: dict, keys: list[str], value) -> None:
|
|
208
|
+
current = mapping
|
|
209
|
+
for key in keys[:-1]:
|
|
210
|
+
if key not in current:
|
|
211
|
+
current[key] = {}
|
|
212
|
+
if not isinstance(current[key], dict):
|
|
213
|
+
raise typer.BadParameter(f"--set path invalid (non-mapping at '{key}')")
|
|
214
|
+
current = current[key]
|
|
215
|
+
current[keys[-1]] = value
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reader-workbench
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Validated, traceable analysis workflows for experimental instrument data
|
|
5
|
+
Author: Eric J. South
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/e-south/reader/tree/main/docs
|
|
8
|
+
Project-URL: Issues, https://github.com/e-south/reader/issues
|
|
9
|
+
Project-URL: Repository, https://github.com/e-south/reader
|
|
10
|
+
Keywords: data-analysis,data-pipelines,scientific-computing,workbench
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: <3.13,>=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.26
|
|
21
|
+
Requires-Dist: matplotlib>=3.8
|
|
22
|
+
Requires-Dist: pandas>=2.1
|
|
23
|
+
Requires-Dist: pydantic>=2.6
|
|
24
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
25
|
+
Requires-Dist: typer>=0.16.0
|
|
26
|
+
Requires-Dist: click>=8.3.3
|
|
27
|
+
Requires-Dist: rich>=13.7
|
|
28
|
+
Requires-Dist: pyarrow>=23.0.1
|
|
29
|
+
Requires-Dist: openpyxl>=3.1
|
|
30
|
+
Requires-Dist: seaborn>=0.13
|
|
31
|
+
Requires-Dist: pillow>=12.3.0
|
|
32
|
+
Requires-Dist: polars>=1.36.1
|
|
33
|
+
Requires-Dist: flowio>=1.4.0
|
|
34
|
+
Requires-Dist: filelock<4,>=3.20.3
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# 
|
|
38
|
+
|
|
39
|
+
[](https://github.com/e-south/reader/actions/workflows/checks.yaml)
|
|
40
|
+
[](https://www.python.org/downloads/release/python-3120/)
|
|
41
|
+
[](https://github.com/e-south/reader/blob/main/LICENSE)
|
|
42
|
+
|
|
43
|
+
Reader ingests instrument files and experiment metadata, applies declared
|
|
44
|
+
transformations, and writes validated records, plots, exports, and notebooks.
|
|
45
|
+
Every unit of work—including a cross-experiment aggregate—has an owned directory
|
|
46
|
+
beneath `experiments/`, with source material in `inputs/`, a `reader/v8`
|
|
47
|
+
contract in `config.yaml`, and generated artifacts in `outputs/`.
|
|
48
|
+
The distribution and import package are named `reader-workbench` and
|
|
49
|
+
`reader_workbench`; the installed command remains `reader`.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
Install Reader as a command-line tool:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv tool install reader-workbench
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For development, install from a checkout:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/e-south/reader.git
|
|
63
|
+
cd reader
|
|
64
|
+
uv sync --locked --group dev
|
|
65
|
+
uv run reader demo
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The installed command is `reader`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
reader demo
|
|
72
|
+
reader protocols
|
|
73
|
+
reader init ./experiments/my_experiment --protocol plate_reader/single_reporter_screen
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The demo prints a guided command tour. It does not execute a pipeline or write
|
|
77
|
+
files. Every generated starter can be inspected and validated before data is
|
|
78
|
+
added.
|
|
79
|
+
|
|
80
|
+
## Learn more
|
|
81
|
+
|
|
82
|
+
- [Getting started](https://github.com/e-south/reader/blob/main/docs/guides/getting_started.md) — install Reader, run the
|
|
83
|
+
demo, and scaffold a first experiment.
|
|
84
|
+
- [Common tasks](https://github.com/e-south/reader/blob/main/docs/guides/common_routes.md) — shortest commands for
|
|
85
|
+
discovery, validation, execution, and automation.
|
|
86
|
+
- [Python API](https://github.com/e-south/reader/blob/main/docs/core/python_api.md) — typed, task-oriented experiment and
|
|
87
|
+
plugin interfaces for integrations.
|
|
88
|
+
- [Documentation index](https://github.com/e-south/reader/blob/main/docs/README.md) — complete user, reference, and
|
|
89
|
+
maintainer documentation.
|
|
90
|
+
|
|
91
|
+
Reader is available under the [MIT license](https://github.com/e-south/reader/blob/main/LICENSE).
|