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.
Files changed (293) hide show
  1. reader_workbench/__init__.py +22 -0
  2. reader_workbench/__main__.py +4 -0
  3. reader_workbench/_version.py +17 -0
  4. reader_workbench/api/__init__.py +74 -0
  5. reader_workbench/api/_record_reads.py +75 -0
  6. reader_workbench/api/artifacts.py +79 -0
  7. reader_workbench/api/facade.py +538 -0
  8. reader_workbench/api/models.py +285 -0
  9. reader_workbench/api/notebooks.py +63 -0
  10. reader_workbench/contracts/__init__.py +18 -0
  11. reader_workbench/contracts/builtins/__init__.py +36 -0
  12. reader_workbench/contracts/builtins/cytometry.py +140 -0
  13. reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
  14. reader_workbench/contracts/builtins/generic.py +21 -0
  15. reader_workbench/contracts/builtins/logic.py +149 -0
  16. reader_workbench/contracts/builtins/plate_reader.py +47 -0
  17. reader_workbench/contracts/catalog.py +257 -0
  18. reader_workbench/contracts/model.py +109 -0
  19. reader_workbench/domains/__init__.py +1 -0
  20. reader_workbench/domains/cytometry/__init__.py +3 -0
  21. reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
  22. reader_workbench/domains/cytometry/analysis/events.py +182 -0
  23. reader_workbench/domains/cytometry/analysis/gating.py +175 -0
  24. reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
  25. reader_workbench/domains/cytometry/io/__init__.py +3 -0
  26. reader_workbench/domains/cytometry/io/fcs.py +135 -0
  27. reader_workbench/domains/cytometry/plots/__init__.py +5 -0
  28. reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
  29. reader_workbench/domains/logic/__init__.py +3 -0
  30. reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
  31. reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
  32. reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
  33. reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
  34. reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
  35. reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
  36. reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
  37. reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
  38. reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
  39. reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
  40. reader_workbench/domains/logic/four_state_vector/config.py +214 -0
  41. reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
  42. reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
  43. reader_workbench/domains/logic/four_state_vector/math.py +191 -0
  44. reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
  45. reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
  46. reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
  47. reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
  48. reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
  49. reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
  50. reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
  51. reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
  52. reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
  53. reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
  54. reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
  55. reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
  56. reader_workbench/domains/logic/treatment_columns.py +42 -0
  57. reader_workbench/domains/plate_reader/__init__.py +1 -0
  58. reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
  59. reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
  60. reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
  61. reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
  62. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
  63. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
  64. reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
  65. reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
  66. reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
  67. reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
  68. reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
  69. reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
  70. reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
  71. reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
  72. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
  73. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
  74. reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
  75. reader_workbench/domains/plate_reader/io/__init__.py +6 -0
  76. reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
  77. reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
  78. reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
  79. reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
  80. reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
  81. reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
  82. reader_workbench/domains/plate_reader/ordering.py +59 -0
  83. reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
  84. reader_workbench/domains/plate_reader/plots/_data.py +29 -0
  85. reader_workbench/domains/plate_reader/plots/common.py +346 -0
  86. reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
  87. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
  88. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
  89. reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
  90. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
  91. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
  92. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
  93. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
  94. reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
  95. reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
  96. reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
  97. reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
  98. reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
  99. reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
  100. reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
  101. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
  102. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
  103. reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
  104. reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
  105. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
  106. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
  107. reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
  108. reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
  109. reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
  110. reader_workbench/domains/time_series/__init__.py +29 -0
  111. reader_workbench/domains/time_series/aggregation.py +60 -0
  112. reader_workbench/domains/time_series/contracts.py +368 -0
  113. reader_workbench/domains/time_series/reduction.py +395 -0
  114. reader_workbench/errors.py +55 -0
  115. reader_workbench/maintenance/__init__.py +6 -0
  116. reader_workbench/maintenance/docs.py +335 -0
  117. reader_workbench/maintenance/model.py +28 -0
  118. reader_workbench/maintenance/release.py +39 -0
  119. reader_workbench/maintenance/skills.py +124 -0
  120. reader_workbench/plotting/__init__.py +20 -0
  121. reader_workbench/plotting/mpl.py +56 -0
  122. reader_workbench/plotting/sinks.py +69 -0
  123. reader_workbench/plotting/style.py +175 -0
  124. reader_workbench/plotting/utils.py +27 -0
  125. reader_workbench/plugins/__init__.py +1 -0
  126. reader_workbench/plugins/catalog.py +33 -0
  127. reader_workbench/plugins/export/__init__.py +0 -0
  128. reader_workbench/plugins/export/_paths.py +21 -0
  129. reader_workbench/plugins/export/csv.py +41 -0
  130. reader_workbench/plugins/export/xlsx.py +44 -0
  131. reader_workbench/plugins/ingest/__init__.py +0 -0
  132. reader_workbench/plugins/ingest/_discovery.py +58 -0
  133. reader_workbench/plugins/ingest/discovery_policy.py +66 -0
  134. reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
  135. reader_workbench/plugins/ingest/synergy_h1.py +234 -0
  136. reader_workbench/plugins/manifests/__init__.py +1 -0
  137. reader_workbench/plugins/manifests/export.py +29 -0
  138. reader_workbench/plugins/manifests/ingest.py +29 -0
  139. reader_workbench/plugins/manifests/plot.py +161 -0
  140. reader_workbench/plugins/manifests/transform.py +172 -0
  141. reader_workbench/plugins/manifests/validator.py +18 -0
  142. reader_workbench/plugins/plot/__init__.py +0 -0
  143. reader_workbench/plugins/plot/_shared.py +55 -0
  144. reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
  145. reader_workbench/plugins/plot/distributions.py +58 -0
  146. reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
  147. reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
  148. reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
  149. reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
  150. reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
  151. reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
  152. reader_workbench/plugins/plot/logic_symmetry.py +56 -0
  153. reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
  154. reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
  155. reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
  156. reader_workbench/plugins/plot/time_series.py +114 -0
  157. reader_workbench/plugins/plot/ts_and_snap.py +210 -0
  158. reader_workbench/plugins/transform/__init__.py +0 -0
  159. reader_workbench/plugins/transform/_four_state_vector.py +204 -0
  160. reader_workbench/plugins/transform/_labeling.py +109 -0
  161. reader_workbench/plugins/transform/alias.py +70 -0
  162. reader_workbench/plugins/transform/assay_labels.py +62 -0
  163. reader_workbench/plugins/transform/blank.py +79 -0
  164. reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
  165. reader_workbench/plugins/transform/cytometry_gating.py +120 -0
  166. reader_workbench/plugins/transform/fold_change.py +79 -0
  167. reader_workbench/plugins/transform/four_state_event_window.py +93 -0
  168. reader_workbench/plugins/transform/four_state_vector.py +62 -0
  169. reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
  170. reader_workbench/plugins/transform/logic_symmetry.py +67 -0
  171. reader_workbench/plugins/transform/outlier_filter.py +60 -0
  172. reader_workbench/plugins/transform/overflow.py +197 -0
  173. reader_workbench/plugins/transform/ratio.py +237 -0
  174. reader_workbench/plugins/transform/sample_map.py +170 -0
  175. reader_workbench/plugins/transform/sample_metadata.py +94 -0
  176. reader_workbench/plugins/validator/__init__.py +1 -0
  177. reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
  178. reader_workbench/protocols/__init__.py +80 -0
  179. reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
  180. reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
  181. reader_workbench/protocols/builtins.py +1656 -0
  182. reader_workbench/protocols/compiler.py +22 -0
  183. reader_workbench/protocols/compilers/__init__.py +1 -0
  184. reader_workbench/protocols/compilers/common.py +100 -0
  185. reader_workbench/protocols/compilers/cytometry.py +87 -0
  186. reader_workbench/protocols/compilers/generic.py +14 -0
  187. reader_workbench/protocols/compilers/logic.py +245 -0
  188. reader_workbench/protocols/compilers/plate_reader.py +937 -0
  189. reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
  190. reader_workbench/protocols/model.py +1486 -0
  191. reader_workbench/protocols/semantic_coverage.py +234 -0
  192. reader_workbench/runtime/__init__.py +12 -0
  193. reader_workbench/runtime/builtin.py +23 -0
  194. reader_workbench/runtime/model.py +42 -0
  195. reader_workbench/workbench/__init__.py +60 -0
  196. reader_workbench/workbench/assets/__init__.py +22 -0
  197. reader_workbench/workbench/assets/types.py +118 -0
  198. reader_workbench/workbench/audit/__init__.py +5 -0
  199. reader_workbench/workbench/audit/experiments.py +307 -0
  200. reader_workbench/workbench/audit/staging.py +187 -0
  201. reader_workbench/workbench/cli/__init__.py +51 -0
  202. reader_workbench/workbench/cli/_lazy.py +9 -0
  203. reader_workbench/workbench/cli/_records_view.py +150 -0
  204. reader_workbench/workbench/cli/_surface_execution.py +443 -0
  205. reader_workbench/workbench/cli/audit.py +95 -0
  206. reader_workbench/workbench/cli/automation.py +229 -0
  207. reader_workbench/workbench/cli/demo.py +46 -0
  208. reader_workbench/workbench/cli/dop.py +91 -0
  209. reader_workbench/workbench/cli/experiments.py +635 -0
  210. reader_workbench/workbench/cli/helpers.py +232 -0
  211. reader_workbench/workbench/cli/main.py +59 -0
  212. reader_workbench/workbench/cli/maintenance.py +82 -0
  213. reader_workbench/workbench/cli/notebooks.py +260 -0
  214. reader_workbench/workbench/cli/pagination.py +117 -0
  215. reader_workbench/workbench/cli/protocols.py +336 -0
  216. reader_workbench/workbench/cli/shared.py +309 -0
  217. reader_workbench/workbench/cli/surfaces.py +534 -0
  218. reader_workbench/workbench/cli/verification.py +128 -0
  219. reader_workbench/workbench/commands.py +10 -0
  220. reader_workbench/workbench/config/__init__.py +47 -0
  221. reader_workbench/workbench/config/identity.py +13 -0
  222. reader_workbench/workbench/config/load.py +405 -0
  223. reader_workbench/workbench/config/model.py +274 -0
  224. reader_workbench/workbench/context.py +26 -0
  225. reader_workbench/workbench/decl/__init__.py +31 -0
  226. reader_workbench/workbench/decl/build.py +190 -0
  227. reader_workbench/workbench/decl/model.py +81 -0
  228. reader_workbench/workbench/dop/__init__.py +12 -0
  229. reader_workbench/workbench/dop/builtins.py +261 -0
  230. reader_workbench/workbench/dop/model.py +209 -0
  231. reader_workbench/workbench/engine/__init__.py +42 -0
  232. reader_workbench/workbench/engine/_shared.py +76 -0
  233. reader_workbench/workbench/engine/contracts.py +283 -0
  234. reader_workbench/workbench/engine/execution.py +326 -0
  235. reader_workbench/workbench/engine/file_outputs.py +260 -0
  236. reader_workbench/workbench/engine/inputs.py +161 -0
  237. reader_workbench/workbench/engine/invocations.py +507 -0
  238. reader_workbench/workbench/engine/planning.py +72 -0
  239. reader_workbench/workbench/engine/runtime.py +464 -0
  240. reader_workbench/workbench/engine/setup.py +149 -0
  241. reader_workbench/workbench/engine/validation.py +684 -0
  242. reader_workbench/workbench/experiment/__init__.py +47 -0
  243. reader_workbench/workbench/experiment/model.py +381 -0
  244. reader_workbench/workbench/experiments.py +133 -0
  245. reader_workbench/workbench/graph/__init__.py +47 -0
  246. reader_workbench/workbench/graph/nodes.py +102 -0
  247. reader_workbench/workbench/graph/normalize.py +177 -0
  248. reader_workbench/workbench/graph/refs.py +148 -0
  249. reader_workbench/workbench/input_discovery.py +19 -0
  250. reader_workbench/workbench/inspection/__init__.py +3 -0
  251. reader_workbench/workbench/inspection/catalogs.py +128 -0
  252. reader_workbench/workbench/inspection/common.py +92 -0
  253. reader_workbench/workbench/inspection/dop.py +64 -0
  254. reader_workbench/workbench/inspection/experiments.py +449 -0
  255. reader_workbench/workbench/inspection/inventory.py +68 -0
  256. reader_workbench/workbench/inspection/protocols.py +368 -0
  257. reader_workbench/workbench/inspection/readiness.py +333 -0
  258. reader_workbench/workbench/inspection/reports.py +367 -0
  259. reader_workbench/workbench/inspection/results.py +166 -0
  260. reader_workbench/workbench/inspection/runtime.py +287 -0
  261. reader_workbench/workbench/inspection/semantics.py +192 -0
  262. reader_workbench/workbench/inspection/validation.py +30 -0
  263. reader_workbench/workbench/notebooks/__init__.py +17 -0
  264. reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
  265. reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
  266. reader_workbench/workbench/notebooks/components/__init__.py +21 -0
  267. reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
  268. reader_workbench/workbench/notebooks/components/overview.py +119 -0
  269. reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
  270. reader_workbench/workbench/notebooks/launch.py +274 -0
  271. reader_workbench/workbench/notebooks/presentation.py +136 -0
  272. reader_workbench/workbench/notebooks/scaffold.py +60 -0
  273. reader_workbench/workbench/ontology.py +78 -0
  274. reader_workbench/workbench/paths.py +44 -0
  275. reader_workbench/workbench/ports/__init__.py +31 -0
  276. reader_workbench/workbench/ports/model.py +168 -0
  277. reader_workbench/workbench/records/__init__.py +44 -0
  278. reader_workbench/workbench/records/epoch.py +329 -0
  279. reader_workbench/workbench/records/evidence.py +247 -0
  280. reader_workbench/workbench/records/identity.py +87 -0
  281. reader_workbench/workbench/records/locking.py +185 -0
  282. reader_workbench/workbench/records/model.py +711 -0
  283. reader_workbench/workbench/records/sources.py +73 -0
  284. reader_workbench/workbench/records/store.py +1022 -0
  285. reader_workbench/workbench/records/verification.py +998 -0
  286. reader_workbench/workbench/registry.py +333 -0
  287. reader_workbench/workbench/spec_overrides.py +215 -0
  288. reader_workbench-1.0.0.dist-info/METADATA +91 -0
  289. reader_workbench-1.0.0.dist-info/RECORD +293 -0
  290. reader_workbench-1.0.0.dist-info/WHEEL +5 -0
  291. reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
  292. reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
  293. reader_workbench-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,464 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from rich import box
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from reader_workbench.errors import ConfigError, ExecutionError, InvocationFinalizationError
13
+ from reader_workbench.plotting.mpl import ensure_mpl_cache_dir
14
+ from reader_workbench.runtime import ReaderRuntime, builtin_runtime
15
+ from reader_workbench.workbench.commands import reader_command
16
+ from reader_workbench.workbench.decl import WorkbenchDecl, load_workbench_decl
17
+ from reader_workbench.workbench.graph import ensure_unique_workbench_ids, resolve_workbench
18
+ from reader_workbench.workbench.paths import resolve_confined_sink_root
19
+ from reader_workbench.workbench.records import current_build_identity
20
+ from reader_workbench.workbench.records.locking import provenance_lock_scope
21
+
22
+ from ._shared import collect_categories
23
+ from .execution import run_steps
24
+ from .invocations import (
25
+ ExecutionResult,
26
+ InvocationLedger,
27
+ ProducedRecordRevision,
28
+ SelectedSteps,
29
+ capture_revision_snapshot,
30
+ declared_input_projection,
31
+ produced_record_revisions,
32
+ )
33
+ from .planning import build_next_steps, explain
34
+ from .setup import (
35
+ build_run_context,
36
+ close_reader_logger,
37
+ configure_logger,
38
+ normalize_log_level,
39
+ resolve_palette_book,
40
+ slice_pipeline_steps,
41
+ )
42
+ from .validation import (
43
+ _assert_no_source_record_output_collisions,
44
+ _planned_output_record_ids,
45
+ _source_record_preflight_issues,
46
+ validation_summary,
47
+ )
48
+
49
+
50
+ def run_spec(
51
+ decl: WorkbenchDecl,
52
+ *,
53
+ resume_from: str | None = None,
54
+ until: str | None = None,
55
+ reset_records: bool = False,
56
+ dry_run: bool = False,
57
+ log_level: str = "INFO",
58
+ verbose: bool = True,
59
+ console: Console | None = None,
60
+ include_pipeline: bool = True,
61
+ include_plots: bool = True,
62
+ include_exports: bool = True,
63
+ plot_specs=None,
64
+ export_specs=None,
65
+ job_label: str | None = None,
66
+ show_next_steps: bool = False,
67
+ runtime: ReaderRuntime | None = None,
68
+ ) -> ExecutionResult:
69
+ os.environ.setdefault("ARROW_LOG_LEVEL", "FATAL")
70
+ runtime = runtime or builtin_runtime()
71
+ layout = decl.experiment_semantics.layout
72
+ try:
73
+ out_dir = resolve_confined_sink_root(layout.outputs_dir, root=decl.experiment.root, label="outputs")
74
+ except ValueError as exc:
75
+ raise ConfigError(str(exc)) from exc
76
+ if out_dir.exists() and not out_dir.is_dir():
77
+ raise ConfigError(f"paths.outputs points to a file: {out_dir}")
78
+
79
+ console = console or Console()
80
+ normalize_log_level(log_level)
81
+
82
+ workbench = resolve_workbench(decl)
83
+ pipeline_steps = list(workbench.pipeline)
84
+ plot_steps = list(plot_specs) if plot_specs is not None else list(workbench.plots)
85
+ export_steps = list(export_specs) if export_specs is not None else list(workbench.exports)
86
+ ensure_unique_workbench_ids(pipeline_steps, plot_steps, export_steps)
87
+
88
+ if not include_pipeline:
89
+ if resume_from or until:
90
+ raise ConfigError(
91
+ f"--from/--until require pipeline execution; use {reader_command('run')} for sliced runs."
92
+ )
93
+ pipeline_steps = []
94
+ else:
95
+ pipeline_steps = slice_pipeline_steps(pipeline_steps, resume_from=resume_from, until=until)
96
+
97
+ if reset_records:
98
+ if dry_run:
99
+ raise ConfigError("reset_records cannot be used for a dry run")
100
+ if not include_pipeline or pipeline_steps != list(workbench.pipeline):
101
+ raise ConfigError("reset_records requires a complete pipeline run without --from, --until, or --only")
102
+
103
+ if not include_plots:
104
+ plot_steps = []
105
+ if not include_exports:
106
+ export_steps = []
107
+
108
+ all_steps = pipeline_steps + plot_steps + export_steps
109
+ categories = collect_categories(list(workbench.plugin_steps()))
110
+ registry = runtime.plugins if categories else None
111
+ selected_steps = SelectedSteps(
112
+ pipeline=tuple(step.id for step in pipeline_steps),
113
+ plots=tuple(step.id for step in plot_steps),
114
+ exports=tuple(step.id for step in export_steps),
115
+ )
116
+ requested_operations = [
117
+ operation
118
+ for operation, enabled in (
119
+ ("run", include_pipeline),
120
+ ("plot", include_plots),
121
+ ("export", include_exports),
122
+ )
123
+ if enabled
124
+ ]
125
+ operation = requested_operations[0] if len(requested_operations) == 1 else "mixed"
126
+
127
+ if dry_run:
128
+ validation_summary(
129
+ decl,
130
+ check_files=False,
131
+ exp_root=decl.experiment.root,
132
+ runtime=runtime,
133
+ plot_specs_override=plot_steps,
134
+ export_specs_override=export_steps,
135
+ )
136
+ console.print(Panel.fit("DRY RUN — printing plan", border_style="yellow", box=box.ROUNDED))
137
+ explain(
138
+ decl,
139
+ console=console,
140
+ registry=registry,
141
+ plot_specs=plot_steps,
142
+ export_specs=export_steps,
143
+ runtime=runtime,
144
+ )
145
+ return ExecutionResult(
146
+ invocation_id=None,
147
+ provenance_epoch_id=None,
148
+ operation=operation,
149
+ status="planned",
150
+ dry_run=True,
151
+ selected_steps=selected_steps,
152
+ produced_record_revisions=(),
153
+ ledger_path=None,
154
+ )
155
+
156
+ if registry is not None:
157
+ planned_record_ids = _planned_output_record_ids(
158
+ pipeline_items=pipeline_steps,
159
+ plot_items=plot_steps,
160
+ export_items=export_steps,
161
+ registry=registry,
162
+ )
163
+ _assert_no_source_record_output_collisions(
164
+ items=pipeline_steps,
165
+ planned_record_ids=planned_record_ids,
166
+ experiment_root=decl.experiment.root,
167
+ )
168
+ _, source_record_issues = _source_record_preflight_issues(
169
+ items=pipeline_steps,
170
+ registry=registry,
171
+ contracts=runtime.contracts,
172
+ )
173
+ if source_record_issues:
174
+ raise ConfigError("Run preflight failed before output mutation: " + source_record_issues[0])
175
+
176
+ plots_cfg = layout.plots_subdir
177
+ exports_cfg = layout.exports_subdir
178
+ store = runtime.record_store(
179
+ out_dir,
180
+ plots_subdir=(plots_cfg if plots_cfg not in ("", ".", "./") else None),
181
+ exports_subdir=(exports_cfg if exports_cfg not in ("", ".", "./") else None),
182
+ experiment_root=decl.experiment.root,
183
+ create=False,
184
+ )
185
+ preserved_output_paths = (layout.subdir_path("notebooks"),)
186
+ store.validate_no_interrupted_epoch()
187
+ if reset_records:
188
+ store.validate_generated_epoch_reset(preserved_paths=preserved_output_paths)
189
+ out_dir.mkdir(parents=True, exist_ok=True)
190
+ # The existing writer lease lives under manifests. Create only its parent
191
+ # before acquisition; catalog and generated-output layout belong inside the
192
+ # guarded section below.
193
+ store.manifests_dir.mkdir(parents=True, exist_ok=True)
194
+ with provenance_lock_scope(
195
+ store.provenance_lock,
196
+ acquire_error=ExecutionError("Could not acquire the experiment writer lease"),
197
+ release_error=ExecutionError(
198
+ "Execution completed, but Reader could not release the experiment writer lease. "
199
+ "Do not retry blindly; run reader verify before continuing."
200
+ ),
201
+ release_note="Reader also could not release the experiment writer lease",
202
+ ):
203
+ return _run_spec_locked(
204
+ decl=decl,
205
+ runtime=runtime,
206
+ out_dir=out_dir,
207
+ store=store,
208
+ log_level=log_level,
209
+ all_steps=all_steps,
210
+ pipeline_steps=pipeline_steps,
211
+ plot_steps=plot_steps,
212
+ export_steps=export_steps,
213
+ registry=registry,
214
+ selected_steps=selected_steps,
215
+ operation=operation,
216
+ reset_records=reset_records,
217
+ verbose=verbose,
218
+ console=console,
219
+ include_pipeline=include_pipeline,
220
+ show_next_steps=show_next_steps,
221
+ job_label=job_label,
222
+ )
223
+
224
+
225
+ def _run_spec_locked(
226
+ *,
227
+ decl: WorkbenchDecl,
228
+ runtime: ReaderRuntime,
229
+ out_dir: Path,
230
+ store: Any,
231
+ log_level: str,
232
+ all_steps: list[Any],
233
+ pipeline_steps: list[Any],
234
+ plot_steps: list[Any],
235
+ export_steps: list[Any],
236
+ registry: Any,
237
+ selected_steps: SelectedSteps,
238
+ operation: str,
239
+ reset_records: bool,
240
+ verbose: bool,
241
+ console: Console,
242
+ include_pipeline: bool,
243
+ show_next_steps: bool,
244
+ job_label: str | None,
245
+ ) -> ExecutionResult:
246
+ """Execute one mutating plan while the caller holds the experiment writer lease."""
247
+
248
+ # Repeat the lightweight preflight under the writer lease. The first check
249
+ # prevents ordinary layout mutation; this one closes the race with another
250
+ # process that may have interrupted a reset before we acquired the lease.
251
+ store.validate_no_interrupted_epoch()
252
+ if not reset_records:
253
+ store.ensure_layout()
254
+ if plot_steps:
255
+ ensure_mpl_cache_dir()
256
+ if reset_records:
257
+ close_reader_logger()
258
+ try:
259
+ provenance_epoch_id = store.reset_generated_epoch(
260
+ preserved_paths=(decl.experiment_semantics.layout.subdir_path("notebooks"),)
261
+ )
262
+ except BaseException as reset_failure:
263
+ try:
264
+ configure_logger(out_dir=out_dir, log_level=log_level, verbose=verbose, console=console)
265
+ except BaseException as logger_failure:
266
+ reset_failure.add_note(
267
+ "Reader also could not restore the process logger after the reset failed "
268
+ f"({type(logger_failure).__name__})."
269
+ )
270
+ raise
271
+ else:
272
+ provenance_epoch_id = store.provenance_epoch_id()
273
+ store.bind_provenance_epoch(provenance_epoch_id)
274
+ logger = configure_logger(out_dir=out_dir, log_level=log_level, verbose=verbose, console=console)
275
+ ledger = InvocationLedger(
276
+ experiment_root=decl.experiment.root,
277
+ outputs_dir=out_dir,
278
+ provenance_epoch_id=provenance_epoch_id,
279
+ epoch_guard=store.assert_provenance_epoch,
280
+ writer_lock=store.provenance_lock,
281
+ )
282
+ ctx = build_run_context(
283
+ decl=decl,
284
+ runtime=runtime,
285
+ out_dir=out_dir,
286
+ store=store,
287
+ logger=logger,
288
+ palette_book=resolve_palette_book(decl=decl, steps=all_steps, dry_run=False),
289
+ )
290
+ steps_by_phase = {
291
+ "pipeline": pipeline_steps,
292
+ "plots": plot_steps,
293
+ "exports": export_steps,
294
+ }
295
+ attempt = ledger.append_attempt(
296
+ config_digest=ctx.config_digest,
297
+ build_identity=current_build_identity(),
298
+ operation=operation,
299
+ selected_step_ids=selected_steps.to_dict(),
300
+ declared_inputs=declared_input_projection(
301
+ steps_by_phase=steps_by_phase,
302
+ experiment_root=decl.experiment.root,
303
+ ),
304
+ )
305
+ before_revisions: dict[str, dict] = {}
306
+
307
+ try:
308
+ before_revisions = capture_revision_snapshot(store)
309
+ ctx.logger.info(
310
+ "run • pipeline=%d step(s)%s • plots=%d spec(s) • exports=%d spec(s)",
311
+ len(pipeline_steps),
312
+ " (skipped)" if not include_pipeline else "",
313
+ len(plot_steps),
314
+ len(export_steps),
315
+ )
316
+ if not verbose:
317
+ console.print(
318
+ f"[accent]run[/accent] • pipeline={len(pipeline_steps)} step(s){' (skipped)' if not include_pipeline else ''} "
319
+ f"• plots={len(plot_steps)} spec(s) • exports={len(export_steps)} spec(s)"
320
+ )
321
+
322
+ if pipeline_steps and registry is None:
323
+ raise ConfigError("pipeline execution requires plugin workbench steps")
324
+ if plot_steps and registry is None:
325
+ raise ConfigError("plot execution requires plugin workbench specs")
326
+ if export_steps and registry is None:
327
+ raise ConfigError("export execution requires plugin workbench specs")
328
+
329
+ run_steps(
330
+ items=pipeline_steps,
331
+ phase="pipeline",
332
+ verbose=verbose,
333
+ console=console,
334
+ ctx=ctx,
335
+ store=store,
336
+ registry=registry,
337
+ )
338
+ if plot_steps:
339
+ run_steps(
340
+ items=plot_steps,
341
+ phase="plots",
342
+ verbose=verbose,
343
+ console=console,
344
+ ctx=ctx,
345
+ store=store,
346
+ registry=registry,
347
+ )
348
+ if export_steps:
349
+ run_steps(
350
+ items=export_steps,
351
+ phase="exports",
352
+ verbose=verbose,
353
+ console=console,
354
+ ctx=ctx,
355
+ store=store,
356
+ registry=registry,
357
+ )
358
+
359
+ revision_payloads = produced_record_revisions(
360
+ before=before_revisions,
361
+ after=capture_revision_snapshot(store),
362
+ )
363
+ except BaseException as failure:
364
+ try:
365
+ revisions = produced_record_revisions(
366
+ before=before_revisions,
367
+ after=capture_revision_snapshot(store),
368
+ )
369
+ except BaseException:
370
+ revisions = []
371
+ try:
372
+ ledger.append_result(
373
+ attempt,
374
+ exit_status=130 if isinstance(failure, KeyboardInterrupt) else 1,
375
+ produced_record_revisions=revisions,
376
+ failure=failure,
377
+ )
378
+ except BaseException as ledger_failure:
379
+ failure.add_note(
380
+ f"Reader also could not persist the failed invocation result ({type(ledger_failure).__name__})."
381
+ )
382
+ raise
383
+
384
+ try:
385
+ ledger.append_result(
386
+ attempt,
387
+ exit_status=0,
388
+ produced_record_revisions=revision_payloads,
389
+ )
390
+ except BaseException as failure:
391
+ raise InvocationFinalizationError(
392
+ "Execution records were committed, but Reader could not confirm the invocation result. "
393
+ "Keep the committed evidence and run reader verify before handoff.",
394
+ invocation_id=attempt.invocation_id,
395
+ produced_record_revisions=tuple(dict(item) for item in revision_payloads),
396
+ ) from failure
397
+
398
+ if show_next_steps:
399
+ next_steps = build_next_steps(decl, job_label=job_label, runtime=runtime)
400
+ table = Table(show_header=True, header_style="bold", box=box.ROUNDED, expand=False)
401
+ table.add_column("Command", style="accent")
402
+ table.add_column("What it does")
403
+ for command, description in next_steps:
404
+ table.add_row(command, description)
405
+ console.print(
406
+ Panel(
407
+ table,
408
+ border_style="green",
409
+ box=box.ROUNDED,
410
+ title=f"Records generated in [path]{out_dir}[/path]",
411
+ title_align="left",
412
+ )
413
+ )
414
+ else:
415
+ console.print(Panel.fit(f"✓ Done — outputs in {str(out_dir)}", border_style="green", box=box.ROUNDED))
416
+ return ExecutionResult(
417
+ invocation_id=attempt.invocation_id,
418
+ provenance_epoch_id=provenance_epoch_id,
419
+ operation=operation,
420
+ status="succeeded",
421
+ dry_run=False,
422
+ selected_steps=selected_steps,
423
+ produced_record_revisions=tuple(
424
+ ProducedRecordRevision.from_payload(revision) for revision in revision_payloads
425
+ ),
426
+ ledger_path=ledger.path,
427
+ )
428
+
429
+
430
+ def run_job(
431
+ spec_path: Path,
432
+ *,
433
+ resume_from: str | None = None,
434
+ until: str | None = None,
435
+ reset_records: bool = False,
436
+ dry_run: bool = False,
437
+ log_level: str = "INFO",
438
+ verbose: bool = True,
439
+ console: Console | None = None,
440
+ include_pipeline: bool = True,
441
+ include_plots: bool = True,
442
+ include_exports: bool = True,
443
+ job_label: str | None = None,
444
+ show_next_steps: bool = False,
445
+ runtime: ReaderRuntime | None = None,
446
+ ) -> ExecutionResult:
447
+ runtime = runtime or builtin_runtime()
448
+ decl = load_workbench_decl(spec_path, protocols=runtime.protocols)
449
+ return run_spec(
450
+ decl,
451
+ resume_from=resume_from,
452
+ until=until,
453
+ reset_records=reset_records,
454
+ dry_run=dry_run,
455
+ log_level=log_level,
456
+ verbose=verbose,
457
+ console=console,
458
+ include_pipeline=include_pipeline,
459
+ include_plots=include_plots,
460
+ include_exports=include_exports,
461
+ job_label=job_label,
462
+ show_next_steps=show_next_steps,
463
+ runtime=runtime,
464
+ )
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import logging
5
+ from importlib import import_module
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from rich.console import Console
10
+ from rich.logging import RichHandler
11
+
12
+ from reader_workbench.errors import ConfigError
13
+ from reader_workbench.runtime import ReaderRuntime
14
+ from reader_workbench.workbench.context import RunContext
15
+ from reader_workbench.workbench.decl import WorkbenchDecl
16
+ from reader_workbench.workbench.records import RecordStore
17
+ from reader_workbench.workbench.records.identity import digest_json
18
+
19
+ from ._shared import needs_plot_palette
20
+
21
+
22
+ def normalize_log_level(log_level: str) -> str:
23
+ level_name = str(log_level).upper()
24
+ valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
25
+ if level_name not in valid_levels:
26
+ raise ConfigError(f"Invalid log level {log_level!r}. Choose one of: {sorted(valid_levels)}")
27
+ return level_name
28
+
29
+
30
+ def close_reader_logger() -> None:
31
+ """Close Reader's process-global handlers before moving an owned log file."""
32
+
33
+ logger = logging.getLogger("reader")
34
+ for handler in list(logger.handlers):
35
+ logger.removeHandler(handler)
36
+ with contextlib.suppress(Exception):
37
+ handler.close()
38
+
39
+
40
+ def configure_logger(*, out_dir: Path, log_level: str, verbose: bool, console: Console) -> logging.Logger:
41
+ level_name = normalize_log_level(log_level)
42
+
43
+ close_reader_logger()
44
+ logger = logging.getLogger("reader")
45
+ logger.setLevel(getattr(logging, level_name))
46
+
47
+ try:
48
+ file_handler = logging.FileHandler(out_dir / "reader.log", encoding="utf-8")
49
+ except OSError as err:
50
+ raise ConfigError(f"Cannot write reader.log in outputs directory: {out_dir}") from err
51
+ file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
52
+
53
+ rich_handler = RichHandler(
54
+ console=console, markup=True, rich_tracebacks=True, show_level=True, show_time=False, show_path=False
55
+ )
56
+ rich_handler.setLevel(getattr(logging, level_name) if verbose else logging.WARNING)
57
+
58
+ logger.addHandler(file_handler)
59
+ logger.addHandler(rich_handler)
60
+ return logger
61
+
62
+
63
+ def resolve_palette_book(*, decl: WorkbenchDecl, steps: list[Any], dry_run: bool) -> Any:
64
+ palette = decl.plotting_palette
65
+ if palette is not None:
66
+ if not isinstance(palette, str) or not palette.strip():
67
+ raise ConfigError("plotting.palette must be a non-empty string or null")
68
+ palette = palette.strip()
69
+
70
+ if dry_run or not needs_plot_palette(steps, palette):
71
+ return None
72
+
73
+ try:
74
+ mod = import_module("reader_workbench.plotting.style")
75
+ except ModuleNotFoundError as err:
76
+ missing = str(err.name or "")
77
+ if missing == "matplotlib" or missing.startswith("matplotlib."):
78
+ raise ConfigError(
79
+ "Plot palettes require matplotlib; install plotting dependencies or set plotting.palette: null."
80
+ ) from err
81
+ raise ConfigError(f"Failed to import plot palette support: {err}") from err
82
+ except Exception as err:
83
+ raise ConfigError(f"Failed to initialize plot palette support: {err}") from err
84
+
85
+ palette_book_cls = getattr(mod, "PaletteBook", None)
86
+ available_palettes = getattr(mod, "available_palettes", None)
87
+ if palette_book_cls is None or available_palettes is None:
88
+ raise ConfigError(
89
+ "reader_workbench.plotting.style must expose PaletteBook and available_palettes for plotting.palette support."
90
+ )
91
+
92
+ if palette not in available_palettes():
93
+ raise ConfigError(
94
+ f"Unknown palette {palette!r}. Available: {available_palettes()} (or set plotting.palette: null)."
95
+ )
96
+ return palette_book_cls(palette)
97
+
98
+
99
+ def build_run_context(
100
+ *,
101
+ decl: WorkbenchDecl,
102
+ runtime: ReaderRuntime,
103
+ out_dir: Path,
104
+ store: RecordStore,
105
+ logger: logging.Logger,
106
+ palette_book: Any,
107
+ ) -> RunContext:
108
+ config_digest = decl.config_digest or digest_json(
109
+ {
110
+ "synthetic_workbench": {
111
+ "experiment_id": decl.experiment.id,
112
+ "root": str(decl.experiment.root),
113
+ }
114
+ }
115
+ )
116
+ return RunContext(
117
+ exp_dir=decl.experiment.root,
118
+ outputs_dir=out_dir,
119
+ artifacts_dir=store.artifacts_dir,
120
+ plots_dir=store.plots_dir,
121
+ exports_dir=store.exports_dir,
122
+ records_path=store.records_path,
123
+ logger=logger,
124
+ palette_book=palette_book,
125
+ experiment=decl.experiment_semantics,
126
+ protocol=runtime.bind_protocol(decl.experiment_semantics.protocol),
127
+ config_digest=config_digest,
128
+ )
129
+
130
+
131
+ def slice_pipeline_steps(steps: list[Any], *, resume_from: str | None, until: str | None) -> list[Any]:
132
+ selected = list(steps)
133
+ start_index = 0
134
+ if resume_from:
135
+ try:
136
+ start_index = next(i for i, step in enumerate(selected) if step.id == resume_from)
137
+ selected = selected[start_index:]
138
+ except StopIteration:
139
+ raise ConfigError(f"--from: step id '{resume_from}' not found") from None
140
+ if until:
141
+ try:
142
+ until_index = next(i for i, step in enumerate(steps) if step.id == until)
143
+ except StopIteration:
144
+ raise ConfigError(f"--until: step id '{until}' not found") from None
145
+ if resume_from and start_index > until_index:
146
+ raise ConfigError(f"--from '{resume_from}' comes after --until '{until}' in pipeline order")
147
+ relative_index = until_index - start_index
148
+ selected = selected[: relative_index + 1]
149
+ return selected