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,534 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich import box
7
+ from rich.panel import Panel
8
+
9
+ from reader_workbench.errors import ReaderError
10
+
11
+ from . import _records_view, _surface_execution, shared
12
+ from ._lazy import load as _load
13
+ from .helpers import (
14
+ ensure_active_lifecycle,
15
+ find_nearest_experiments_dir,
16
+ find_year_jobs,
17
+ infer_job_path,
18
+ load_job_models,
19
+ require_dataframe_records,
20
+ )
21
+ from .shared import (
22
+ EXPORT_EXCLUDE_OPTION,
23
+ EXPORT_INPUT_OPTION,
24
+ EXPORT_ONLY_OPTION,
25
+ EXPORT_SET_OPTION,
26
+ PLOT_EXCLUDE_OPTION,
27
+ PLOT_INPUT_OPTION,
28
+ PLOT_ONLY_OPTION,
29
+ PLOT_SET_OPTION,
30
+ abort,
31
+ app,
32
+ emit_json,
33
+ handle_reader_error,
34
+ normalize_output_format,
35
+ table,
36
+ )
37
+
38
+
39
+ def _spec_overrides():
40
+ return _surface_execution.spec_overrides()
41
+
42
+
43
+ def _validate_list_mode_flags(
44
+ *,
45
+ list_only: bool,
46
+ dry_run: bool,
47
+ inputs: list[str] | None,
48
+ sets: list[str] | None,
49
+ ) -> None:
50
+ _surface_execution.validate_list_mode_flags(
51
+ list_only=list_only,
52
+ dry_run=dry_run,
53
+ inputs=inputs,
54
+ sets=sets,
55
+ )
56
+
57
+
58
+ def _apply_surface_overrides(
59
+ selected,
60
+ *,
61
+ inputs: list[str] | None,
62
+ sets: list[str] | None,
63
+ experiment_root: Path,
64
+ resources,
65
+ ):
66
+ return _surface_execution.apply_surface_overrides(
67
+ selected,
68
+ inputs=inputs,
69
+ sets=sets,
70
+ experiment_root=experiment_root,
71
+ resources=resources,
72
+ )
73
+
74
+
75
+ def _validate_plot_job_for_execution(
76
+ job_path: Path,
77
+ *,
78
+ only: list[str] | None,
79
+ exclude: list[str] | None,
80
+ dry_run: bool,
81
+ inputs: list[str] | None,
82
+ sets: list[str] | None,
83
+ ) -> None:
84
+ _surface_execution.validate_plot_job_for_execution(
85
+ job_path,
86
+ only=only,
87
+ exclude=exclude,
88
+ dry_run=dry_run,
89
+ inputs=inputs,
90
+ sets=sets,
91
+ ensure_active_lifecycle_fn=ensure_active_lifecycle,
92
+ require_dataframe_records_fn=require_dataframe_records,
93
+ )
94
+
95
+
96
+ def _render_surface_specs_table(
97
+ *, title_text: str, selected, runtime, record_producers, summaries: dict[str, str]
98
+ ) -> None:
99
+ _surface_execution.render_surface_specs_table(
100
+ title_text=title_text,
101
+ selected=selected,
102
+ runtime=runtime,
103
+ record_producers=record_producers,
104
+ summaries=summaries,
105
+ )
106
+
107
+
108
+ def _surface_next_steps(*, job_hint: str | None, output_dir: Path, include_plot: bool, include_export: bool) -> None:
109
+ _surface_execution.surface_next_steps(
110
+ job_hint=job_hint,
111
+ output_dir=output_dir,
112
+ include_plot=include_plot,
113
+ include_export=include_export,
114
+ )
115
+
116
+
117
+ def _run_plot_job(
118
+ job_path: Path,
119
+ *,
120
+ job_hint: str | None,
121
+ only: list[str] | None,
122
+ exclude: list[str] | None,
123
+ list_only: bool,
124
+ format: str,
125
+ dry_run: bool,
126
+ log_level: str,
127
+ inputs: list[str] | None,
128
+ sets: list[str] | None,
129
+ ) -> None:
130
+ _surface_execution.run_plot_job(
131
+ job_path,
132
+ job_hint=job_hint,
133
+ only=only,
134
+ exclude=exclude,
135
+ list_only=list_only,
136
+ format=format,
137
+ dry_run=dry_run,
138
+ log_level=log_level,
139
+ inputs=inputs,
140
+ sets=sets,
141
+ ensure_active_lifecycle_fn=ensure_active_lifecycle,
142
+ require_dataframe_records_fn=require_dataframe_records,
143
+ )
144
+
145
+
146
+ def _run_export_job(
147
+ job_path: Path,
148
+ *,
149
+ job_hint: str | None,
150
+ only: list[str] | None,
151
+ exclude: list[str] | None,
152
+ list_only: bool,
153
+ format: str,
154
+ dry_run: bool,
155
+ log_level: str,
156
+ inputs: list[str] | None,
157
+ sets: list[str] | None,
158
+ ) -> None:
159
+ _surface_execution.run_export_job(
160
+ job_path,
161
+ job_hint=job_hint,
162
+ only=only,
163
+ exclude=exclude,
164
+ list_only=list_only,
165
+ format=format,
166
+ dry_run=dry_run,
167
+ log_level=log_level,
168
+ inputs=inputs,
169
+ sets=sets,
170
+ ensure_active_lifecycle_fn=ensure_active_lifecycle,
171
+ require_dataframe_records_fn=require_dataframe_records,
172
+ )
173
+
174
+
175
+ @app.command(help="List plot specs or save plot files from existing dataframe records.")
176
+ def plot(
177
+ job: str | None = typer.Argument(
178
+ None,
179
+ metavar="CONFIG|DIR|INDEX",
180
+ help=shared.JOB_ARG_HELP_SHORT,
181
+ ),
182
+ year: str | None = typer.Option(
183
+ None, "--year", metavar="YYYY", help="Run plots for all experiments under experiments/YYYY."
184
+ ),
185
+ root: str | None = typer.Option(
186
+ None,
187
+ "--root",
188
+ metavar="DIR",
189
+ help="Override experiments root when using --year (default: nearest experiments/).",
190
+ ),
191
+ only: list[str] = PLOT_ONLY_OPTION,
192
+ exclude: list[str] = PLOT_EXCLUDE_OPTION,
193
+ list_only: bool = typer.Option(False, "--list", help="List plot specs for this config and exit."),
194
+ format: str = typer.Option(
195
+ "table", "--format", metavar="FMT", help="Output format for --list: table | json (default: table)."
196
+ ),
197
+ dry_run: bool = typer.Option(
198
+ False, "--dry-run", help="Plan only: validate and print the plot plan without executing."
199
+ ),
200
+ log_level: str = typer.Option(
201
+ "INFO",
202
+ "--log-level",
203
+ metavar="LEVEL",
204
+ help="Logging level: DEBUG | INFO | WARNING | ERROR | CRITICAL (default: INFO).",
205
+ ),
206
+ inputs: list[str] = PLOT_INPUT_OPTION,
207
+ sets: list[str] = PLOT_SET_OPTION,
208
+ ):
209
+ if root and not year:
210
+ raise typer.BadParameter("--root is only valid with --year")
211
+ _validate_list_mode_flags(list_only=list_only, dry_run=dry_run, inputs=inputs, sets=sets)
212
+ fmt = normalize_output_format(format)
213
+ if year:
214
+ if fmt == "json":
215
+ raise typer.BadParameter("--format json is only supported for single-experiment plot listings")
216
+ if job is not None:
217
+ raise typer.BadParameter("--year cannot be combined with CONFIG|DIR|INDEX")
218
+ root_path = find_nearest_experiments_dir(Path.cwd()) if root is None else Path(root).resolve()
219
+ jobs = find_year_jobs(year, root_path)
220
+ if not list_only:
221
+ failures: list[tuple[Path, str]] = []
222
+ for job_path in jobs:
223
+ try:
224
+ _validate_plot_job_for_execution(
225
+ job_path,
226
+ only=only,
227
+ exclude=exclude,
228
+ dry_run=dry_run,
229
+ inputs=inputs,
230
+ sets=sets,
231
+ )
232
+ except (ReaderError, typer.BadParameter) as exc:
233
+ failures.append((job_path, str(exc)))
234
+ if failures:
235
+ lines = [f"{len(failures)} experiment(s) are not runnable for year {year}:"]
236
+ lines += [f"- {path.parent.name}: {msg}" for path, msg in failures]
237
+ abort("\n".join(lines))
238
+ shared.console.print(
239
+ Panel.fit(
240
+ f"Plotting {len(jobs)} experiment(s) for {year} under [path]{root_path}[/path].",
241
+ border_style="accent",
242
+ box=box.ROUNDED,
243
+ )
244
+ )
245
+ failures: list[tuple[Path, str]] = []
246
+ total = len(jobs)
247
+ for index, job_path in enumerate(jobs, 1):
248
+ exp_name = job_path.parent.name
249
+ cmd_line = " ".join(
250
+ _spec_overrides().build_surface_command(
251
+ "reader plot",
252
+ job_path,
253
+ only=only,
254
+ exclude=exclude,
255
+ list_only=list_only,
256
+ dry_run=dry_run,
257
+ log_level=log_level,
258
+ inputs=inputs,
259
+ sets=sets,
260
+ )
261
+ )
262
+ shared.console.print(f"[accent]{index}/{total}[/accent] {exp_name}")
263
+ shared.console.print(f"[muted]{cmd_line}[/muted]")
264
+ try:
265
+ _run_plot_job(
266
+ job_path,
267
+ job_hint=str(job_path),
268
+ only=only,
269
+ exclude=exclude,
270
+ list_only=list_only,
271
+ format=fmt,
272
+ dry_run=dry_run,
273
+ log_level=log_level,
274
+ inputs=inputs,
275
+ sets=sets,
276
+ )
277
+ except (ReaderError, typer.BadParameter) as exc:
278
+ failures.append((job_path, str(exc)))
279
+ shared.console.print(
280
+ Panel.fit(f"[error]✗ {exp_name}: {exc}[/error]", border_style="error", box=box.ROUNDED)
281
+ )
282
+ if failures:
283
+ lines = [f"{len(failures)} experiment(s) failed while plotting year {year}:"]
284
+ lines += [f"- {path.parent.name}: {msg}" for path, msg in failures]
285
+ abort("\n".join(lines))
286
+ return
287
+
288
+ try:
289
+ job_path = infer_job_path(job)
290
+ _run_plot_job(
291
+ job_path,
292
+ job_hint=job,
293
+ only=only,
294
+ exclude=exclude,
295
+ list_only=list_only,
296
+ format=fmt,
297
+ dry_run=dry_run,
298
+ log_level=log_level,
299
+ inputs=inputs,
300
+ sets=sets,
301
+ )
302
+ except ReaderError as err:
303
+ handle_reader_error(err)
304
+
305
+
306
+ @app.command(help="List export specs or write export files from existing dataframe records.")
307
+ def export(
308
+ job: str | None = typer.Argument(
309
+ None,
310
+ metavar="CONFIG|DIR|INDEX",
311
+ help=shared.JOB_ARG_HELP_SHORT,
312
+ ),
313
+ only: list[str] = EXPORT_ONLY_OPTION,
314
+ exclude: list[str] = EXPORT_EXCLUDE_OPTION,
315
+ list_only: bool = typer.Option(False, "--list", help="List export specs for this config and exit."),
316
+ format: str = typer.Option(
317
+ "table", "--format", metavar="FMT", help="Output format for --list: table | json (default: table)."
318
+ ),
319
+ dry_run: bool = typer.Option(
320
+ False, "--dry-run", help="Plan only: validate and print the export plan without executing."
321
+ ),
322
+ log_level: str = typer.Option(
323
+ "INFO",
324
+ "--log-level",
325
+ metavar="LEVEL",
326
+ help="Logging level: DEBUG | INFO | WARNING | ERROR | CRITICAL (default: INFO).",
327
+ ),
328
+ inputs: list[str] = EXPORT_INPUT_OPTION,
329
+ sets: list[str] = EXPORT_SET_OPTION,
330
+ ):
331
+ try:
332
+ _validate_list_mode_flags(list_only=list_only, dry_run=dry_run, inputs=inputs, sets=sets)
333
+ job_path = infer_job_path(job)
334
+ _run_export_job(
335
+ job_path,
336
+ job_hint=job,
337
+ only=only,
338
+ exclude=exclude,
339
+ list_only=list_only,
340
+ format=format,
341
+ dry_run=dry_run,
342
+ log_level=log_level,
343
+ inputs=inputs,
344
+ sets=sets,
345
+ )
346
+ except ReaderError as err:
347
+ handle_reader_error(err)
348
+
349
+
350
+ @app.command(help="List current records from outputs/manifests/records.json.")
351
+ def records(
352
+ job: str | None = typer.Argument(
353
+ None,
354
+ metavar="[CONFIG]",
355
+ help=shared.JOB_ARG_HELP_WITH_DEFAULT,
356
+ ),
357
+ all: bool = typer.Option(
358
+ False,
359
+ "--all",
360
+ help="Include prior and retired record identities with revision counts.",
361
+ ),
362
+ format: str = typer.Option(
363
+ "table", "--format", metavar="FMT", help="Output format: table | json (default: table)."
364
+ ),
365
+ limit: int | None = typer.Option(
366
+ None,
367
+ "--limit",
368
+ metavar="N",
369
+ help="Maximum records per JSON page (default: 25; maximum: 100).",
370
+ ),
371
+ continuation: str | None = typer.Option(
372
+ None,
373
+ "--continuation",
374
+ metavar="TOKEN",
375
+ help="Opaque continuation token from a previous JSON page.",
376
+ ),
377
+ ):
378
+ try:
379
+ job_path = infer_job_path(job)
380
+ _records_view.render_records(
381
+ job_path=job_path,
382
+ all_revisions=all,
383
+ format=format,
384
+ limit=limit,
385
+ continuation=continuation,
386
+ )
387
+ except ReaderError as err:
388
+ handle_reader_error(err)
389
+
390
+
391
+ @app.command(help="List pipeline steps and bindings for a config.")
392
+ def steps(
393
+ job: str | None = typer.Argument(
394
+ None,
395
+ metavar="[CONFIG]",
396
+ help=shared.JOB_ARG_HELP_WITH_DEFAULT,
397
+ ),
398
+ format: str = typer.Option(
399
+ "table", "--format", metavar="FMT", help="Output format: table | json (default: table)."
400
+ ),
401
+ ):
402
+ try:
403
+ job_path = infer_job_path(job)
404
+ spec, decl = load_job_models(job_path)
405
+ except ReaderError as err:
406
+ handle_reader_error(err)
407
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
408
+ fmt = normalize_output_format(format)
409
+ workbench = _load("reader_workbench.workbench.graph").resolve_workbench(decl)
410
+ pipeline = list(workbench.pipeline)
411
+ inspection_experiments = _load("reader_workbench.workbench.inspection.experiments")
412
+ inspection_runtime = _load("reader_workbench.workbench.inspection.runtime")
413
+ payload = inspection_experiments.experiment_steps_payload(job_path=job_path, spec=spec, decl=decl, runtime=runtime)
414
+ if fmt == "json":
415
+ emit_json(payload)
416
+ return
417
+ listing = table("Steps")
418
+ listing.add_column("#", justify="right", style="muted")
419
+ listing.add_column("stage", style="accent")
420
+ listing.add_column("id", style="accent", overflow="fold")
421
+ listing.add_column("plugin", overflow="fold")
422
+ listing.add_column("from", overflow="fold")
423
+ listing.add_column("writes", overflow="fold")
424
+ for index, item in enumerate(payload["implementation"]["compiled"]["pipeline"], 1):
425
+ from_refs = ", ".join(inspection_runtime.render_read_binding(entry) for entry in item["reads"]) or "—"
426
+ writes = (
427
+ ", ".join(
428
+ (f"{entry['label']} -> {entry['display']}" if entry.get("kind") == "dataframe" else str(entry["label"]))
429
+ for entry in item["writes"]
430
+ )
431
+ or "—"
432
+ )
433
+ listing.add_row(str(index), str(item["stage"]), str(item["id"]), str(item["plugin"]), from_refs, writes)
434
+ shared.console.print(
435
+ Panel(listing, border_style="accent", box=box.ROUNDED, subtitle=f"[muted]{len(pipeline)} total[/muted]")
436
+ )
437
+
438
+
439
+ @app.command(help="List plugins by category, domain, and family.")
440
+ def plugins(
441
+ category: str | None = typer.Option(
442
+ None, "--category", metavar="NAME", help="Filter by category: ingest | transform | plot | export | validator"
443
+ ),
444
+ domain: str | None = typer.Option(
445
+ None,
446
+ "--domain",
447
+ metavar="NAME",
448
+ help="Filter by domain, for example: plate_reader | cytometry | logic | generic",
449
+ ),
450
+ family: str | None = typer.Option(
451
+ None,
452
+ "--family",
453
+ metavar="NAME",
454
+ help="Filter by family, for example: time_series | metadata_merge | workbook_ingest",
455
+ ),
456
+ protocol: str | None = typer.Option(
457
+ None, "--protocol", metavar="ID", help="Limit to plugins used by the named protocol's default plan."
458
+ ),
459
+ format: str = typer.Option(
460
+ "table", "--format", metavar="FMT", help="Output format: table | json (default: table)."
461
+ ),
462
+ limit: int | None = typer.Option(
463
+ None,
464
+ "--limit",
465
+ metavar="N",
466
+ help="Maximum plugins per JSON page (default: 25; maximum: 100).",
467
+ ),
468
+ continuation: str | None = typer.Option(
469
+ None,
470
+ "--continuation",
471
+ metavar="TOKEN",
472
+ help="Opaque continuation token from a previous JSON page.",
473
+ ),
474
+ ):
475
+ protocol = protocol if isinstance(protocol, str) else None
476
+ fmt = normalize_output_format(format)
477
+ limit, continuation = shared.normalize_paging_options(limit, continuation)
478
+ shared.require_json_paging(format=fmt, limit=limit, continuation=continuation)
479
+ try:
480
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
481
+ descriptors = runtime.plugins.catalog().filter(category=category, domain=domain, family=family)
482
+ if protocol:
483
+ bound_protocol = runtime.bind_protocol(_load("reader_workbench.protocols").ProtocolBinding(id=protocol))
484
+ plan = bound_protocol.compile()
485
+ allowed_plugins = {step.plugin for step in (*plan.pipeline, *plan.plots, *plan.exports)}
486
+ descriptors = [descriptor for descriptor in descriptors if descriptor.plugin in allowed_plugins]
487
+ except ReaderError as err:
488
+ handle_reader_error(err)
489
+ if fmt == "json":
490
+ payload = _load("reader_workbench.workbench.inspection.catalogs").plugin_registry_payload(
491
+ descriptors=descriptors,
492
+ category=category,
493
+ domain=domain,
494
+ family=family,
495
+ protocol=protocol,
496
+ )
497
+ page = shared.page_json_collection(
498
+ payload["plugins"],
499
+ key=lambda item: str(item["plugin"]),
500
+ surface="plugins",
501
+ selection=payload["selection"],
502
+ limit=limit,
503
+ continuation=continuation,
504
+ )
505
+ payload["plugins"] = list(page.items)
506
+ emit_json(payload, truncated=page.truncated, continuation=page.continuation)
507
+ return
508
+ listing = table("Plugins")
509
+ listing.add_column("category", style="accent")
510
+ listing.add_column("domain")
511
+ listing.add_column("family")
512
+ listing.add_column("key")
513
+ listing.add_column("summary", overflow="fold")
514
+ listing.add_column("class", style="muted", overflow="fold")
515
+ for descriptor in descriptors:
516
+ listing.add_row(
517
+ descriptor.category,
518
+ descriptor.domain,
519
+ descriptor.family,
520
+ descriptor.key,
521
+ descriptor.summary,
522
+ f"{descriptor.cls.__module__}.{descriptor.cls.__name__}",
523
+ )
524
+ shared.console.print(
525
+ Panel(
526
+ listing,
527
+ border_style="accent",
528
+ box=box.ROUNDED,
529
+ subtitle=(
530
+ f"[muted]{len(descriptors)} plugin(s) discovered"
531
+ f"{f' • protocol: {protocol}' if protocol else ''}[/muted]"
532
+ ),
533
+ )
534
+ )
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+ from rich import box
5
+ from rich.panel import Panel
6
+
7
+ from reader_workbench.errors import ReaderError
8
+
9
+ from . import shared
10
+ from ._lazy import load as _load
11
+ from .helpers import infer_job_path, load_job_models
12
+ from .shared import app, emit_json, emit_json_error, normalize_output_format, table
13
+
14
+
15
+ @app.command(help="Verify current records against source, config, build, upstream revision, and artifact evidence.")
16
+ def verify(
17
+ job: str | None = typer.Argument(
18
+ None,
19
+ metavar="[CONFIG]",
20
+ help=shared.JOB_ARG_HELP_WITH_DEFAULT,
21
+ ),
22
+ format: str = typer.Option(
23
+ "table", "--format", metavar="FMT", help="Output format: table | json (default: table)."
24
+ ),
25
+ ) -> None:
26
+ fmt = normalize_output_format(format)
27
+ try:
28
+ job_path = infer_job_path(job)
29
+ _spec, decl = load_job_models(job_path)
30
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
31
+ workbench = _load("reader_workbench.workbench.graph").resolve_workbench(decl)
32
+ layout = decl.experiment_semantics.layout
33
+ store = runtime.record_store(
34
+ layout.outputs_dir,
35
+ plots_subdir=layout.plots_subdir,
36
+ exports_subdir=layout.exports_subdir,
37
+ experiment_root=decl.experiment.root,
38
+ create=False,
39
+ )
40
+ report = _load("reader_workbench.workbench.records").verify_record_store(
41
+ store,
42
+ experiment_root=decl.experiment.root,
43
+ expected_config_digest=decl.config_digest,
44
+ scope=_load("reader_workbench.workbench.inspection.runtime").workbench_record_verification_scope(
45
+ workbench,
46
+ runtime=runtime,
47
+ ),
48
+ )
49
+ except ReaderError as exc:
50
+ report = {
51
+ "schema": "reader.verify/v1",
52
+ "status": "failed",
53
+ "summary": {
54
+ "checked": 0,
55
+ "failed": 1,
56
+ "unverifiable": 0,
57
+ "invocations_checked": 0,
58
+ "invocation_failures": 0,
59
+ },
60
+ "issues": [
61
+ {
62
+ "code": "experiment.invalid",
63
+ "field": "config",
64
+ "reason": str(exc),
65
+ "remediation": "Correct the experiment path or configuration, then verify again.",
66
+ "retryable": False,
67
+ }
68
+ ],
69
+ "records": [],
70
+ }
71
+
72
+ if fmt == "json":
73
+ if report["status"] == "ok":
74
+ emit_json(report)
75
+ else:
76
+ issues = list(report.get("issues") or [])
77
+ for row in list(report.get("records") or []):
78
+ issues.extend(row.get("issues") or [])
79
+ issue = issues[0] if issues else {}
80
+ emit_json_error(
81
+ code=str(issue.get("code") or "verification_failed"),
82
+ field=str(issue.get("field") or "records"),
83
+ reason=str(issue.get("reason") or "Record verification failed."),
84
+ remediation=str(
85
+ issue.get("remediation") or "Repair or regenerate the affected records, then verify again."
86
+ ),
87
+ retryable=bool(issue.get("retryable", False)),
88
+ )
89
+ else:
90
+ _render_report(report)
91
+ if report["status"] != "ok":
92
+ raise typer.Exit(code=1)
93
+
94
+
95
+ def _render_report(report: dict[str, object]) -> None:
96
+ summary = report["summary"]
97
+ status = str(report["status"])
98
+ color = "ok" if status == "ok" else ("warn" if status == "unverifiable" else "error")
99
+ shared.console.print(
100
+ Panel.fit(
101
+ f"[{color}]{status}[/{color}] • checked={summary['checked']} • "
102
+ f"failed={summary['failed']} • unverifiable={summary['unverifiable']} • "
103
+ f"invocations={summary['invocations_checked']} • "
104
+ f"invocation failures={summary['invocation_failures']}",
105
+ title="Record verification",
106
+ border_style=color,
107
+ box=box.ROUNDED,
108
+ )
109
+ )
110
+ rows = list(report.get("records") or [])
111
+ issues = list(report.get("issues") or [])
112
+ for row in rows:
113
+ issues.extend(row.get("issues") or [])
114
+ if not issues:
115
+ return
116
+ listing = table("Verification issues")
117
+ listing.add_column("Code", style="accent")
118
+ listing.add_column("Field")
119
+ listing.add_column("Reason", overflow="fold")
120
+ listing.add_column("Remediation", overflow="fold")
121
+ for issue in issues:
122
+ listing.add_row(
123
+ str(issue["code"]),
124
+ str(issue["field"]),
125
+ str(issue["reason"]),
126
+ str(issue["remediation"]),
127
+ )
128
+ shared.console.print(listing)
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def reader_command(*parts: object) -> str:
5
+ tokens = ["uv run reader"]
6
+ for part in parts:
7
+ text = str(part).strip()
8
+ if text:
9
+ tokens.append(text)
10
+ return " ".join(tokens)