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,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from reader_workbench.errors import RecordError
|
|
10
|
+
from reader_workbench.workbench.commands import reader_command
|
|
11
|
+
from reader_workbench.workbench.experiments import discover_experiment_configs
|
|
12
|
+
|
|
13
|
+
from ._lazy import load as _load
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from reader_workbench.runtime import ReaderRuntime
|
|
17
|
+
from reader_workbench.workbench.config import ReaderSpec
|
|
18
|
+
from reader_workbench.workbench.decl import WorkbenchDecl
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_job_models(job_path: Path, *, runtime: ReaderRuntime | None = None) -> tuple[ReaderSpec, WorkbenchDecl]:
|
|
22
|
+
runtime = runtime or _load("reader_workbench.runtime").builtin_runtime()
|
|
23
|
+
spec = _load("reader_workbench.workbench.config").ReaderSpec.load(job_path)
|
|
24
|
+
return spec, _load("reader_workbench.workbench.decl").build_workbench_decl(
|
|
25
|
+
spec, source_path=job_path, protocols=runtime.protocols
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def bind_decl_protocol(*, decl: WorkbenchDecl, runtime: ReaderRuntime):
|
|
30
|
+
return runtime.bind_protocol(decl.experiment_semantics.protocol)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def default_notebook_name() -> str:
|
|
34
|
+
return f"EDA_{datetime.now().strftime('%Y%m%d')}.py"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def next_available_path(path: Path) -> Path:
|
|
38
|
+
if not path.exists():
|
|
39
|
+
return path
|
|
40
|
+
stem = path.stem
|
|
41
|
+
suffix = path.suffix
|
|
42
|
+
counter = 1
|
|
43
|
+
while True:
|
|
44
|
+
candidate = path.with_name(f"{stem}_{counter}{suffix}")
|
|
45
|
+
if not candidate.exists():
|
|
46
|
+
return candidate
|
|
47
|
+
counter += 1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def find_nearest_experiments_dir(start: Path) -> Path:
|
|
51
|
+
for base in [start] + list(start.parents):
|
|
52
|
+
candidate = base / "experiments"
|
|
53
|
+
if candidate.exists() and candidate.is_dir():
|
|
54
|
+
return candidate.resolve()
|
|
55
|
+
return (start / "experiments").resolve()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_jobs(root: Path, *, include_scaffolds: bool = False) -> list[Path]:
|
|
59
|
+
return discover_experiment_configs(root, include_scaffolds=include_scaffolds)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def indexed_jobs(root: Path, *, include_scaffolds: bool = False) -> list[tuple[int, Path]]:
|
|
63
|
+
all_jobs = find_jobs(root, include_scaffolds=True)
|
|
64
|
+
if include_scaffolds:
|
|
65
|
+
return list(enumerate(all_jobs, 1))
|
|
66
|
+
visible_jobs = set(find_jobs(root, include_scaffolds=False))
|
|
67
|
+
return [(idx, job) for idx, job in enumerate(all_jobs, 1) if job in visible_jobs]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def find_year_jobs(year: str, root: Path) -> list[Path]:
|
|
71
|
+
year_str = str(year).strip()
|
|
72
|
+
if not year_str:
|
|
73
|
+
raise typer.BadParameter("--year cannot be empty")
|
|
74
|
+
if not year_str.isdigit() or len(year_str) != 4:
|
|
75
|
+
raise typer.BadParameter("--year expects a 4-digit year (e.g., 2025).")
|
|
76
|
+
if not root.exists() or not root.is_dir():
|
|
77
|
+
raise typer.BadParameter(f"Experiments root not found: {root}")
|
|
78
|
+
year_dir = root / year_str
|
|
79
|
+
if not year_dir.exists() or not year_dir.is_dir():
|
|
80
|
+
raise typer.BadParameter(f"No experiments directory for year {year_str} under {root}.")
|
|
81
|
+
jobs = find_jobs(year_dir)
|
|
82
|
+
if not jobs:
|
|
83
|
+
raise typer.BadParameter(f"No experiments found under {year_dir}.")
|
|
84
|
+
return jobs
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def infer_job_path(job: str | None) -> Path:
|
|
88
|
+
if job:
|
|
89
|
+
value = str(job).strip()
|
|
90
|
+
path = Path(value)
|
|
91
|
+
if path.exists():
|
|
92
|
+
if path.is_dir():
|
|
93
|
+
candidate = path / "config.yaml"
|
|
94
|
+
if candidate.exists():
|
|
95
|
+
return candidate.resolve()
|
|
96
|
+
raise typer.BadParameter(
|
|
97
|
+
f"CONFIG directory {path!s} has no 'config.yaml'. "
|
|
98
|
+
"Pass a file path, an experiment directory that contains config.yaml, or a numeric index "
|
|
99
|
+
f"(see '{reader_command('ls')}')."
|
|
100
|
+
)
|
|
101
|
+
return path.resolve()
|
|
102
|
+
if value.isdigit():
|
|
103
|
+
idx = int(value)
|
|
104
|
+
root_path = find_nearest_experiments_dir(Path.cwd())
|
|
105
|
+
jobs = indexed_jobs(root_path)
|
|
106
|
+
jobs_with_scaffolds = indexed_jobs(root_path, include_scaffolds=True)
|
|
107
|
+
if not jobs_with_scaffolds:
|
|
108
|
+
raise typer.BadParameter(f"No experiments found under {root_path}. Use '{reader_command('ls')}' first.")
|
|
109
|
+
job_lookup = dict(jobs)
|
|
110
|
+
if idx in job_lookup:
|
|
111
|
+
return job_lookup[idx]
|
|
112
|
+
jobs_with_scaffolds_lookup = dict(jobs_with_scaffolds)
|
|
113
|
+
if idx in jobs_with_scaffolds_lookup:
|
|
114
|
+
hidden_job = jobs_with_scaffolds_lookup[idx]
|
|
115
|
+
raise typer.BadParameter(
|
|
116
|
+
f"Experiment index {idx} points to hidden scaffold/template config {hidden_job.parent} under {root_path}. "
|
|
117
|
+
"Numeric indexes only address the default 'reader ls' inventory. "
|
|
118
|
+
"Pass the path explicitly for scaffold/template configs."
|
|
119
|
+
)
|
|
120
|
+
scaffold_hint = ""
|
|
121
|
+
if jobs_with_scaffolds != jobs:
|
|
122
|
+
scaffold_hint = (
|
|
123
|
+
" Default index range: "
|
|
124
|
+
f"{_format_index_span(index for index, _ in jobs)}; with '--all': "
|
|
125
|
+
f"{_format_index_span(index for index, _ in jobs_with_scaffolds)}."
|
|
126
|
+
)
|
|
127
|
+
raise typer.BadParameter(
|
|
128
|
+
f"Experiment index out of range: {idx} (valid: {_format_index_span(index for index, _ in jobs)}"
|
|
129
|
+
f" under {root_path})."
|
|
130
|
+
f"{scaffold_hint} Use '{reader_command('ls')}' to see the index numbers."
|
|
131
|
+
)
|
|
132
|
+
raise typer.BadParameter(
|
|
133
|
+
f"CONFIG not found: {job!r}. Pass a path to a config.yaml, an experiment directory, "
|
|
134
|
+
f"or a numeric experiment index from '{reader_command('ls')}'."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
cwd = Path.cwd()
|
|
138
|
+
candidate = cwd / "config.yaml"
|
|
139
|
+
if candidate.exists():
|
|
140
|
+
return candidate.resolve()
|
|
141
|
+
for base in cwd.parents:
|
|
142
|
+
candidate = base / "config.yaml"
|
|
143
|
+
if candidate.exists():
|
|
144
|
+
return candidate.resolve()
|
|
145
|
+
raise typer.BadParameter(
|
|
146
|
+
"Missing CONFIG and no 'config.yaml' found in the current or parent directories. "
|
|
147
|
+
"Run inside an experiment dir or pass a path to the config (or the experiment dir). "
|
|
148
|
+
f"Tip: use '{reader_command('ls')}' to list experiments and pass its index."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _format_index_span(indices) -> str:
|
|
153
|
+
values = [int(value) for value in indices]
|
|
154
|
+
if not values:
|
|
155
|
+
return "none"
|
|
156
|
+
if values == list(range(values[0], values[-1] + 1)):
|
|
157
|
+
if values[0] == values[-1]:
|
|
158
|
+
return str(values[0])
|
|
159
|
+
return f"{values[0]}..{values[-1]}"
|
|
160
|
+
return ", ".join(str(value) for value in values)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def format_job_arg(job: str | None) -> str | None:
|
|
164
|
+
if job is None:
|
|
165
|
+
return None
|
|
166
|
+
value = str(job).strip()
|
|
167
|
+
return value or None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def ensure_active_lifecycle(decl: WorkbenchDecl, job_path: Path, *, command_name: str) -> None:
|
|
171
|
+
lifecycle = decl.experiment.lifecycle
|
|
172
|
+
if lifecycle == "active":
|
|
173
|
+
return
|
|
174
|
+
raise typer.BadParameter(
|
|
175
|
+
f"Experiment lifecycle '{lifecycle}' is not runnable for '{command_name}'. "
|
|
176
|
+
f"Use '{reader_command('validate', job_path, '--no-files')}' to check the config or "
|
|
177
|
+
f"'{reader_command('inspect', job_path)}' for details."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def require_dataframe_records(decl: WorkbenchDecl, job_path: Path, *, runtime: ReaderRuntime) -> None:
|
|
182
|
+
layout = decl.experiment_semantics.layout
|
|
183
|
+
outputs_dir = layout.outputs_dir
|
|
184
|
+
store = runtime.record_store(
|
|
185
|
+
outputs_dir,
|
|
186
|
+
plots_subdir=layout.plots_subdir,
|
|
187
|
+
exports_subdir=layout.exports_subdir,
|
|
188
|
+
create=False,
|
|
189
|
+
)
|
|
190
|
+
if not store.catalog_exists():
|
|
191
|
+
raise RecordError(
|
|
192
|
+
f"No outputs/manifests/records.json found. Run '{reader_command('run', job_path)}' first to generate dataframe records."
|
|
193
|
+
)
|
|
194
|
+
try:
|
|
195
|
+
records = store.iter_latest_records(kind="dataframe_artifact")
|
|
196
|
+
except RecordError as exc:
|
|
197
|
+
raise RecordError(f"Could not read record catalog at {store.records_path}: {exc}") from exc
|
|
198
|
+
if not records:
|
|
199
|
+
raise RecordError(
|
|
200
|
+
f"No dataframe records listed in outputs/manifests/records.json. Run '{reader_command('run', job_path)}' first."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def resolve_pipeline_step_id(decl: WorkbenchDecl, which: str, *, job_path: Path | None = None) -> str:
|
|
205
|
+
which_str = str(which).strip()
|
|
206
|
+
pipeline = list(_load("reader_workbench.workbench.graph").resolve_workbench(decl).pipeline)
|
|
207
|
+
if any(step.id == which_str for step in pipeline):
|
|
208
|
+
return which_str
|
|
209
|
+
options = ", ".join(step.id for step in pipeline[:12])
|
|
210
|
+
steps_command = reader_command("steps", job_path) if job_path is not None else reader_command("steps")
|
|
211
|
+
raise typer.BadParameter(
|
|
212
|
+
f"Unknown pipeline step id '{which_str}'. Tip: use '{steps_command}' to list ids "
|
|
213
|
+
f"(first few: {options}{' …' if len(pipeline) > 12 else ''})."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def spec_to_dict(spec_obj) -> dict:
|
|
218
|
+
if hasattr(spec_obj, "to_dict"):
|
|
219
|
+
return spec_obj.to_dict()
|
|
220
|
+
return {
|
|
221
|
+
"id": spec_obj.id,
|
|
222
|
+
"plugin": spec_obj.plugin,
|
|
223
|
+
"reads": {
|
|
224
|
+
key: _load("reader_workbench.workbench.graph").input_ref_to_dict(value)
|
|
225
|
+
for key, value in (spec_obj.reads or {}).items()
|
|
226
|
+
},
|
|
227
|
+
"with": dict(spec_obj.with_ or {}),
|
|
228
|
+
"writes": {
|
|
229
|
+
key: _load("reader_workbench.workbench.graph").output_ref_to_dict(value)
|
|
230
|
+
for key, value in (spec_obj.writes or {}).items()
|
|
231
|
+
},
|
|
232
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from reader_workbench.errors import ReaderError
|
|
10
|
+
|
|
11
|
+
from .automation import (
|
|
12
|
+
as_click_error,
|
|
13
|
+
begin_request,
|
|
14
|
+
click_error_details,
|
|
15
|
+
emit_document,
|
|
16
|
+
end_request,
|
|
17
|
+
error_envelope,
|
|
18
|
+
json_requested,
|
|
19
|
+
reader_error_details,
|
|
20
|
+
)
|
|
21
|
+
from .shared import app
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(args: Sequence[str] | None = None) -> int:
|
|
25
|
+
argv = list(sys.argv[1:] if args is None else args)
|
|
26
|
+
tokens = begin_request(argv)
|
|
27
|
+
try:
|
|
28
|
+
command = typer.main.get_command(app)
|
|
29
|
+
result = command.main(args=argv, prog_name="reader", standalone_mode=False)
|
|
30
|
+
return int(result) if isinstance(result, int) else 0
|
|
31
|
+
except (click.exceptions.Exit, typer.Exit) as exc:
|
|
32
|
+
return int(exc.exit_code)
|
|
33
|
+
except ReaderError as exc:
|
|
34
|
+
if json_requested():
|
|
35
|
+
emit_document(error_envelope(**reader_error_details(exc)))
|
|
36
|
+
return 1
|
|
37
|
+
raise
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
click_error = as_click_error(exc)
|
|
40
|
+
if click_error is not None:
|
|
41
|
+
if json_requested():
|
|
42
|
+
emit_document(error_envelope(**click_error_details(click_error)))
|
|
43
|
+
else:
|
|
44
|
+
click_error.show()
|
|
45
|
+
return int(click_error.exit_code)
|
|
46
|
+
if not json_requested():
|
|
47
|
+
raise
|
|
48
|
+
emit_document(
|
|
49
|
+
error_envelope(
|
|
50
|
+
code="internal_error",
|
|
51
|
+
field="command",
|
|
52
|
+
reason="Reader encountered an unexpected internal error.",
|
|
53
|
+
remediation="Report this Reader defect with the command and error code; do not retry unchanged.",
|
|
54
|
+
retryable=False,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
return 1
|
|
58
|
+
finally:
|
|
59
|
+
end_request(tokens)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from . import shared
|
|
8
|
+
from ._lazy import load as _load
|
|
9
|
+
from .shared import app, emit_json, emit_json_error, normalize_output_format
|
|
10
|
+
|
|
11
|
+
maintain_app = typer.Typer(
|
|
12
|
+
add_completion=False,
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
help="Run checks that maintain the Reader source repository.",
|
|
15
|
+
)
|
|
16
|
+
REPO_ROOT_OPTION = typer.Option(Path("."), "--repo-root", metavar="DIR", help="Reader source checkout root.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _emit_report(report, *, format: str) -> None:
|
|
20
|
+
if format == "json":
|
|
21
|
+
if report.ok:
|
|
22
|
+
emit_json(report.to_payload())
|
|
23
|
+
else:
|
|
24
|
+
first_error = report.errors[0] if report.errors else "unknown maintenance check failure"
|
|
25
|
+
emit_json_error(
|
|
26
|
+
code="maintenance_check_failed",
|
|
27
|
+
field="repo_root",
|
|
28
|
+
reason=f"{report.check} check found {len(report.errors)} problem(s). First: {first_error}",
|
|
29
|
+
remediation=f"Correct the reported {report.check} problem(s), then rerun this command.",
|
|
30
|
+
)
|
|
31
|
+
return
|
|
32
|
+
status = "ok" if report.ok else "failed"
|
|
33
|
+
shared.console.print(f"{report.check} integrity {status}: {report.checked} checked")
|
|
34
|
+
for error in report.errors:
|
|
35
|
+
shared.console.print(f"- {error}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_check(name: str, *, repo_root: Path, format: str) -> None:
|
|
39
|
+
fmt = normalize_output_format(format)
|
|
40
|
+
if (
|
|
41
|
+
not repo_root.is_dir()
|
|
42
|
+
or not (repo_root / "pyproject.toml").is_file()
|
|
43
|
+
or not (repo_root / "src" / "reader_workbench").is_dir()
|
|
44
|
+
):
|
|
45
|
+
raise typer.BadParameter(
|
|
46
|
+
f"Reader source checkout not found: {repo_root}",
|
|
47
|
+
param_hint="--repo-root",
|
|
48
|
+
)
|
|
49
|
+
module = _load(f"reader_workbench.maintenance.{name}")
|
|
50
|
+
report = getattr(module, f"check_{name}")(repo_root)
|
|
51
|
+
_emit_report(report, format=fmt)
|
|
52
|
+
if not report.ok:
|
|
53
|
+
raise typer.Exit(code=1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@maintain_app.command("docs", help="Check documentation links, routes, anchors, and front matter.")
|
|
57
|
+
def docs(
|
|
58
|
+
repo_root: Path = REPO_ROOT_OPTION,
|
|
59
|
+
format: str = typer.Option(
|
|
60
|
+
"table",
|
|
61
|
+
"--format",
|
|
62
|
+
metavar="FMT",
|
|
63
|
+
help="Output format: table | json (default: table).",
|
|
64
|
+
),
|
|
65
|
+
) -> None:
|
|
66
|
+
_run_check("docs", repo_root=repo_root, format=format)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@maintain_app.command("skills", help="Check repo-local skill structure and source references.")
|
|
70
|
+
def skills(
|
|
71
|
+
repo_root: Path = REPO_ROOT_OPTION,
|
|
72
|
+
format: str = typer.Option(
|
|
73
|
+
"table",
|
|
74
|
+
"--format",
|
|
75
|
+
metavar="FMT",
|
|
76
|
+
help="Output format: table | json (default: table).",
|
|
77
|
+
),
|
|
78
|
+
) -> None:
|
|
79
|
+
_run_check("skills", repo_root=repo_root, format=format)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
app.add_typer(maintain_app, name="maintain")
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich import box
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from reader_workbench.errors import ConfigError, RecordError
|
|
11
|
+
from reader_workbench.workbench.paths import resolve_confined_sink_root, resolve_path_within_root
|
|
12
|
+
|
|
13
|
+
from . import shared
|
|
14
|
+
from ._lazy import load as _load
|
|
15
|
+
from .helpers import (
|
|
16
|
+
default_notebook_name,
|
|
17
|
+
infer_job_path,
|
|
18
|
+
load_job_models,
|
|
19
|
+
next_available_path,
|
|
20
|
+
)
|
|
21
|
+
from .shared import (
|
|
22
|
+
NOTEBOOK_MODE_OPTION,
|
|
23
|
+
app,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render_marimo_help(target: Path, *, mode: str, has_fcs: bool) -> None:
|
|
28
|
+
marimo_cmd = f"{shared.sys.executable} -m marimo {mode} {target}"
|
|
29
|
+
shared.console.print(
|
|
30
|
+
Panel.fit(
|
|
31
|
+
"Could not launch marimo automatically.\n\n"
|
|
32
|
+
"Reader does not publish a notebook dependency extra while the released Marimo and "
|
|
33
|
+
"PyMdown constraints remain unresolved. Use a separately managed and audited "
|
|
34
|
+
"environment that provides Marimo, Altair, and DuckDB, then run:\n"
|
|
35
|
+
f" {marimo_cmd}\n\n"
|
|
36
|
+
f"Notebook: [path]{target}[/path]",
|
|
37
|
+
border_style="warn",
|
|
38
|
+
box=box.ROUNDED,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def render_marimo_routes(*, target: Path, url: str, runtime_root: Path) -> None:
|
|
44
|
+
check_cmd = f"uv run marimo check {target}"
|
|
45
|
+
shared.console.print(
|
|
46
|
+
Panel.fit(
|
|
47
|
+
"Review routes:\n"
|
|
48
|
+
f" Static check: {check_cmd}\n"
|
|
49
|
+
f" Browser review: {url}\n"
|
|
50
|
+
" In-app browser: open the URL in a fresh isolated page.\n\n"
|
|
51
|
+
f"Managed runtime root: [path]{runtime_root}[/path]",
|
|
52
|
+
border_style="accent",
|
|
53
|
+
box=box.ROUNDED,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _notebook_filename(raw: str) -> str:
|
|
59
|
+
candidate = Path(raw)
|
|
60
|
+
if (
|
|
61
|
+
not raw
|
|
62
|
+
or raw != raw.strip()
|
|
63
|
+
or candidate.is_absolute()
|
|
64
|
+
or len(candidate.parts) != 1
|
|
65
|
+
or candidate in {Path("."), Path("..")}
|
|
66
|
+
or candidate.suffix != ".py"
|
|
67
|
+
):
|
|
68
|
+
raise ConfigError("--name must be a non-empty .py filename with no directory components")
|
|
69
|
+
return raw
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _launch_marimo(
|
|
73
|
+
mode: str,
|
|
74
|
+
target: Path,
|
|
75
|
+
*,
|
|
76
|
+
has_fcs: bool,
|
|
77
|
+
headless: bool = False,
|
|
78
|
+
port: int | None = None,
|
|
79
|
+
repo_root: Path | None = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
launch = _load("reader_workbench.workbench.notebooks.launch")
|
|
82
|
+
plan = launch.plan_marimo_launch(
|
|
83
|
+
mode=mode,
|
|
84
|
+
target=target,
|
|
85
|
+
headless=headless,
|
|
86
|
+
preferred_port=port,
|
|
87
|
+
base_env=os.environ.copy(),
|
|
88
|
+
repo_root=repo_root,
|
|
89
|
+
)
|
|
90
|
+
if plan.terminated_sessions:
|
|
91
|
+
shared.console.print(
|
|
92
|
+
Panel.fit(
|
|
93
|
+
f"Pruned {len(plan.terminated_sessions)} existing reader-managed Marimo session(s) "
|
|
94
|
+
"for this experiment before launch.",
|
|
95
|
+
border_style="warn",
|
|
96
|
+
box=box.ROUNDED,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
if plan.reused_session is not None:
|
|
100
|
+
shared.console.print(
|
|
101
|
+
Panel.fit(
|
|
102
|
+
f"Notebook already running: [path]{target}[/path]\n[muted]url[/muted]: {plan.url}",
|
|
103
|
+
border_style="ok",
|
|
104
|
+
box=box.ROUNDED,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
render_marimo_routes(target=target, url=plan.url, runtime_root=plan.runtime_paths.root)
|
|
108
|
+
if not headless:
|
|
109
|
+
launch.open_url(plan.url)
|
|
110
|
+
return
|
|
111
|
+
shared.console.print(
|
|
112
|
+
Panel.fit(
|
|
113
|
+
f"Launching: {' '.join(plan.cmd)}\n[muted]url[/muted]: {plan.url}",
|
|
114
|
+
border_style="accent",
|
|
115
|
+
box=box.ROUNDED,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
render_marimo_routes(target=target, url=plan.url, runtime_root=plan.runtime_paths.root)
|
|
119
|
+
try:
|
|
120
|
+
proc = shared.subprocess.Popen(plan.cmd, env=plan.env)
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
render_marimo_help(target, mode=mode, has_fcs=has_fcs)
|
|
123
|
+
raise typer.Exit(code=1) from None
|
|
124
|
+
launch.register_managed_session(
|
|
125
|
+
registry_path=plan.runtime_paths.registry_path,
|
|
126
|
+
pid=proc.pid,
|
|
127
|
+
port=plan.port,
|
|
128
|
+
host=plan.host,
|
|
129
|
+
mode=mode,
|
|
130
|
+
target=target,
|
|
131
|
+
repo_root=plan.repo_root,
|
|
132
|
+
)
|
|
133
|
+
try:
|
|
134
|
+
returncode = proc.wait()
|
|
135
|
+
finally:
|
|
136
|
+
launch.unregister_managed_session(registry_path=plan.runtime_paths.registry_path, pid=proc.pid)
|
|
137
|
+
if returncode != 0:
|
|
138
|
+
render_marimo_help(target, mode=mode, has_fcs=has_fcs)
|
|
139
|
+
raise typer.Exit(code=1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _scaffold_notebook(
|
|
143
|
+
*,
|
|
144
|
+
job: str | None,
|
|
145
|
+
name: str | None,
|
|
146
|
+
overwrite: bool,
|
|
147
|
+
new: bool,
|
|
148
|
+
refresh: bool,
|
|
149
|
+
mode: str,
|
|
150
|
+
headless: bool,
|
|
151
|
+
port: int | None,
|
|
152
|
+
) -> None:
|
|
153
|
+
try:
|
|
154
|
+
if overwrite and new:
|
|
155
|
+
raise typer.BadParameter("--overwrite cannot be combined with --new.")
|
|
156
|
+
if refresh:
|
|
157
|
+
overwrite = True
|
|
158
|
+
mode_value = (mode or "").strip().lower()
|
|
159
|
+
if mode_value not in {"edit", "run", "none"}:
|
|
160
|
+
raise typer.BadParameter("--mode must be one of: edit, run, none.")
|
|
161
|
+
job_path = infer_job_path(job)
|
|
162
|
+
exp_dir = job_path.parent
|
|
163
|
+
_, decl = load_job_models(job_path)
|
|
164
|
+
layout = decl.experiment_semantics.layout
|
|
165
|
+
outputs_dir = layout.outputs_dir
|
|
166
|
+
notebooks_cfg = layout.notebooks_subdir
|
|
167
|
+
notebook_root = outputs_dir if notebooks_cfg in ("", ".", "./") else outputs_dir / str(notebooks_cfg)
|
|
168
|
+
try:
|
|
169
|
+
nb_dir = resolve_confined_sink_root(notebook_root, root=outputs_dir, label="notebooks")
|
|
170
|
+
except ValueError as exc:
|
|
171
|
+
raise ConfigError(str(exc)) from exc
|
|
172
|
+
target_name = default_notebook_name() if name is None else _notebook_filename(name)
|
|
173
|
+
target_candidate = nb_dir / target_name
|
|
174
|
+
if target_candidate.is_symlink():
|
|
175
|
+
raise ConfigError(f"Notebook target must not be a symlink: {target_candidate}")
|
|
176
|
+
try:
|
|
177
|
+
target = resolve_path_within_root(target_candidate, root=nb_dir)
|
|
178
|
+
except ValueError as exc:
|
|
179
|
+
raise ConfigError(f"Notebook target must stay within the notebooks sink root: {target_candidate}") from exc
|
|
180
|
+
if new:
|
|
181
|
+
target = next_available_path(target)
|
|
182
|
+
has_fcs = any(path.suffix.lower() == ".fcs" for path in exp_dir.rglob("*.fcs"))
|
|
183
|
+
existed = target.exists()
|
|
184
|
+
target, created = _load("reader_workbench.workbench.notebooks").write_experiment_notebook(
|
|
185
|
+
target,
|
|
186
|
+
experiment_root=decl.experiment.root,
|
|
187
|
+
notebooks_root=nb_dir,
|
|
188
|
+
overwrite=overwrite,
|
|
189
|
+
)
|
|
190
|
+
if created:
|
|
191
|
+
if existed and overwrite:
|
|
192
|
+
status = f"✓ Notebook overwritten: [path]{target}[/path]"
|
|
193
|
+
else:
|
|
194
|
+
status = f"✓ Notebook created: [path]{target}[/path]"
|
|
195
|
+
border_style = "ok"
|
|
196
|
+
else:
|
|
197
|
+
action = "opening existing" if mode_value != "none" else "using existing"
|
|
198
|
+
status = f"Notebook already exists: [path]{target}[/path] {action}."
|
|
199
|
+
border_style = "warn"
|
|
200
|
+
shared.console.print(Panel.fit(status, border_style=border_style, box=box.ROUNDED))
|
|
201
|
+
if mode_value == "none":
|
|
202
|
+
shared.console.print(str(target))
|
|
203
|
+
return
|
|
204
|
+
_launch_marimo(mode_value, target, has_fcs=has_fcs, headless=headless, port=port)
|
|
205
|
+
except (ConfigError, RecordError) as err:
|
|
206
|
+
raise typer.BadParameter(str(err)) from err
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command(help="Scaffold an interactive marimo notebook and open it.")
|
|
210
|
+
def notebook(
|
|
211
|
+
job: str | None = typer.Argument(
|
|
212
|
+
None,
|
|
213
|
+
metavar="CONFIG|DIR|INDEX",
|
|
214
|
+
help=shared.JOB_ARG_HELP_SHORT,
|
|
215
|
+
),
|
|
216
|
+
name: str | None = typer.Option(
|
|
217
|
+
None,
|
|
218
|
+
"--name",
|
|
219
|
+
help="Notebook filename (created under outputs/notebooks). Defaults to EDA_YYYYMMDD.py.",
|
|
220
|
+
),
|
|
221
|
+
overwrite: bool = typer.Option(
|
|
222
|
+
False,
|
|
223
|
+
"--overwrite",
|
|
224
|
+
"--force",
|
|
225
|
+
help="Overwrite the selected generated notebook if it already exists.",
|
|
226
|
+
),
|
|
227
|
+
new: bool = typer.Option(
|
|
228
|
+
False,
|
|
229
|
+
"--new",
|
|
230
|
+
help="Create an additional notebook by appending a numeric suffix if needed.",
|
|
231
|
+
),
|
|
232
|
+
refresh: bool = typer.Option(
|
|
233
|
+
False,
|
|
234
|
+
"--refresh",
|
|
235
|
+
help="Regenerate the notebook even if it exists (same as --overwrite).",
|
|
236
|
+
),
|
|
237
|
+
headless: bool = typer.Option(
|
|
238
|
+
False,
|
|
239
|
+
"--headless",
|
|
240
|
+
help="Launch without opening a browser. Reader prints a loopback URL suitable for in-app review.",
|
|
241
|
+
),
|
|
242
|
+
port: int | None = typer.Option(
|
|
243
|
+
None,
|
|
244
|
+
"--port",
|
|
245
|
+
min=1,
|
|
246
|
+
max=65535,
|
|
247
|
+
help="Preferred loopback port. Defaults to a reader-managed clean port starting at 2718.",
|
|
248
|
+
),
|
|
249
|
+
mode: str = NOTEBOOK_MODE_OPTION,
|
|
250
|
+
):
|
|
251
|
+
_scaffold_notebook(
|
|
252
|
+
job=job,
|
|
253
|
+
name=name,
|
|
254
|
+
overwrite=overwrite,
|
|
255
|
+
new=new,
|
|
256
|
+
refresh=refresh,
|
|
257
|
+
headless=headless,
|
|
258
|
+
port=port,
|
|
259
|
+
mode=mode,
|
|
260
|
+
)
|