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,153 @@
1
+ import marimo
2
+
3
+ __generated_with = "0.23.14"
4
+ app = marimo.App(width="medium")
5
+
6
+
7
+ @app.cell(hide_code=True)
8
+ def _():
9
+ from pathlib import Path
10
+
11
+ import marimo as mo
12
+ from reader_workbench.api import read_artifact, read_dataframe, records, verify
13
+ from reader_workbench.api.notebooks import (
14
+ build_notebook_deliverable_selector,
15
+ build_notebook_overview,
16
+ collect_notebook_deliverables,
17
+ load_notebook_context,
18
+ render_notebook_deliverable_viewport,
19
+ render_notebook_overview_panel,
20
+ )
21
+
22
+ return (
23
+ Path,
24
+ mo,
25
+ read_artifact,
26
+ read_dataframe,
27
+ records,
28
+ verify,
29
+ build_notebook_deliverable_selector,
30
+ build_notebook_overview,
31
+ collect_notebook_deliverables,
32
+ load_notebook_context,
33
+ render_notebook_deliverable_viewport,
34
+ render_notebook_overview_panel,
35
+ )
36
+
37
+
38
+ @app.cell(hide_code=True)
39
+ def _(Path, build_notebook_overview, load_notebook_context):
40
+ notebook_context = load_notebook_context(Path(__file__).resolve())
41
+ experiment = notebook_context.experiment
42
+ outputs_dir = notebook_context.outputs_dir
43
+ notebooks_dir = notebook_context.notebooks_dir
44
+ notebook_overview = build_notebook_overview(
45
+ experiment_id=experiment.identity.id,
46
+ experiment_title=experiment.identity.title or "",
47
+ protocol_id=experiment.identity.protocol,
48
+ experiment_root=notebook_context.experiment_root,
49
+ outputs_dir=outputs_dir,
50
+ notebooks_dir=notebooks_dir,
51
+ pipeline_step_ids=notebook_context.pipeline_step_ids,
52
+ )
53
+ return experiment, notebook_overview, outputs_dir
54
+
55
+
56
+ @app.cell(hide_code=True)
57
+ def _(experiment, records, verify):
58
+ _record_catalog = records(experiment)
59
+ _verification = verify(experiment)
60
+ record_entries = _record_catalog.entries
61
+ verification_issues = _verification.issues
62
+ verification_records = _verification.records
63
+ verification_status = _verification.status
64
+ return record_entries, verification_issues, verification_records, verification_status
65
+
66
+
67
+ @app.cell(hide_code=True)
68
+ def _(
69
+ collect_notebook_deliverables,
70
+ outputs_dir,
71
+ record_entries,
72
+ verification_issues,
73
+ verification_records,
74
+ verification_status,
75
+ ):
76
+ deliverables = collect_notebook_deliverables(
77
+ record_entries,
78
+ outputs_dir=outputs_dir,
79
+ verification_status=verification_status,
80
+ verification_issues=verification_issues,
81
+ verification_records=verification_records,
82
+ )
83
+ return (deliverables,)
84
+
85
+
86
+ @app.cell(hide_code=True)
87
+ def _(mo, notebook_overview, render_notebook_overview_panel):
88
+ eda_overview_panel = render_notebook_overview_panel(mo, notebook_overview)
89
+ return (eda_overview_panel,)
90
+
91
+
92
+ @app.cell(hide_code=True)
93
+ def _(build_notebook_deliverable_selector, deliverables, mo):
94
+ deliverable_selector = build_notebook_deliverable_selector(mo, deliverables)
95
+ return (deliverable_selector,)
96
+
97
+
98
+ @app.cell(hide_code=True)
99
+ def _(
100
+ deliverable_selector,
101
+ deliverables,
102
+ experiment,
103
+ mo,
104
+ read_artifact,
105
+ read_dataframe,
106
+ render_notebook_deliverable_viewport,
107
+ ):
108
+ def _load_dataframe(_record_id, _revision, _revision_digest):
109
+ return read_dataframe(
110
+ experiment,
111
+ _record_id,
112
+ revision=_revision,
113
+ revision_digest=_revision_digest,
114
+ row_limit=200,
115
+ ).dataframe
116
+
117
+ def _load_artifact(_record_id, _revision, _revision_digest, _path):
118
+ return read_artifact(
119
+ experiment,
120
+ _record_id,
121
+ revision=_revision,
122
+ revision_digest=_revision_digest,
123
+ path=_path,
124
+ ).content
125
+
126
+ deliverables_panel = render_notebook_deliverable_viewport(
127
+ mo,
128
+ deliverables,
129
+ deliverable_selector,
130
+ dataframe_loader=_load_dataframe,
131
+ artifact_loader=_load_artifact,
132
+ )
133
+ return (deliverables_panel,)
134
+
135
+
136
+ @app.cell(hide_code=True)
137
+ def _(deliverables_panel, eda_overview_panel, mo):
138
+ notebook_panel = mo.vstack(
139
+ [
140
+ eda_overview_panel,
141
+ deliverables_panel,
142
+ ]
143
+ )
144
+ return (notebook_panel,)
145
+
146
+
147
+ @app.cell(hide_code=True)
148
+ def _(notebook_panel):
149
+ notebook_panel
150
+
151
+
152
+ if __name__ == "__main__":
153
+ app.run()
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import signal
5
+ import socket
6
+ import sys
7
+ import time
8
+ import webbrowser
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from reader_workbench.errors import ConfigError
13
+
14
+ from ._launch_registry import (
15
+ MarimoSessionRecord,
16
+ )
17
+ from ._launch_registry import (
18
+ load_registry as _load_registry,
19
+ )
20
+ from ._launch_registry import (
21
+ prune_registry as _prune_registry,
22
+ )
23
+ from ._launch_registry import (
24
+ session_matches_current_inputs as _session_matches_current_inputs,
25
+ )
26
+ from ._launch_registry import (
27
+ write_registry as _write_registry,
28
+ )
29
+ from ._launch_runtime import (
30
+ MarimoRuntimePaths,
31
+ )
32
+ from ._launch_runtime import (
33
+ build_env as _build_env,
34
+ )
35
+ from ._launch_runtime import (
36
+ find_experiment_root as _find_experiment_root,
37
+ )
38
+ from ._launch_runtime import (
39
+ resolve_repo_root as _resolve_repo_root,
40
+ )
41
+ from ._launch_runtime import (
42
+ runtime_fingerprint as _runtime_fingerprint,
43
+ )
44
+ from ._launch_runtime import (
45
+ runtime_paths_for_repo_root as _runtime_paths_for_repo_root,
46
+ )
47
+ from ._launch_runtime import (
48
+ target_signature as _target_signature,
49
+ )
50
+
51
+ DEFAULT_HOST = "127.0.0.1"
52
+ DEFAULT_PORT = 2718
53
+ DEFAULT_PORT_SCAN_LIMIT = 32
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class MarimoLaunchPlan:
58
+ cmd: tuple[str, ...]
59
+ env: dict[str, str]
60
+ url: str
61
+ port: int
62
+ host: str
63
+ target: Path
64
+ repo_root: Path
65
+ runtime_paths: MarimoRuntimePaths
66
+ reused_session: MarimoSessionRecord | None = None
67
+ terminated_sessions: tuple[MarimoSessionRecord, ...] = ()
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class _LaunchContext:
72
+ resolved_target: Path
73
+ experiment_root: Path
74
+ repo_root: Path
75
+ notebook_mtime_ns: int
76
+ notebook_size_bytes: int
77
+ runtime_fingerprint: str
78
+
79
+
80
+ def _pid_is_live(pid: int) -> bool:
81
+ try:
82
+ os.kill(pid, 0)
83
+ except ProcessLookupError:
84
+ return False
85
+ except PermissionError:
86
+ return True
87
+ return True
88
+
89
+
90
+ def _port_is_open(host: str, port: int, *, timeout: float = 0.15) -> bool:
91
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
92
+ sock.settimeout(timeout)
93
+ return sock.connect_ex((host, port)) == 0
94
+
95
+
96
+ def _terminate_pid(pid: int, *, grace_seconds: float = 1.0) -> bool:
97
+ try:
98
+ os.kill(pid, signal.SIGTERM)
99
+ except ProcessLookupError:
100
+ return True
101
+ except PermissionError:
102
+ return False
103
+
104
+ deadline = time.monotonic() + grace_seconds
105
+ while time.monotonic() < deadline:
106
+ if not _pid_is_live(pid):
107
+ return True
108
+ time.sleep(0.05)
109
+
110
+ try:
111
+ os.kill(pid, signal.SIGKILL)
112
+ except ProcessLookupError:
113
+ return True
114
+ except PermissionError:
115
+ return False
116
+ return not _pid_is_live(pid)
117
+
118
+
119
+ def _resolve_launch_context(target: Path, *, repo_root: Path | None = None) -> _LaunchContext:
120
+ resolved_target = target.resolve()
121
+ experiment_root = _find_experiment_root(resolved_target)
122
+ resolved_repo_root = _resolve_repo_root(resolved_target, repo_root=repo_root)
123
+ notebook_mtime_ns, notebook_size_bytes = _target_signature(resolved_target)
124
+ return _LaunchContext(
125
+ resolved_target=resolved_target,
126
+ experiment_root=experiment_root,
127
+ repo_root=resolved_repo_root,
128
+ notebook_mtime_ns=notebook_mtime_ns,
129
+ notebook_size_bytes=notebook_size_bytes,
130
+ runtime_fingerprint=_runtime_fingerprint(resolved_repo_root),
131
+ )
132
+
133
+
134
+ def _choose_port(
135
+ *,
136
+ host: str,
137
+ preferred_port: int | None,
138
+ records: list[MarimoSessionRecord],
139
+ scan_limit: int = DEFAULT_PORT_SCAN_LIMIT,
140
+ ) -> int:
141
+ occupied_ports = {record.port for record in records if _pid_is_live(record.pid)}
142
+
143
+ if preferred_port is not None:
144
+ if preferred_port in occupied_ports or _port_is_open(host, preferred_port):
145
+ raise ConfigError(f"Port {preferred_port} is already in use on {host}")
146
+ return preferred_port
147
+
148
+ for port in range(DEFAULT_PORT, DEFAULT_PORT + scan_limit):
149
+ if port in occupied_ports:
150
+ continue
151
+ if not _port_is_open(host, port):
152
+ return port
153
+ raise ConfigError(
154
+ f"No free Marimo port found on {host} in range {DEFAULT_PORT}-{DEFAULT_PORT + scan_limit - 1}. "
155
+ "Close existing sessions or provide --port."
156
+ )
157
+
158
+
159
+ def plan_marimo_launch(
160
+ *,
161
+ mode: str,
162
+ target: Path,
163
+ headless: bool = False,
164
+ preferred_port: int | None = None,
165
+ base_env: dict[str, str] | None = None,
166
+ repo_root: Path | None = None,
167
+ ) -> MarimoLaunchPlan:
168
+ context = _resolve_launch_context(target, repo_root=repo_root)
169
+ runtime_paths = _runtime_paths_for_repo_root(context.repo_root)
170
+ env = _build_env(context.repo_root, runtime_paths=runtime_paths, base_env=base_env)
171
+ records = _prune_registry(_load_registry(runtime_paths.registry_path), pid_is_live=_pid_is_live)
172
+
173
+ for record in records:
174
+ if _session_matches_current_inputs(
175
+ record,
176
+ mode=mode,
177
+ resolved_target=context.resolved_target,
178
+ experiment_root=context.experiment_root,
179
+ runtime_fingerprint=context.runtime_fingerprint,
180
+ notebook_mtime_ns=context.notebook_mtime_ns,
181
+ notebook_size_bytes=context.notebook_size_bytes,
182
+ port_is_open=lambda host, port: _port_is_open(host, port),
183
+ ):
184
+ _write_registry(runtime_paths.registry_path, records)
185
+ return MarimoLaunchPlan(
186
+ cmd=(),
187
+ env=env,
188
+ url=f"http://{record.host}:{record.port}",
189
+ port=record.port,
190
+ host=record.host,
191
+ target=context.resolved_target,
192
+ repo_root=context.repo_root,
193
+ runtime_paths=runtime_paths,
194
+ reused_session=record,
195
+ )
196
+
197
+ terminated_sessions: list[MarimoSessionRecord] = []
198
+ kept_records: list[MarimoSessionRecord] = []
199
+ for record in records:
200
+ if (
201
+ record.mode == mode
202
+ and record.experiment_root == str(context.experiment_root)
203
+ and _terminate_pid(record.pid)
204
+ ):
205
+ terminated_sessions.append(record)
206
+ continue
207
+ kept_records.append(record)
208
+
209
+ port = _choose_port(host=DEFAULT_HOST, preferred_port=preferred_port, records=kept_records)
210
+
211
+ cmd = [sys.executable, "-m", "marimo", mode, "--host", DEFAULT_HOST, "--port", str(port)]
212
+ if mode == "edit":
213
+ cmd.append("--skip-update-check")
214
+ if headless:
215
+ cmd.append("--headless")
216
+ cmd.append("--no-token")
217
+ cmd.append(str(context.resolved_target))
218
+
219
+ _write_registry(runtime_paths.registry_path, kept_records)
220
+ return MarimoLaunchPlan(
221
+ cmd=tuple(cmd),
222
+ env=env,
223
+ url=f"http://{DEFAULT_HOST}:{port}",
224
+ port=port,
225
+ host=DEFAULT_HOST,
226
+ target=context.resolved_target,
227
+ repo_root=context.repo_root,
228
+ runtime_paths=runtime_paths,
229
+ terminated_sessions=tuple(terminated_sessions),
230
+ )
231
+
232
+
233
+ def register_managed_session(
234
+ *,
235
+ registry_path: Path,
236
+ pid: int,
237
+ port: int,
238
+ host: str,
239
+ mode: str,
240
+ target: Path,
241
+ repo_root: Path,
242
+ ) -> None:
243
+ records = _prune_registry(_load_registry(registry_path), pid_is_live=_pid_is_live)
244
+ target_mtime_ns, target_size_bytes = _target_signature(target)
245
+ resolved_repo_root = _resolve_repo_root(target, repo_root=repo_root)
246
+ runtime_fingerprint = _runtime_fingerprint(resolved_repo_root)
247
+ record = MarimoSessionRecord(
248
+ pid=pid,
249
+ port=port,
250
+ host=host,
251
+ mode=mode,
252
+ notebook=str(target.resolve()),
253
+ experiment_root=str(_find_experiment_root(target)),
254
+ repo_root=str(resolved_repo_root),
255
+ launched_at=time.time(),
256
+ notebook_mtime_ns=target_mtime_ns,
257
+ notebook_size_bytes=target_size_bytes,
258
+ runtime_fingerprint=runtime_fingerprint,
259
+ )
260
+ records = [existing for existing in records if existing.pid != pid]
261
+ records.append(record)
262
+ _write_registry(registry_path, records)
263
+
264
+
265
+ def unregister_managed_session(*, registry_path: Path, pid: int) -> None:
266
+ records = _prune_registry(_load_registry(registry_path), pid_is_live=_pid_is_live)
267
+ _write_registry(registry_path, [record for record in records if record.pid != pid])
268
+
269
+
270
+ def open_url(url: str) -> None:
271
+ try:
272
+ webbrowser.open(url, new=0, autoraise=True)
273
+ except Exception:
274
+ return
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections import Counter
5
+ from collections.abc import Mapping
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ from reader_workbench.workbench.config import load_reader_config_document
10
+
11
+ _DATE_PREFIX = re.compile(r"^(?P<date>\d{8})(?:[_-]+(?P<name>.+))?$")
12
+ _WORD_BREAK = re.compile(r"[_-]+")
13
+ _UPPERCASE_TOKENS = frozenset(
14
+ {
15
+ "cfp",
16
+ "dna",
17
+ "gfp",
18
+ "iptg",
19
+ "od600",
20
+ "rfp",
21
+ "rna",
22
+ "yfp",
23
+ }
24
+ )
25
+
26
+
27
+ def experiment_display_title(*, experiment_id: str, authored_title: str | None = None) -> str:
28
+ """Resolve a human title without changing the stable experiment identity."""
29
+
30
+ stable_id = _required_text(experiment_id, field="experiment_id")
31
+ title = str(authored_title or "").strip()
32
+ if title and title != stable_id:
33
+ return title
34
+
35
+ match = _DATE_PREFIX.fullmatch(stable_id)
36
+ if match is None:
37
+ return _humanize_slug(stable_id)
38
+
39
+ date_label = _date_label(match.group("date"))
40
+ name = str(match.group("name") or "").strip()
41
+ if not name:
42
+ return date_label
43
+ return f"{date_label} · {_humanize_slug(name)}"
44
+
45
+
46
+ def experiment_display_title_from_config(
47
+ config_path: Path,
48
+ *,
49
+ expected_experiment_id: str | None = None,
50
+ ) -> str:
51
+ """Resolve the authored or fallback title from a Reader experiment config."""
52
+
53
+ payload = load_reader_config_document(Path(config_path))
54
+ experiment = payload.get("experiment")
55
+ if not isinstance(experiment, Mapping):
56
+ raise ValueError("experiment config must define an experiment mapping.")
57
+ experiment_id = _required_text(experiment.get("id"), field="experiment.id")
58
+ authored_title = experiment.get("title")
59
+ if authored_title is not None and not isinstance(authored_title, str):
60
+ raise ValueError("experiment.title must be a string when provided.")
61
+ if expected_experiment_id is not None and experiment_id != expected_experiment_id:
62
+ raise ValueError(
63
+ "experiment config identity disagrees with the expected experiment ID: "
64
+ f"{experiment_id!r} != {expected_experiment_id!r}."
65
+ )
66
+ return experiment_display_title(
67
+ experiment_id=experiment_id,
68
+ authored_title=authored_title,
69
+ )
70
+
71
+
72
+ def experiment_selector_options(experiment_titles: Mapping[str, str]) -> dict[str, str]:
73
+ """Map readable selector labels to stable experiment IDs."""
74
+
75
+ normalized: dict[str, str] = {}
76
+ for raw_experiment_id, title in experiment_titles.items():
77
+ experiment_id = _required_text(raw_experiment_id, field="experiment_id")
78
+ if experiment_id in normalized:
79
+ raise ValueError(f"duplicate experiment IDs after normalization: {experiment_id!r}.")
80
+ normalized[experiment_id] = _required_text(title, field="experiment_title")
81
+ title_counts = Counter(normalized.values())
82
+ options: dict[str, str] = {}
83
+ for experiment_id in sorted(normalized):
84
+ title = normalized[experiment_id]
85
+ label = title if title_counts[title] == 1 else f"{title} · {_identity_suffix(experiment_id)}"
86
+ if label in options:
87
+ label = f"{title} · {experiment_id}"
88
+ options[label] = experiment_id
89
+ if len(options) != len(normalized):
90
+ raise ValueError("experiment selector labels must be unique.")
91
+ return options
92
+
93
+
94
+ def _humanize_slug(value: str) -> str:
95
+ words = [word for word in _WORD_BREAK.split(value.strip()) if word]
96
+ if not words:
97
+ raise ValueError("experiment_id must contain displayable text.")
98
+ return " ".join(_display_word(word) for word in words)
99
+
100
+
101
+ def _display_word(word: str) -> str:
102
+ lowered = word.casefold()
103
+ if lowered in _UPPERCASE_TOKENS:
104
+ return word.upper()
105
+ if any(character.isupper() for character in word) and any(character.islower() for character in word):
106
+ return word
107
+ if any(character.isdigit() for character in word):
108
+ return word.upper()
109
+ return word[:1].upper() + word[1:]
110
+
111
+
112
+ def _identity_suffix(experiment_id: str) -> str:
113
+ match = _DATE_PREFIX.fullmatch(experiment_id)
114
+ return _date_label(match.group("date")) if match is not None else experiment_id
115
+
116
+
117
+ def _date_label(value: str) -> str:
118
+ try:
119
+ parsed = datetime.strptime(value, "%Y%m%d")
120
+ except ValueError:
121
+ return value
122
+ return parsed.strftime("%Y-%m-%d")
123
+
124
+
125
+ def _required_text(value: object, *, field: str) -> str:
126
+ text = str(value or "").strip()
127
+ if not text:
128
+ raise ValueError(f"{field} is required for notebook presentation.")
129
+ return text
130
+
131
+
132
+ __all__ = [
133
+ "experiment_display_title",
134
+ "experiment_display_title_from_config",
135
+ "experiment_selector_options",
136
+ ]
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.resources import files
4
+ from pathlib import Path
5
+
6
+ from reader_workbench.errors import ConfigError
7
+ from reader_workbench.workbench.paths import resolve_confined_sink_root, resolve_path_within_root
8
+
9
+ CANONICAL_NOTEBOOK_ID = "notebook/eda"
10
+ _CANONICAL_NOTEBOOK_SOURCE = "eda.marimo.py.txt"
11
+
12
+
13
+ def write_experiment_notebook(
14
+ target: Path,
15
+ *,
16
+ experiment_root: Path,
17
+ notebooks_root: Path,
18
+ overwrite: bool = False,
19
+ ) -> tuple[Path, bool]:
20
+ target = _confined_notebook_target(
21
+ target,
22
+ experiment_root=experiment_root,
23
+ notebooks_root=notebooks_root,
24
+ )
25
+ if target.exists() and not overwrite:
26
+ return target, False
27
+ body = _load_canonical_notebook_body()
28
+ target.parent.mkdir(parents=True, exist_ok=True)
29
+ target.write_text(body, encoding="utf-8")
30
+ return target, True
31
+
32
+
33
+ def _load_canonical_notebook_body() -> str:
34
+ try:
35
+ return files(__package__).joinpath(_CANONICAL_NOTEBOOK_SOURCE).read_text(encoding="utf-8")
36
+ except (FileNotFoundError, OSError) as exc:
37
+ raise ConfigError(f"Canonical notebook source {CANONICAL_NOTEBOOK_ID!r} is unavailable.") from exc
38
+
39
+
40
+ def _confined_notebook_target(target: Path, *, experiment_root: Path, notebooks_root: Path) -> Path:
41
+ try:
42
+ owned_root = resolve_confined_sink_root(
43
+ notebooks_root,
44
+ root=experiment_root,
45
+ label="notebooks",
46
+ )
47
+ except ValueError as exc:
48
+ raise ConfigError(str(exc)) from exc
49
+
50
+ candidate = Path(target).expanduser()
51
+ if not candidate.is_absolute():
52
+ candidate = owned_root / candidate
53
+ candidate = candidate.absolute()
54
+ if candidate.is_symlink():
55
+ raise ConfigError(f"Notebook target must not be a symlink: {candidate}")
56
+ try:
57
+ resolve_confined_sink_root(candidate.parent, root=owned_root, label="notebook target parent")
58
+ return resolve_path_within_root(candidate, root=owned_root)
59
+ except ValueError as exc:
60
+ raise ConfigError(f"Notebook target must stay within the configured notebooks root: {candidate}") from exc
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ PluginCategory = Literal["ingest", "transform", "plot", "export", "validator"]
7
+ PluginDomain = Literal["generic", "plate_reader", "cytometry", "logic"]
8
+ WorkbenchItemKind = Literal["pipeline", "plot", "export"]
9
+ WorkbenchPluginStepKind = Literal["pipeline", "plot", "export"]
10
+ WorkbenchProducerKind = Literal["pipeline", "plot", "export"]
11
+ WorkbenchRecordKind = Literal["dataframe_artifact", "file_bundle"]
12
+ WorkbenchRefKind = Literal["plugin"]
13
+
14
+ KNOWN_PLUGIN_DOMAINS: tuple[PluginDomain, ...] = ("generic", "plate_reader", "cytometry", "logic")
15
+
16
+
17
+ def validate_plugin_domain(domain: str) -> PluginDomain:
18
+ normalized = str(domain).strip()
19
+ if normalized in KNOWN_PLUGIN_DOMAINS:
20
+ return normalized # type: ignore[return-value]
21
+ options = ", ".join(KNOWN_PLUGIN_DOMAINS)
22
+ raise ValueError(f"Unknown plugin domain '{domain}'. Expected one of: {options}")
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class PluginSemantics:
27
+ domain: PluginDomain
28
+ family: str
29
+ summary: str
30
+ tags: tuple[str, ...] = ()
31
+
32
+ def __post_init__(self) -> None:
33
+ object.__setattr__(self, "domain", validate_plugin_domain(self.domain))
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class WorkbenchSurfaceSemantics:
38
+ kind: WorkbenchItemKind
39
+ section: str
40
+ title: str
41
+ item_label: str
42
+ items_label: str
43
+ ref_kind: WorkbenchRefKind
44
+ expected_plugin_category: str | None = None
45
+
46
+
47
+ _WORKBENCH_SURFACE_SEMANTICS: dict[WorkbenchItemKind, WorkbenchSurfaceSemantics] = {
48
+ "pipeline": WorkbenchSurfaceSemantics(
49
+ kind="pipeline",
50
+ section="pipeline",
51
+ title="Pipeline",
52
+ item_label="pipeline step",
53
+ items_label="pipeline steps",
54
+ ref_kind="plugin",
55
+ ),
56
+ "plot": WorkbenchSurfaceSemantics(
57
+ kind="plot",
58
+ section="plots",
59
+ title="Plots",
60
+ item_label="plot spec",
61
+ items_label="plot specs",
62
+ ref_kind="plugin",
63
+ expected_plugin_category="plot",
64
+ ),
65
+ "export": WorkbenchSurfaceSemantics(
66
+ kind="export",
67
+ section="exports",
68
+ title="Exports",
69
+ item_label="export spec",
70
+ items_label="export specs",
71
+ ref_kind="plugin",
72
+ expected_plugin_category="export",
73
+ ),
74
+ }
75
+
76
+
77
+ def get_workbench_surface_semantics(kind: WorkbenchItemKind) -> WorkbenchSurfaceSemantics:
78
+ return _WORKBENCH_SURFACE_SEMANTICS[kind]