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,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from reader_workbench.errors import ConfigError
|
|
9
|
+
|
|
10
|
+
RUNTIME_FINGERPRINT_SUFFIXES = (".py", ".txt")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class MarimoRuntimePaths:
|
|
15
|
+
root: Path
|
|
16
|
+
registry_path: Path
|
|
17
|
+
xdg_config_home: Path
|
|
18
|
+
xdg_state_home: Path
|
|
19
|
+
xdg_cache_home: Path
|
|
20
|
+
mplconfigdir: Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_repo_root(start: Path) -> Path:
|
|
24
|
+
for base in [start.resolve()] + list(start.resolve().parents):
|
|
25
|
+
if (base / "pyproject.toml").exists():
|
|
26
|
+
return base
|
|
27
|
+
raise ConfigError(f"Could not find repository root from {start}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_repo_root(target: Path, *, repo_root: Path | None = None) -> Path:
|
|
31
|
+
if repo_root is None:
|
|
32
|
+
return find_repo_root(target)
|
|
33
|
+
resolved_root = repo_root.expanduser().resolve()
|
|
34
|
+
if not (resolved_root / "pyproject.toml").is_file():
|
|
35
|
+
raise ConfigError(f"Reader repository root does not contain pyproject.toml: {resolved_root}")
|
|
36
|
+
return resolved_root
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def find_experiment_root(start: Path) -> Path:
|
|
40
|
+
for base in [start.resolve()] + list(start.resolve().parents):
|
|
41
|
+
if (base / "config.yaml").exists():
|
|
42
|
+
return base
|
|
43
|
+
return start.resolve().parent
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def runtime_paths_for_repo_root(repo_root: Path) -> MarimoRuntimePaths:
|
|
47
|
+
root = repo_root / ".cache" / "marimo"
|
|
48
|
+
xdg_config_home = root / "xdg-config"
|
|
49
|
+
xdg_state_home = root / "xdg-state"
|
|
50
|
+
xdg_cache_home = root / "xdg-cache"
|
|
51
|
+
mplconfigdir = root / "matplotlib"
|
|
52
|
+
for path in (root, xdg_config_home, xdg_state_home, xdg_cache_home, mplconfigdir):
|
|
53
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
return MarimoRuntimePaths(
|
|
55
|
+
root=root,
|
|
56
|
+
registry_path=root / "sessions.json",
|
|
57
|
+
xdg_config_home=xdg_config_home,
|
|
58
|
+
xdg_state_home=xdg_state_home,
|
|
59
|
+
xdg_cache_home=xdg_cache_home,
|
|
60
|
+
mplconfigdir=mplconfigdir,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def target_signature(target: Path) -> tuple[int, int]:
|
|
65
|
+
stat = target.resolve().stat()
|
|
66
|
+
return stat.st_mtime_ns, stat.st_size
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def runtime_fingerprint(repo_root: Path) -> str:
|
|
70
|
+
resolved_root = repo_root.resolve()
|
|
71
|
+
hasher = hashlib.sha256()
|
|
72
|
+
candidates: list[Path] = []
|
|
73
|
+
pyproject = resolved_root / "pyproject.toml"
|
|
74
|
+
if pyproject.exists():
|
|
75
|
+
candidates.append(pyproject)
|
|
76
|
+
source_root = resolved_root / "src" / "reader_workbench"
|
|
77
|
+
if source_root.exists():
|
|
78
|
+
for suffix in RUNTIME_FINGERPRINT_SUFFIXES:
|
|
79
|
+
candidates.extend(path for path in source_root.rglob(f"*{suffix}") if path.is_file())
|
|
80
|
+
for path in sorted({item.resolve() for item in candidates}):
|
|
81
|
+
stat = path.stat()
|
|
82
|
+
relative = path.relative_to(resolved_root)
|
|
83
|
+
hasher.update(f"{relative}:{stat.st_mtime_ns}:{stat.st_size}\n".encode())
|
|
84
|
+
return hasher.hexdigest()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_env(
|
|
88
|
+
repo_root: Path,
|
|
89
|
+
*,
|
|
90
|
+
runtime_paths: MarimoRuntimePaths,
|
|
91
|
+
base_env: dict[str, str] | None = None,
|
|
92
|
+
) -> dict[str, str]:
|
|
93
|
+
env = dict(base_env or os.environ)
|
|
94
|
+
pythonpath_parts = [str(repo_root)]
|
|
95
|
+
existing_pythonpath = env.get("PYTHONPATH", "").strip()
|
|
96
|
+
if existing_pythonpath:
|
|
97
|
+
pythonpath_parts.append(existing_pythonpath)
|
|
98
|
+
env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
|
|
99
|
+
env["XDG_CONFIG_HOME"] = str(runtime_paths.xdg_config_home)
|
|
100
|
+
env["XDG_STATE_HOME"] = str(runtime_paths.xdg_state_home)
|
|
101
|
+
env["XDG_CACHE_HOME"] = str(runtime_paths.xdg_cache_home)
|
|
102
|
+
env["READER_MPLCONFIGDIR"] = str(runtime_paths.mplconfigdir)
|
|
103
|
+
env["MPLCONFIGDIR"] = str(runtime_paths.mplconfigdir)
|
|
104
|
+
return env
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .deliverables import (
|
|
2
|
+
NotebookDeliverables,
|
|
3
|
+
build_notebook_deliverable_selector,
|
|
4
|
+
collect_notebook_deliverables,
|
|
5
|
+
render_notebook_deliverable_viewport,
|
|
6
|
+
)
|
|
7
|
+
from .overview import (
|
|
8
|
+
NotebookOverview,
|
|
9
|
+
build_notebook_overview,
|
|
10
|
+
render_notebook_overview_panel,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"NotebookDeliverables",
|
|
15
|
+
"NotebookOverview",
|
|
16
|
+
"build_notebook_deliverable_selector",
|
|
17
|
+
"build_notebook_overview",
|
|
18
|
+
"collect_notebook_deliverables",
|
|
19
|
+
"render_notebook_deliverable_viewport",
|
|
20
|
+
"render_notebook_overview_panel",
|
|
21
|
+
]
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class NotebookDeliverables:
|
|
12
|
+
summary_rows: tuple[dict[str, Any], ...]
|
|
13
|
+
record_rows: tuple[dict[str, Any], ...]
|
|
14
|
+
plot_rows: tuple[dict[str, Any], ...]
|
|
15
|
+
export_rows: tuple[dict[str, Any], ...]
|
|
16
|
+
issue_rows: tuple[dict[str, Any], ...]
|
|
17
|
+
verification_status: str
|
|
18
|
+
artifact_rows: tuple[dict[str, Any], ...] = ()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_notebook_deliverable_selector(mo: Any, deliverables: NotebookDeliverables) -> Any | None:
|
|
22
|
+
options = _deliverable_options(deliverables)
|
|
23
|
+
if not options:
|
|
24
|
+
return None
|
|
25
|
+
labels = {label: key for key, label, _ in options}
|
|
26
|
+
return mo.ui.dropdown(
|
|
27
|
+
options=labels,
|
|
28
|
+
value=next(iter(labels)),
|
|
29
|
+
label="Deliverable",
|
|
30
|
+
full_width=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render_notebook_deliverable_viewport(
|
|
35
|
+
mo: Any,
|
|
36
|
+
deliverables: NotebookDeliverables,
|
|
37
|
+
selector: Any | None,
|
|
38
|
+
*,
|
|
39
|
+
dataframe_loader: Callable[[str, int, str], Any],
|
|
40
|
+
artifact_loader: Callable[[str, int, str, str], bytes],
|
|
41
|
+
) -> Any:
|
|
42
|
+
options = {key: (label, row) for key, label, row in _deliverable_options(deliverables)}
|
|
43
|
+
if selector is None or not options:
|
|
44
|
+
primary = mo.md("No deliverables are registered yet. Run the experiment, then refresh this notebook.")
|
|
45
|
+
selected_details = mo.md("No deliverable is selected.")
|
|
46
|
+
else:
|
|
47
|
+
selected = selector.value
|
|
48
|
+
if selected not in options:
|
|
49
|
+
raise ValueError("The selected deliverable no longer exists; refresh the notebook catalog.")
|
|
50
|
+
_label, row = options[selected]
|
|
51
|
+
selected_verification = str(row.get("Verification") or "unknown")
|
|
52
|
+
if deliverables.verification_status != "ok" or (row.get("Record ID") and selected_verification != "ok"):
|
|
53
|
+
primary = mo.md(
|
|
54
|
+
"Preview unavailable because Reader verification status is "
|
|
55
|
+
f"`{deliverables.verification_status}` and the selected record status is "
|
|
56
|
+
f"`{selected_verification}`. Review readiness notes, repair the records, and refresh."
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
primary = _render_deliverable_preview(
|
|
60
|
+
mo,
|
|
61
|
+
row,
|
|
62
|
+
dataframe_loader=dataframe_loader,
|
|
63
|
+
artifact_loader=artifact_loader,
|
|
64
|
+
)
|
|
65
|
+
selected_details = _render_table(mo, (row,), "No metadata is available.")
|
|
66
|
+
|
|
67
|
+
sections = {
|
|
68
|
+
"Selected metadata": selected_details,
|
|
69
|
+
"Summary": _render_table(mo, deliverables.summary_rows, "No deliverable summary is available."),
|
|
70
|
+
"Plots": _render_table(mo, deliverables.plot_rows, "No plot files are registered yet."),
|
|
71
|
+
"Exports": _render_table(mo, deliverables.export_rows, "No export files are registered yet."),
|
|
72
|
+
"Records": _render_table(mo, deliverables.record_rows, "No dataframe records are registered yet."),
|
|
73
|
+
"Artifacts": _render_table(mo, deliverables.artifact_rows, "No other artifact files are registered yet."),
|
|
74
|
+
}
|
|
75
|
+
if deliverables.issue_rows:
|
|
76
|
+
sections["Readiness notes"] = _render_table(mo, deliverables.issue_rows, "No readiness notes.")
|
|
77
|
+
controls = [] if selector is None else [selector]
|
|
78
|
+
return mo.vstack(
|
|
79
|
+
[
|
|
80
|
+
mo.md("## Deliverables"),
|
|
81
|
+
mo.md(f"**Verification:** `{deliverables.verification_status}`"),
|
|
82
|
+
*controls,
|
|
83
|
+
primary,
|
|
84
|
+
mo.accordion(sections, multiple=False, lazy=True),
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _deliverable_options(
|
|
90
|
+
deliverables: NotebookDeliverables,
|
|
91
|
+
) -> tuple[tuple[str, str, dict[str, Any]], ...]:
|
|
92
|
+
groups = (
|
|
93
|
+
("plot", "Plot", deliverables.plot_rows),
|
|
94
|
+
("export", "Export", deliverables.export_rows),
|
|
95
|
+
("record", "Data", deliverables.record_rows),
|
|
96
|
+
("artifact", "Artifact", deliverables.artifact_rows),
|
|
97
|
+
)
|
|
98
|
+
options: list[tuple[str, str, dict[str, Any]]] = []
|
|
99
|
+
used_labels: set[str] = set()
|
|
100
|
+
for kind, prefix, rows in groups:
|
|
101
|
+
for index, row in enumerate(rows):
|
|
102
|
+
record_id = str(row.get("Record ID") or "")
|
|
103
|
+
revision = row.get("Revision")
|
|
104
|
+
filename = str(row.get("File") or "")
|
|
105
|
+
if record_id and filename:
|
|
106
|
+
identity = f"{filename} · {record_id} · r{revision}"
|
|
107
|
+
elif record_id:
|
|
108
|
+
identity = f"{record_id} · r{revision}"
|
|
109
|
+
else:
|
|
110
|
+
identity = str(row.get("Path"))
|
|
111
|
+
label = f"{prefix} · {identity}"
|
|
112
|
+
if label in used_labels:
|
|
113
|
+
label = f"{label} · {index + 1}"
|
|
114
|
+
used_labels.add(label)
|
|
115
|
+
options.append((f"{kind}:{index}", label, row))
|
|
116
|
+
return tuple(options)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _render_deliverable_preview(
|
|
120
|
+
mo: Any,
|
|
121
|
+
row: dict[str, Any],
|
|
122
|
+
*,
|
|
123
|
+
dataframe_loader: Callable[[str, int, str], Any],
|
|
124
|
+
artifact_loader: Callable[[str, int, str, str], bytes],
|
|
125
|
+
) -> Any:
|
|
126
|
+
raw_path = row.get("Path")
|
|
127
|
+
if not isinstance(raw_path, str) or not raw_path.strip():
|
|
128
|
+
return _render_table(mo, (row,), "No preview is available.")
|
|
129
|
+
if Path(raw_path).is_absolute() or ".." in Path(raw_path).parts:
|
|
130
|
+
return mo.md("The selected deliverable does not have a safe experiment-relative path.")
|
|
131
|
+
suffix = Path(raw_path).suffix.lower()
|
|
132
|
+
description = str(row.get("Description") or row.get("Record ID") or Path(raw_path).name)
|
|
133
|
+
record_id = row.get("Record ID")
|
|
134
|
+
revision = row.get("Revision")
|
|
135
|
+
revision_digest = row.get("Revision digest")
|
|
136
|
+
if suffix in {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"}:
|
|
137
|
+
try:
|
|
138
|
+
content = artifact_loader(
|
|
139
|
+
_required_record_id(record_id),
|
|
140
|
+
_required_revision(revision),
|
|
141
|
+
_required_revision_digest(revision_digest),
|
|
142
|
+
raw_path,
|
|
143
|
+
)
|
|
144
|
+
except Exception:
|
|
145
|
+
return mo.md("Could not verify and load the selected artifact. Refresh the catalog or rerun its producer.")
|
|
146
|
+
return mo.image(content, alt=description)
|
|
147
|
+
if suffix == ".pdf":
|
|
148
|
+
try:
|
|
149
|
+
content = artifact_loader(
|
|
150
|
+
_required_record_id(record_id),
|
|
151
|
+
_required_revision(revision),
|
|
152
|
+
_required_revision_digest(revision_digest),
|
|
153
|
+
raw_path,
|
|
154
|
+
)
|
|
155
|
+
except Exception:
|
|
156
|
+
return mo.md("Could not verify and load the selected artifact. Refresh the catalog or rerun its producer.")
|
|
157
|
+
return mo.pdf(BytesIO(content), width="100%", height="70vh")
|
|
158
|
+
if row.get("Kind") == "dataframe_artifact":
|
|
159
|
+
try:
|
|
160
|
+
frame = dataframe_loader(
|
|
161
|
+
_required_record_id(record_id),
|
|
162
|
+
_required_revision(revision),
|
|
163
|
+
_required_revision_digest(revision_digest),
|
|
164
|
+
)
|
|
165
|
+
except Exception:
|
|
166
|
+
return mo.md("Could not verify and load the selected dataframe. Refresh the catalog or rerun its producer.")
|
|
167
|
+
return mo.ui.table(frame.head(200), page_size=min(12, max(1, len(frame))))
|
|
168
|
+
return mo.md(f"**{Path(raw_path).name}** \n{description} \n`{raw_path}`")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _required_record_id(value: object) -> str:
|
|
172
|
+
if not isinstance(value, str) or not value:
|
|
173
|
+
raise ValueError("deliverable record id is missing")
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _required_revision(value: object) -> int:
|
|
178
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
179
|
+
raise ValueError("deliverable revision is invalid")
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _required_revision_digest(value: object) -> str:
|
|
184
|
+
if not isinstance(value, str) or not value.startswith("sha256:") or len(value) != 71:
|
|
185
|
+
raise ValueError("deliverable revision digest is invalid")
|
|
186
|
+
return value
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def collect_notebook_deliverables(
|
|
190
|
+
entries: Iterable[Mapping[str, object]],
|
|
191
|
+
*,
|
|
192
|
+
outputs_dir: Path,
|
|
193
|
+
verification_status: str,
|
|
194
|
+
verification_issues: Iterable[Mapping[str, object]],
|
|
195
|
+
verification_records: Iterable[Mapping[str, object]],
|
|
196
|
+
) -> NotebookDeliverables:
|
|
197
|
+
outputs = Path(outputs_dir).expanduser().resolve()
|
|
198
|
+
verification_issue_items = tuple(verification_issues)
|
|
199
|
+
verification_record_items = tuple(verification_records)
|
|
200
|
+
record_statuses = {
|
|
201
|
+
str(item.get("record_id")): str(item.get("status") or "unknown")
|
|
202
|
+
for item in verification_record_items
|
|
203
|
+
if isinstance(item.get("record_id"), str)
|
|
204
|
+
}
|
|
205
|
+
record_rows: list[dict[str, Any]] = []
|
|
206
|
+
plot_rows: list[dict[str, Any]] = []
|
|
207
|
+
export_rows: list[dict[str, Any]] = []
|
|
208
|
+
artifact_rows: list[dict[str, Any]] = []
|
|
209
|
+
|
|
210
|
+
for entry in entries:
|
|
211
|
+
if entry.get("kind") == "dataframe_artifact":
|
|
212
|
+
record_rows.append(
|
|
213
|
+
_dataframe_row(
|
|
214
|
+
entry,
|
|
215
|
+
outputs_dir=outputs,
|
|
216
|
+
verification_status=record_statuses.get(str(entry.get("record_id")), "unknown"),
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
elif entry.get("kind") == "file_bundle":
|
|
220
|
+
producer = entry.get("producer")
|
|
221
|
+
producer_info = producer if isinstance(producer, Mapping) else {}
|
|
222
|
+
producer_kind = producer_info.get("kind")
|
|
223
|
+
if producer_kind == "plot":
|
|
224
|
+
plot_rows.extend(
|
|
225
|
+
_file_rows(
|
|
226
|
+
entry,
|
|
227
|
+
outputs_dir=outputs,
|
|
228
|
+
verification_status=record_statuses.get(str(entry.get("record_id")), "unknown"),
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
elif producer_kind == "export":
|
|
232
|
+
export_rows.extend(
|
|
233
|
+
_file_rows(
|
|
234
|
+
entry,
|
|
235
|
+
outputs_dir=outputs,
|
|
236
|
+
verification_status=record_statuses.get(str(entry.get("record_id")), "unknown"),
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
artifact_rows.extend(
|
|
241
|
+
_file_rows(
|
|
242
|
+
entry,
|
|
243
|
+
outputs_dir=outputs,
|
|
244
|
+
verification_status=record_statuses.get(str(entry.get("record_id")), "unknown"),
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
summary_rows = (
|
|
249
|
+
{"Deliverable": "Dataframe records", "Count": len(record_rows)},
|
|
250
|
+
{"Deliverable": "Plot files", "Count": len(plot_rows)},
|
|
251
|
+
{"Deliverable": "Export files", "Count": len(export_rows)},
|
|
252
|
+
{"Deliverable": "Other artifact files", "Count": len(artifact_rows)},
|
|
253
|
+
)
|
|
254
|
+
return NotebookDeliverables(
|
|
255
|
+
summary_rows=summary_rows,
|
|
256
|
+
record_rows=tuple(record_rows),
|
|
257
|
+
plot_rows=tuple(plot_rows),
|
|
258
|
+
export_rows=tuple(export_rows),
|
|
259
|
+
issue_rows=_readiness_rows(
|
|
260
|
+
status=verification_status,
|
|
261
|
+
issues=verification_issue_items,
|
|
262
|
+
records=verification_record_items,
|
|
263
|
+
),
|
|
264
|
+
verification_status=verification_status,
|
|
265
|
+
artifact_rows=tuple(artifact_rows),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _render_table(mo: Any, rows: tuple[dict[str, Any], ...], empty_text: str) -> Any:
|
|
270
|
+
rows_list = list(rows)
|
|
271
|
+
if not rows_list:
|
|
272
|
+
return mo.md(empty_text)
|
|
273
|
+
return mo.ui.table(rows_list, page_size=min(12, max(1, len(rows_list))))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _dataframe_row(
|
|
277
|
+
record: Mapping[str, object],
|
|
278
|
+
*,
|
|
279
|
+
outputs_dir: Path,
|
|
280
|
+
verification_status: str,
|
|
281
|
+
) -> dict[str, Any]:
|
|
282
|
+
path = _entry_path(record.get("path"), outputs_dir=outputs_dir)
|
|
283
|
+
return {
|
|
284
|
+
"Kind": "dataframe_artifact",
|
|
285
|
+
"Record ID": str(record.get("record_id") or ""),
|
|
286
|
+
"Producer": _producer_label(record),
|
|
287
|
+
"Plugin": _producer_value(record, "plugin"),
|
|
288
|
+
"Contract": str(record.get("contract_id") or ""),
|
|
289
|
+
"Path": _display_path(path, outputs_dir=outputs_dir),
|
|
290
|
+
"Revision": record.get("revision"),
|
|
291
|
+
"Revision digest": record.get("revision_digest"),
|
|
292
|
+
"Verification": verification_status,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _file_rows(
|
|
297
|
+
record: Mapping[str, object],
|
|
298
|
+
*,
|
|
299
|
+
outputs_dir: Path,
|
|
300
|
+
verification_status: str,
|
|
301
|
+
) -> list[dict[str, Any]]:
|
|
302
|
+
raw_descriptions = record.get("path_descriptions")
|
|
303
|
+
descriptions_by_path = (
|
|
304
|
+
{
|
|
305
|
+
str(item.get("path")): str(item.get("description") or "")
|
|
306
|
+
for item in raw_descriptions
|
|
307
|
+
if isinstance(item, Mapping) and isinstance(item.get("path"), str)
|
|
308
|
+
}
|
|
309
|
+
if isinstance(raw_descriptions, list)
|
|
310
|
+
else {}
|
|
311
|
+
)
|
|
312
|
+
bundle_description = str(record.get("description") or "Description unavailable in this record.")
|
|
313
|
+
producer_kind = _producer_value(record, "kind")
|
|
314
|
+
raw_files = record.get("files")
|
|
315
|
+
files = raw_files if isinstance(raw_files, list) else []
|
|
316
|
+
rows = []
|
|
317
|
+
for raw_path in files:
|
|
318
|
+
if not isinstance(raw_path, str) or not raw_path:
|
|
319
|
+
continue
|
|
320
|
+
path = _entry_path(raw_path, outputs_dir=outputs_dir)
|
|
321
|
+
description = (
|
|
322
|
+
descriptions_by_path.get(raw_path, bundle_description) if producer_kind == "plot" else bundle_description
|
|
323
|
+
)
|
|
324
|
+
rows.append(
|
|
325
|
+
{
|
|
326
|
+
"Kind": "file_bundle",
|
|
327
|
+
"Record ID": str(record.get("record_id") or ""),
|
|
328
|
+
"Producer": _producer_label(record),
|
|
329
|
+
"Plugin": _producer_value(record, "plugin"),
|
|
330
|
+
"Description": description,
|
|
331
|
+
"File": path.name,
|
|
332
|
+
"Path": _display_path(path, outputs_dir=outputs_dir),
|
|
333
|
+
"Revision": record.get("revision"),
|
|
334
|
+
"Revision digest": record.get("revision_digest"),
|
|
335
|
+
"Verification": verification_status,
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
return rows
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _producer_label(record: Mapping[str, object]) -> str:
|
|
342
|
+
return f"{_producer_value(record, 'kind')}:{_producer_value(record, 'id')}"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _producer_value(record: Mapping[str, object], key: str) -> str:
|
|
346
|
+
producer = record.get("producer")
|
|
347
|
+
if not isinstance(producer, Mapping):
|
|
348
|
+
return ""
|
|
349
|
+
return str(producer.get(key) or "")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _entry_path(raw_path: object, *, outputs_dir: Path) -> Path:
|
|
353
|
+
path = Path(str(raw_path or ""))
|
|
354
|
+
return path if path.is_absolute() else outputs_dir / path
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _display_path(path: Path, *, outputs_dir: Path) -> str:
|
|
358
|
+
try:
|
|
359
|
+
relative = path.relative_to(outputs_dir)
|
|
360
|
+
except ValueError:
|
|
361
|
+
return "[outside outputs]"
|
|
362
|
+
if relative == Path(".") or ".." in relative.parts:
|
|
363
|
+
return "[outside outputs]"
|
|
364
|
+
return relative.as_posix()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _readiness_rows(
|
|
368
|
+
*,
|
|
369
|
+
status: str,
|
|
370
|
+
issues: Iterable[Mapping[str, object]],
|
|
371
|
+
records: Iterable[Mapping[str, object]],
|
|
372
|
+
) -> tuple[dict[str, Any], ...]:
|
|
373
|
+
rows = [_readiness_row(issue, scope="catalog", record_id="", status=status) for issue in issues]
|
|
374
|
+
for record in records:
|
|
375
|
+
record_id = str(record.get("record_id") or "")
|
|
376
|
+
record_status = str(record.get("status") or "unknown")
|
|
377
|
+
raw_issues = record.get("issues")
|
|
378
|
+
if not isinstance(raw_issues, list):
|
|
379
|
+
continue
|
|
380
|
+
rows.extend(
|
|
381
|
+
_readiness_row(issue, scope="record", record_id=record_id, status=record_status)
|
|
382
|
+
for issue in raw_issues
|
|
383
|
+
if isinstance(issue, Mapping)
|
|
384
|
+
)
|
|
385
|
+
return tuple(rows)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _readiness_row(
|
|
389
|
+
issue: Mapping[str, object],
|
|
390
|
+
*,
|
|
391
|
+
scope: str,
|
|
392
|
+
record_id: str,
|
|
393
|
+
status: str,
|
|
394
|
+
) -> dict[str, Any]:
|
|
395
|
+
return {
|
|
396
|
+
"Scope": scope,
|
|
397
|
+
"Record ID": record_id,
|
|
398
|
+
"Status": status,
|
|
399
|
+
"Code": str(issue.get("code") or "unknown"),
|
|
400
|
+
"Field": str(issue.get("field") or ""),
|
|
401
|
+
"Action": str(issue.get("remediation") or "Inspect Reader verification output."),
|
|
402
|
+
"Retryable": "yes" if issue.get("retryable") is True else "no",
|
|
403
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from reader_workbench.workbench.notebooks.presentation import experiment_display_title
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class NotebookOverview:
|
|
13
|
+
experiment_id: str
|
|
14
|
+
experiment_title: str
|
|
15
|
+
protocol_id: str
|
|
16
|
+
experiment_root: Path
|
|
17
|
+
outputs_dir: Path
|
|
18
|
+
notebooks_dir: Path
|
|
19
|
+
pipeline_rows: tuple[dict[str, Any], ...]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_notebook_overview(
|
|
23
|
+
*,
|
|
24
|
+
experiment_id: str,
|
|
25
|
+
experiment_title: str | None,
|
|
26
|
+
protocol_id: str,
|
|
27
|
+
experiment_root: Path,
|
|
28
|
+
outputs_dir: Path,
|
|
29
|
+
notebooks_dir: Path,
|
|
30
|
+
pipeline_step_ids: Sequence[str],
|
|
31
|
+
) -> NotebookOverview:
|
|
32
|
+
resolved_experiment_id = _required_text(experiment_id, "experiment_id")
|
|
33
|
+
resolved_protocol_id = _required_text(protocol_id, "protocol_id")
|
|
34
|
+
title = experiment_display_title(
|
|
35
|
+
experiment_id=resolved_experiment_id,
|
|
36
|
+
authored_title=experiment_title,
|
|
37
|
+
)
|
|
38
|
+
pipeline_rows = tuple(
|
|
39
|
+
{"Order": idx, "Step ID": str(step_id)} for idx, step_id in enumerate(pipeline_step_ids, start=1)
|
|
40
|
+
)
|
|
41
|
+
return NotebookOverview(
|
|
42
|
+
experiment_id=resolved_experiment_id,
|
|
43
|
+
experiment_title=title,
|
|
44
|
+
protocol_id=resolved_protocol_id,
|
|
45
|
+
experiment_root=Path(experiment_root).expanduser().resolve(),
|
|
46
|
+
outputs_dir=Path(outputs_dir).expanduser().resolve(),
|
|
47
|
+
notebooks_dir=Path(notebooks_dir).expanduser().resolve(),
|
|
48
|
+
pipeline_rows=pipeline_rows,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render_notebook_overview_panel(
|
|
53
|
+
mo: Any,
|
|
54
|
+
overview: NotebookOverview,
|
|
55
|
+
*,
|
|
56
|
+
detail_sections: Mapping[str, Any] | None = None,
|
|
57
|
+
include_heading: bool = True,
|
|
58
|
+
) -> Any:
|
|
59
|
+
at_a_glance_rows = (
|
|
60
|
+
{"Field": "Experiment ID", "Value": overview.experiment_id},
|
|
61
|
+
{"Field": "Protocol", "Value": overview.protocol_id},
|
|
62
|
+
{"Field": "Pipeline steps", "Value": len(overview.pipeline_rows)},
|
|
63
|
+
)
|
|
64
|
+
path_rows = (
|
|
65
|
+
{
|
|
66
|
+
"Path": "Experiment root",
|
|
67
|
+
"Value": _path_label(overview.experiment_root, base=overview.experiment_root),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"Path": "Outputs",
|
|
71
|
+
"Value": _path_label(overview.outputs_dir, base=overview.experiment_root),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"Path": "Generated notebooks",
|
|
75
|
+
"Value": _path_label(overview.notebooks_dir, base=overview.experiment_root),
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
rendered_detail_sections = dict(detail_sections or {})
|
|
79
|
+
reserved_sections = {"Pipeline", "Paths"}
|
|
80
|
+
conflicts = sorted(reserved_sections.intersection(rendered_detail_sections))
|
|
81
|
+
if conflicts:
|
|
82
|
+
raise ValueError(f"Notebook overview detail sections use reserved names: {', '.join(conflicts)}")
|
|
83
|
+
rendered_detail_sections.update(
|
|
84
|
+
{
|
|
85
|
+
"Pipeline": _render_table(
|
|
86
|
+
mo,
|
|
87
|
+
overview.pipeline_rows,
|
|
88
|
+
"No pipeline steps are declared.",
|
|
89
|
+
),
|
|
90
|
+
"Paths": _render_table(mo, path_rows, "No output paths are available."),
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
items = [
|
|
94
|
+
mo.ui.table(list(at_a_glance_rows), page_size=len(at_a_glance_rows)),
|
|
95
|
+
mo.accordion(rendered_detail_sections, multiple=True, lazy=True),
|
|
96
|
+
]
|
|
97
|
+
if include_heading:
|
|
98
|
+
items.insert(0, mo.md(f"# {overview.experiment_title}"))
|
|
99
|
+
return mo.vstack(items)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _render_table(mo: Any, rows: tuple[dict[str, Any], ...], empty_text: str) -> Any:
|
|
103
|
+
if not rows:
|
|
104
|
+
return mo.md(empty_text)
|
|
105
|
+
return mo.ui.table(list(rows), page_size=min(12, max(1, len(rows))))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _path_label(path: Path, *, base: Path) -> str:
|
|
109
|
+
try:
|
|
110
|
+
return str(path.relative_to(base))
|
|
111
|
+
except ValueError:
|
|
112
|
+
return str(path)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _required_text(value: str, field: str) -> str:
|
|
116
|
+
text = str(value or "").strip()
|
|
117
|
+
if not text:
|
|
118
|
+
raise ValueError(f"{field} is required for notebook overview rendering.")
|
|
119
|
+
return text
|