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,102 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from reader_workbench.errors import ConfigError
9
+ from reader_workbench.workbench.graph.refs import (
10
+ InputRef,
11
+ OutputRef,
12
+ input_ref_to_dict,
13
+ output_ref_to_dict,
14
+ )
15
+ from reader_workbench.workbench.ontology import (
16
+ WorkbenchItemKind,
17
+ WorkbenchPluginStepKind,
18
+ WorkbenchSurfaceSemantics,
19
+ get_workbench_surface_semantics,
20
+ )
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class RecipeSource:
25
+ recipe: str
26
+ with_: dict[str, Any] = field(default_factory=dict)
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class PluginStep:
31
+ kind: WorkbenchPluginStepKind
32
+ id: str
33
+ plugin: str
34
+ reads: dict[str, InputRef] = field(default_factory=dict)
35
+ with_: dict[str, Any] = field(default_factory=dict)
36
+ writes: dict[str, OutputRef] = field(default_factory=dict)
37
+ source_recipe: RecipeSource | None = None
38
+
39
+ @property
40
+ def semantics(self) -> WorkbenchSurfaceSemantics:
41
+ return get_workbench_surface_semantics(self.kind)
42
+
43
+ @property
44
+ def plugin_category(self) -> str:
45
+ return self.plugin.split("/", 1)[0] if "/" in self.plugin else self.plugin
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ payload = {
49
+ "id": self.id,
50
+ "plugin": self.plugin,
51
+ "reads": {key: input_ref_to_dict(value) for key, value in (self.reads or {}).items()},
52
+ "with": dict(self.with_ or {}),
53
+ "writes": {key: output_ref_to_dict(value) for key, value in (self.writes or {}).items()},
54
+ }
55
+ if self.source_recipe is not None:
56
+ payload["source_recipe"] = {
57
+ "recipe": self.source_recipe.recipe,
58
+ "with": dict(self.source_recipe.with_ or {}),
59
+ }
60
+ return payload
61
+
62
+
63
+ WorkbenchItem = PluginStep
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class Workbench:
68
+ pipeline: tuple[PluginStep, ...] = ()
69
+ plots: tuple[PluginStep, ...] = ()
70
+ exports: tuple[PluginStep, ...] = ()
71
+
72
+ def by_kind(self, kind: WorkbenchItemKind) -> tuple[WorkbenchItem, ...]:
73
+ if kind == "pipeline":
74
+ return self.pipeline
75
+ if kind == "plot":
76
+ return self.plots
77
+ if kind == "export":
78
+ return self.exports
79
+ raise ValueError(f"Unknown workbench item kind {kind!r}")
80
+
81
+ def all_specs(self) -> tuple[WorkbenchItem, ...]:
82
+ return self.pipeline + self.plots + self.exports
83
+
84
+ def plugin_steps(self) -> tuple[PluginStep, ...]:
85
+ return self.pipeline + self.plots + self.exports
86
+
87
+ def plugin_categories(self) -> set[str]:
88
+ return {item.plugin_category for item in self.plugin_steps()}
89
+
90
+ def counts(self) -> dict[WorkbenchItemKind, int]:
91
+ return {
92
+ "pipeline": len(self.pipeline),
93
+ "plot": len(self.plots),
94
+ "export": len(self.exports),
95
+ }
96
+
97
+
98
+ def ensure_unique_workbench_ids(*collections: Sequence[WorkbenchItem]) -> None:
99
+ ids: list[str] = [item.id for collection in collections for item in collection if item.id]
100
+ dupes = sorted(item_id for item_id, count in Counter(ids).items() if count > 1)
101
+ if dupes:
102
+ raise ConfigError(f"Duplicate step/spec id(s) across pipeline/plots/exports: {dupes}")
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from reader_workbench.errors import ConfigError
7
+ from reader_workbench.workbench.decl import (
8
+ FileInputDecl,
9
+ PluginStepDecl,
10
+ RecordCollectionInputDecl,
11
+ RecordInputDecl,
12
+ RecordOutputDecl,
13
+ ResourceInputDecl,
14
+ WorkbenchDecl,
15
+ )
16
+ from reader_workbench.workbench.experiment import ResourceCatalog
17
+ from reader_workbench.workbench.graph.nodes import PluginStep, Workbench, ensure_unique_workbench_ids
18
+ from reader_workbench.workbench.graph.refs import (
19
+ FileRef,
20
+ InputRef,
21
+ OutputRef,
22
+ RecordCollectionRef,
23
+ RecordRef,
24
+ ResourceRef,
25
+ SourceRecordRef,
26
+ )
27
+ from reader_workbench.workbench.ontology import WorkbenchPluginStepKind, get_workbench_surface_semantics
28
+ from reader_workbench.workbench.paths import resolve_path_within_root
29
+
30
+
31
+ def resolve_workbench(decl: WorkbenchDecl) -> Workbench:
32
+ pipeline = tuple(_resolve_plugin_steps(decl, kind="pipeline", specs=decl.pipeline.steps))
33
+ plots = tuple(_resolve_plugin_steps(decl, kind="plot", specs=decl.plots.specs))
34
+ exports = tuple(_resolve_plugin_steps(decl, kind="export", specs=decl.exports.specs))
35
+ ensure_unique_workbench_ids(pipeline, plots, exports)
36
+ return Workbench(pipeline=pipeline, plots=plots, exports=exports)
37
+
38
+
39
+ def materialize_workbench(decl: WorkbenchDecl) -> dict[str, list[dict[str, Any]]]:
40
+ workbench = resolve_workbench(decl)
41
+ return {
42
+ "pipeline": [item.to_dict() for item in workbench.pipeline],
43
+ "plots": [item.to_dict() for item in workbench.plots],
44
+ "exports": [item.to_dict() for item in workbench.exports],
45
+ }
46
+
47
+
48
+ def select_workbench_specs(
49
+ specs: list[PluginStep],
50
+ *,
51
+ only: list[str],
52
+ exclude: list[str],
53
+ kind_label: str,
54
+ ) -> list[PluginStep]:
55
+ ids = [item.id for item in specs]
56
+ available = set(ids)
57
+ if only:
58
+ only_ids = set(only)
59
+ missing = sorted(only_ids - available)
60
+ if missing:
61
+ raise ConfigError(f"Unknown {kind_label} id(s): {missing}.")
62
+ selected = [item for item in specs if item.id in only_ids]
63
+ else:
64
+ selected = list(specs)
65
+ if exclude:
66
+ exclude_ids = set(exclude)
67
+ missing = sorted(exclude_ids - available)
68
+ if missing:
69
+ raise ConfigError(f"Unknown {kind_label} id(s): {missing}.")
70
+ selected = [item for item in selected if item.id not in exclude_ids]
71
+ return selected
72
+
73
+
74
+ def normalize_input_binding(
75
+ binding: Any,
76
+ *,
77
+ root: Path,
78
+ resources: ResourceCatalog,
79
+ section: str,
80
+ step_id: str,
81
+ key: str,
82
+ ) -> InputRef:
83
+ if isinstance(binding, RecordInputDecl):
84
+ return RecordRef(record_id=binding.record_id)
85
+ if isinstance(binding, FileInputDecl):
86
+ try:
87
+ path = resolve_path_within_root(binding.path, root=root)
88
+ except ValueError as err:
89
+ raise ConfigError(
90
+ f"{section} {step_id}: reads '{key}' must stay under the experiment root after resolving symlinks."
91
+ ) from err
92
+ return FileRef(path=path)
93
+ if isinstance(binding, ResourceInputDecl):
94
+ try:
95
+ resource = resources.require_file(binding.resource_id)
96
+ except ValueError as err:
97
+ raise ConfigError(
98
+ f"{section} {step_id}: reads '{key}' references unknown resource '{binding.resource_id}'."
99
+ ) from err
100
+ return ResourceRef(resource_id=binding.resource_id, path=resource.path)
101
+ if isinstance(binding, RecordCollectionInputDecl):
102
+ records: list[SourceRecordRef] = []
103
+ for resource_id in binding.resource_ids:
104
+ try:
105
+ resource = resources.require_record(resource_id)
106
+ except ValueError as err:
107
+ raise ConfigError(
108
+ f"{section} {step_id}: reads '{key}' references unknown record resource '{resource_id}'."
109
+ ) from err
110
+ records.append(
111
+ SourceRecordRef(
112
+ resource_id=resource_id,
113
+ experiment_id=resource.experiment_id,
114
+ record_id=resource.record_id,
115
+ experiment_root=resource.experiment_root,
116
+ outputs_dir=resource.outputs_dir,
117
+ )
118
+ )
119
+ return RecordCollectionRef(records=tuple(records))
120
+ raise ConfigError(f"{section} {step_id}: reads '{key}' uses unsupported binding type {type(binding).__name__}")
121
+
122
+
123
+ def normalize_output_binding(binding: RecordOutputDecl) -> OutputRef:
124
+ return OutputRef(record_id=binding.record_id)
125
+
126
+
127
+ def _resolve_plugin_steps(
128
+ decl: WorkbenchDecl,
129
+ *,
130
+ kind: WorkbenchPluginStepKind,
131
+ specs: tuple[PluginStepDecl, ...],
132
+ ) -> list[PluginStep]:
133
+ semantics = get_workbench_surface_semantics(kind)
134
+ root = decl.experiment.root
135
+ resources = decl.experiment_semantics.resources
136
+ seen: set[str] = set()
137
+ resolved: list[PluginStep] = []
138
+ for step in specs or ():
139
+ step_id = step.id
140
+ if not step_id or not isinstance(step_id, str):
141
+ raise ConfigError(f"Every {semantics.section} spec must include an id.")
142
+ if step_id in seen:
143
+ raise ConfigError(f"{semantics.section} contains duplicate spec id(s): {step_id}")
144
+ seen.add(step_id)
145
+ plugin = step.plugin
146
+ if not plugin or not isinstance(plugin, str):
147
+ raise ConfigError(f"{semantics.section} {step_id}: plugin must be a non-empty string")
148
+ if "/" not in plugin:
149
+ raise ConfigError(f"{semantics.section} {step_id}: plugin must be 'category/key'")
150
+ category = plugin.split("/", 1)[0]
151
+ expected_category = semantics.expected_plugin_category
152
+ if kind == "pipeline" and category in {"plot", "export"}:
153
+ raise ConfigError(f"pipeline {step_id}: plot/export plugins are not allowed in pipeline.")
154
+ if expected_category is not None and category != expected_category:
155
+ raise ConfigError(f"{semantics.section} {step_id}: plugin must be {expected_category}/*")
156
+ resolved.append(
157
+ PluginStep(
158
+ kind=kind,
159
+ id=step_id,
160
+ plugin=plugin,
161
+ reads={
162
+ key: normalize_input_binding(
163
+ value,
164
+ root=root,
165
+ resources=resources,
166
+ section=semantics.section,
167
+ step_id=step_id,
168
+ key=key,
169
+ )
170
+ for key, value in (step.reads or {}).items()
171
+ },
172
+ with_=dict(step.with_ or {}),
173
+ writes={key: normalize_output_binding(value) for key, value in (step.writes or {}).items()},
174
+ source_recipe=step.source_recipe,
175
+ )
176
+ )
177
+ return resolved
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class RecordRef:
10
+ record_id: str
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FileRef:
15
+ path: Path
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ResourceRef:
20
+ resource_id: str
21
+ path: Path
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class SourceRecordRef:
26
+ resource_id: str
27
+ experiment_id: str
28
+ record_id: str
29
+ experiment_root: Path
30
+ outputs_dir: Path
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class RecordCollectionRef:
35
+ records: tuple[SourceRecordRef, ...]
36
+
37
+
38
+ InputRef = RecordRef | FileRef | ResourceRef | SourceRecordRef | RecordCollectionRef
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class OutputRef:
43
+ record_id: str
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ProvenanceInput:
48
+ label: str
49
+ ref: InputRef
50
+ discovery_policy: str | None = None
51
+
52
+
53
+ def input_ref_display(ref: InputRef) -> str:
54
+ if isinstance(ref, RecordRef):
55
+ return ref.record_id
56
+ if isinstance(ref, ResourceRef):
57
+ return f"resource({ref.resource_id})"
58
+ if isinstance(ref, SourceRecordRef):
59
+ return f"record_resource({ref.resource_id}:{ref.experiment_id}:{ref.record_id})"
60
+ if isinstance(ref, RecordCollectionRef):
61
+ return "record_collection(" + ", ".join(item.resource_id for item in ref.records) + ")"
62
+ return f"file({ref.path})"
63
+
64
+
65
+ def output_ref_display(ref: OutputRef) -> str:
66
+ return ref.record_id
67
+
68
+
69
+ def input_ref_to_dict(ref: InputRef) -> dict[str, Any]:
70
+ if isinstance(ref, RecordRef):
71
+ return {"record": ref.record_id}
72
+ if isinstance(ref, ResourceRef):
73
+ return {"resource": ref.resource_id, "path": str(ref.path)}
74
+ if isinstance(ref, SourceRecordRef):
75
+ return {
76
+ "record_resource": ref.resource_id,
77
+ "experiment": ref.experiment_id,
78
+ "record": ref.record_id,
79
+ }
80
+ if isinstance(ref, RecordCollectionRef):
81
+ return {
82
+ "record_collection": [
83
+ {
84
+ "resource": item.resource_id,
85
+ "experiment": item.experiment_id,
86
+ "record": item.record_id,
87
+ }
88
+ for item in ref.records
89
+ ]
90
+ }
91
+ return {"file": str(ref.path)}
92
+
93
+
94
+ def output_ref_to_dict(ref: OutputRef) -> dict[str, str]:
95
+ return {"record": ref.record_id}
96
+
97
+
98
+ def provenance_input_to_dict(binding: ProvenanceInput) -> dict[str, Any]:
99
+ return {"label": binding.label, **input_ref_to_dict(binding.ref)}
100
+
101
+
102
+ def input_ref_from_dict(payload: dict[str, Any]) -> InputRef:
103
+ if not isinstance(payload, dict):
104
+ raise TypeError("input binding payload must be a mapping")
105
+ keys = set(payload)
106
+ if keys == {"record"}:
107
+ record = payload["record"]
108
+ if isinstance(record, str) and record:
109
+ return RecordRef(record_id=record)
110
+ raise ValueError("record binding must include a non-empty record id")
111
+ if keys == {"file"}:
112
+ file_path = payload["file"]
113
+ if isinstance(file_path, str) and file_path:
114
+ return FileRef(path=Path(file_path))
115
+ raise ValueError("file binding must include a non-empty path")
116
+ if keys == {"resource", "path"}:
117
+ resource_id = payload["resource"]
118
+ path = payload["path"]
119
+ if not isinstance(resource_id, str) or not resource_id:
120
+ raise ValueError("resource binding must include a non-empty resource id")
121
+ if not isinstance(path, str) or not path:
122
+ raise ValueError(f"resource binding {resource_id!r} must include a non-empty path")
123
+ return ResourceRef(resource_id=resource_id, path=Path(path))
124
+ raise ValueError(
125
+ "input binding payload must contain exactly one reference shape: {'record'}, {'file'}, or {'resource', 'path'}"
126
+ )
127
+
128
+
129
+ def output_ref_from_dict(payload: dict[str, Any]) -> OutputRef:
130
+ if not isinstance(payload, dict):
131
+ raise TypeError("output binding payload must be a mapping")
132
+ if set(payload) != {"record"}:
133
+ raise ValueError("output binding payload must contain only 'record'")
134
+ record = payload.get("record")
135
+ if not isinstance(record, str) or not record:
136
+ raise ValueError("output binding payload must include record")
137
+ return OutputRef(record_id=record)
138
+
139
+
140
+ def provenance_input_from_dict(payload: dict[str, Any]) -> ProvenanceInput:
141
+ if not isinstance(payload, dict):
142
+ raise TypeError("provenance input payload must be a mapping")
143
+ label = payload.get("label")
144
+ if not isinstance(label, str) or not label:
145
+ raise ValueError("provenance input payload must include label")
146
+ ref_payload = dict(payload)
147
+ ref_payload.pop("label", None)
148
+ return ProvenanceInput(label=label, ref=input_ref_from_dict(ref_payload))
@@ -0,0 +1,19 @@
1
+ """Neutral defaults shared by protocol authoring and ingest discovery mechanics."""
2
+
3
+ DEFAULT_INPUT_ROOTS = ("./inputs", "./raw", "./raw_data")
4
+ DEFAULT_WORKBOOK_INCLUDE = ("*.xlsx", "*.XLSX")
5
+ DEFAULT_INPUT_EXCLUDE = (
6
+ "~$*",
7
+ "._*",
8
+ "#*#",
9
+ "*.tmp",
10
+ "*.temp",
11
+ "*.bak",
12
+ "metadata.*",
13
+ "metadata_filtered.*",
14
+ "sample_map.*",
15
+ "sample_metadata.*",
16
+ "plate_map.*",
17
+ )
18
+
19
+ __all__ = ["DEFAULT_INPUT_EXCLUDE", "DEFAULT_INPUT_ROOTS", "DEFAULT_WORKBOOK_INCLUDE"]
@@ -0,0 +1,3 @@
1
+ """Inspection helpers for experiment discovery, reporting, and readiness."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from copy import deepcopy
5
+
6
+ from reader_workbench.workbench.graph import resolve_workbench
7
+
8
+ from .experiments import experiment_identity_payload
9
+ from .runtime import export_output_summaries, plot_output_summaries, record_producer_map, spec_step_payload
10
+
11
+
12
+ def _sorted_counter(values: list[str]) -> dict[str, int]:
13
+ counts = Counter(value for value in values if value)
14
+ return dict(sorted(counts.items()))
15
+
16
+
17
+ def workbench_surface_catalog_payload(
18
+ *,
19
+ experiment: dict[str, object],
20
+ protocol: str,
21
+ kind: str,
22
+ only: list[str],
23
+ exclude: list[str],
24
+ entries: list[dict[str, object]],
25
+ ) -> dict[str, object]:
26
+ payload_key = f"{kind}s"
27
+ summary_key = payload_key
28
+ plugin_ids = [str(entry.get("plugin") or "") for entry in entries]
29
+ domains = [
30
+ str(semantics.get("domain") or "")
31
+ for semantics in (entry.get("semantics") for entry in entries)
32
+ if isinstance(semantics, dict)
33
+ ]
34
+ families = [
35
+ str(semantics.get("family") or "")
36
+ for semantics in (entry.get("semantics") for entry in entries)
37
+ if isinstance(semantics, dict)
38
+ ]
39
+ return {
40
+ "experiment": deepcopy(experiment),
41
+ "catalog": {
42
+ "kind": kind,
43
+ "protocol": protocol,
44
+ },
45
+ "selection": {
46
+ "only": list(only),
47
+ "exclude": list(exclude),
48
+ },
49
+ "summary": {
50
+ summary_key: len(entries),
51
+ "by_plugin": _sorted_counter(plugin_ids),
52
+ "by_domain": _sorted_counter(domains),
53
+ "by_family": _sorted_counter(families),
54
+ },
55
+ payload_key: deepcopy(entries),
56
+ }
57
+
58
+
59
+ def plugin_registry_payload(
60
+ *,
61
+ descriptors,
62
+ category: str | None,
63
+ domain: str | None,
64
+ family: str | None,
65
+ protocol: str | None,
66
+ ) -> dict[str, object]:
67
+ plugins = [
68
+ {
69
+ "category": descriptor.category,
70
+ "domain": descriptor.domain,
71
+ "family": descriptor.family,
72
+ "key": descriptor.key,
73
+ "plugin": descriptor.plugin,
74
+ "summary": descriptor.summary,
75
+ "class": f"{descriptor.cls.__module__}.{descriptor.cls.__name__}",
76
+ }
77
+ for descriptor in descriptors
78
+ ]
79
+ return {
80
+ "selection": {
81
+ "category": category,
82
+ "domain": domain,
83
+ "family": family,
84
+ "protocol": protocol,
85
+ },
86
+ "summary": {
87
+ "plugins": len(plugins),
88
+ "by_category": _sorted_counter([str(item["category"]) for item in plugins]),
89
+ "by_domain": _sorted_counter([str(item["domain"]) for item in plugins]),
90
+ "by_family": _sorted_counter([str(item["family"]) for item in plugins]),
91
+ },
92
+ "plugins": plugins,
93
+ }
94
+
95
+
96
+ def workbench_surface_specs_payload(
97
+ *,
98
+ job_path,
99
+ decl,
100
+ runtime,
101
+ bound_protocol,
102
+ selected,
103
+ kind: str,
104
+ only: list[str],
105
+ exclude: list[str],
106
+ ) -> dict[str, object]:
107
+ workbench = resolve_workbench(decl)
108
+ record_producers = record_producer_map(workbench.plugin_steps(), runtime=runtime)
109
+ summary_lookup = (
110
+ plot_output_summaries(bound_protocol) if kind == "plot" else export_output_summaries(bound_protocol)
111
+ )
112
+ entries = [
113
+ spec_step_payload(
114
+ step,
115
+ summary=summary_lookup.get(step.id, "—"),
116
+ runtime=runtime,
117
+ record_producers=record_producers,
118
+ )
119
+ for step in selected
120
+ ]
121
+ return workbench_surface_catalog_payload(
122
+ experiment=experiment_identity_payload(job_path=job_path, decl=decl, protocol_id=bound_protocol.id),
123
+ protocol=bound_protocol.id,
124
+ kind=kind,
125
+ only=only,
126
+ exclude=exclude,
127
+ entries=entries,
128
+ )
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def count_visible_files(path: Path) -> int:
8
+ if not path.exists():
9
+ return 0
10
+ return sum(1 for item in path.rglob("*") if item.is_file() and not item.name.startswith("."))
11
+
12
+
13
+ def count_visible_glob(path: Path, pattern: str) -> int:
14
+ if not path.exists():
15
+ return 0
16
+ return sum(1 for item in path.rglob(pattern) if item.is_file() and not item.name.startswith("."))
17
+
18
+
19
+ def visible_relative_files(path: Path, *, base: Path, limit: int = 8) -> list[str]:
20
+ if not path.exists():
21
+ return []
22
+ files = sorted(item for item in path.rglob("*") if item.is_file() and not item.name.startswith("."))
23
+ return [format_relative_path(item, base=base) for item in files[:limit]]
24
+
25
+
26
+ def format_relative_path(path: Path, *, base: Path) -> str:
27
+ try:
28
+ return str(path.relative_to(base))
29
+ except ValueError:
30
+ return str(path)
31
+
32
+
33
+ def resolve_output_subdir(outputs_dir: Path, subdir: str) -> Path:
34
+ return outputs_dir if subdir in ("", ".", "./") else outputs_dir / subdir
35
+
36
+
37
+ def summarize_outputs_dir(
38
+ outputs_dir: Path,
39
+ *,
40
+ plots_subdir: str = "plots",
41
+ exports_subdir: str = "exports",
42
+ notebooks_subdir: str = "notebooks",
43
+ ) -> dict[str, int]:
44
+ plots_dir = resolve_output_subdir(outputs_dir, plots_subdir)
45
+ exports_dir = resolve_output_subdir(outputs_dir, exports_subdir)
46
+ notebooks_dir = resolve_output_subdir(outputs_dir, notebooks_subdir)
47
+ artifacts_dir = outputs_dir / "artifacts"
48
+ return {
49
+ "records": count_visible_glob(artifacts_dir, "*.parquet"),
50
+ "plots": count_visible_files(plots_dir),
51
+ "exports": count_visible_files(exports_dir),
52
+ "notebooks": count_visible_files(notebooks_dir),
53
+ }
54
+
55
+
56
+ def preview_output_files(path: Path, *, base: Path, limit: int = 4) -> str:
57
+ files = visible_relative_files(path, base=base, limit=limit)
58
+ if not files:
59
+ return "—"
60
+ remaining = count_visible_files(path) - len(files)
61
+ preview = ", ".join(files)
62
+ if remaining > 0:
63
+ preview += f", … (+{remaining} more)"
64
+ return preview
65
+
66
+
67
+ def render_compact_value(value) -> str:
68
+ if value is None:
69
+ return "null"
70
+ if isinstance(value, bool):
71
+ return "true" if value else "false"
72
+ if isinstance(value, (int, float)):
73
+ return str(value)
74
+ if isinstance(value, str):
75
+ return value
76
+ if isinstance(value, tuple):
77
+ value = list(value)
78
+ try:
79
+ return json.dumps(value, ensure_ascii=True, sort_keys=True)
80
+ except TypeError:
81
+ return str(value)
82
+
83
+
84
+ def flatten_binding_rows(value, *, prefix: str = "") -> list[tuple[str, str]]:
85
+ rows: list[tuple[str, str]] = []
86
+ if isinstance(value, dict):
87
+ for key in sorted(value):
88
+ child_path = f"{prefix}.{key}" if prefix else str(key)
89
+ rows.extend(flatten_binding_rows(value[key], prefix=child_path))
90
+ return rows
91
+ rows.append((prefix or "value", render_compact_value(value)))
92
+ return rows