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,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
+ )