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,274 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import import_module
4
+ from pathlib import Path
5
+ from typing import Annotated, Any, Literal
6
+
7
+ from pydantic import BaseModel, Field, field_validator, model_validator
8
+
9
+ from reader_workbench.errors import ConfigError
10
+ from reader_workbench.workbench.dop import builtin_dop_registry
11
+
12
+
13
+ class ExperimentSpec(BaseModel):
14
+ id: str
15
+ title: str | None = None
16
+ lifecycle: str = "active"
17
+
18
+ model_config = {"extra": "forbid"}
19
+
20
+ @field_validator("id", mode="after")
21
+ @classmethod
22
+ def _validate_id(cls, v: str) -> str:
23
+ if not isinstance(v, str) or not v.strip():
24
+ raise ConfigError("experiment.id must be a non-empty string")
25
+ return v
26
+
27
+ @field_validator("lifecycle", mode="after")
28
+ @classmethod
29
+ def _validate_lifecycle(cls, v: str) -> str:
30
+ if not isinstance(v, str) or not v.strip():
31
+ raise ConfigError("experiment.lifecycle must be a non-empty string")
32
+ lifecycle = v.strip().lower()
33
+ allowed = {"active", "draft", "template"}
34
+ if lifecycle not in allowed:
35
+ raise ConfigError(f"experiment.lifecycle must be one of: {', '.join(sorted(allowed))}")
36
+ return lifecycle
37
+
38
+
39
+ ReplicateKind = Literal["biological", "technical", "mixed", "unknown", "not_applicable"]
40
+
41
+
42
+ class EvidenceSpec(BaseModel):
43
+ data_class: str
44
+ data_class_reason: str
45
+ replicate_kind: ReplicateKind
46
+ replicate_identity_field: str | None = None
47
+
48
+ model_config = {"extra": "forbid"}
49
+
50
+ @field_validator("data_class", mode="after")
51
+ @classmethod
52
+ def _validate_data_class(cls, value: str) -> str:
53
+ cleaned = value.strip()
54
+ if not cleaned:
55
+ raise ValueError("evidence.data_class must be a non-empty string")
56
+ builtin_dop_registry().data_class(cleaned)
57
+ return cleaned
58
+
59
+ @field_validator("data_class_reason", mode="after")
60
+ @classmethod
61
+ def _validate_data_class_reason(cls, value: str) -> str:
62
+ cleaned = value.strip()
63
+ if not cleaned:
64
+ raise ValueError("evidence.data_class_reason must be a non-empty string")
65
+ return cleaned
66
+
67
+ @field_validator("replicate_identity_field", mode="after")
68
+ @classmethod
69
+ def _validate_replicate_identity_field(cls, value: str | None) -> str | None:
70
+ if value is None:
71
+ return None
72
+ cleaned = value.strip()
73
+ if not cleaned:
74
+ raise ValueError("evidence.replicate_identity_field must be a non-empty string when provided")
75
+ return cleaned
76
+
77
+ @model_validator(mode="after")
78
+ def _validate_replicate_identity(self) -> EvidenceSpec:
79
+ if self.replicate_kind == "not_applicable" and self.replicate_identity_field is not None:
80
+ raise ValueError("evidence.replicate_identity_field cannot be set when replicate_kind is not_applicable")
81
+ return self
82
+
83
+
84
+ class PathsSpec(BaseModel):
85
+ outputs: str = "./outputs"
86
+ plots: str = "plots"
87
+ exports: str = "exports"
88
+ notebooks: str = "notebooks"
89
+
90
+ model_config = {"extra": "forbid"}
91
+
92
+
93
+ class PlottingSpec(BaseModel):
94
+ palette: str | None = "colorblind"
95
+
96
+ model_config = {"extra": "forbid"}
97
+
98
+
99
+ class FileResourceSpec(BaseModel):
100
+ kind: Literal["file"]
101
+ path: str
102
+
103
+ model_config = {"extra": "forbid"}
104
+
105
+
106
+ class RecordResourceSpec(BaseModel):
107
+ kind: Literal["record"]
108
+ experiment: str
109
+ record: str
110
+
111
+ model_config = {"extra": "forbid"}
112
+
113
+ @field_validator("experiment", "record", mode="after")
114
+ @classmethod
115
+ def _validate_nonempty(cls, value: str) -> str:
116
+ if not isinstance(value, str) or not value.strip():
117
+ raise ValueError("record resource experiment and record values must be non-empty strings")
118
+ return value.strip()
119
+
120
+
121
+ ResourceSpec = Annotated[FileResourceSpec | RecordResourceSpec, Field(discriminator="kind")]
122
+
123
+
124
+ class ResourcesSpec(BaseModel):
125
+ by_id: dict[str, ResourceSpec] = Field(default_factory=dict)
126
+
127
+ model_config = {"extra": "forbid"}
128
+
129
+
130
+ class AnnotationLabelSpec(BaseModel):
131
+ source: str
132
+ values: dict[str, str] = Field(default_factory=dict)
133
+ output: str | None = None
134
+
135
+ model_config = {"extra": "forbid"}
136
+
137
+
138
+ class AnnotationOrderSpec(BaseModel):
139
+ column: str
140
+ values: list[str] = Field(default_factory=list)
141
+
142
+ model_config = {"extra": "forbid"}
143
+
144
+
145
+ class AnnotationCollectionSpec(BaseModel):
146
+ column: str
147
+ items: dict[str, list[str]] = Field(default_factory=dict)
148
+
149
+ model_config = {"extra": "forbid"}
150
+
151
+
152
+ class AnnotationOrderedStateSpaceSpec(BaseModel):
153
+ column: str
154
+ state_order: list[str]
155
+ values: dict[str, str]
156
+ case_sensitive: bool = True
157
+
158
+ model_config = {"extra": "forbid"}
159
+
160
+ @field_validator("column", mode="after")
161
+ @classmethod
162
+ def _validate_column(cls, value: str) -> str:
163
+ if not isinstance(value, str) or not value.strip():
164
+ raise ValueError("ordered state-space column must be a non-empty string")
165
+ return value.strip()
166
+
167
+ @model_validator(mode="after")
168
+ def _validate_states(self) -> AnnotationOrderedStateSpaceSpec:
169
+ if not self.state_order:
170
+ raise ValueError("state_order must be a non-empty list")
171
+ state_ids = self.state_order
172
+ if any(not state_id.strip() for state_id in state_ids):
173
+ raise ValueError("state_order must contain non-empty strings")
174
+ if len(set(state_ids)) != len(state_ids):
175
+ raise ValueError("state ids must be unique")
176
+ if set(self.values) != set(state_ids):
177
+ raise ValueError("values must have exactly the ids declared by state_order")
178
+ source_values = [self.values[state_id] for state_id in state_ids]
179
+ if any(not value.strip() for value in source_values):
180
+ raise ValueError("values must map state ids to non-empty strings")
181
+ compared = source_values if self.case_sensitive else [value.strip().casefold() for value in source_values]
182
+ if len(set(compared)) != len(compared):
183
+ sensitivity = "true" if self.case_sensitive else "false"
184
+ raise ValueError(f"source values must be unique under case_sensitive={sensitivity}")
185
+ return self
186
+
187
+
188
+ class AnnotationSpec(BaseModel):
189
+ labels: dict[str, AnnotationLabelSpec] = Field(default_factory=dict)
190
+ orders: dict[str, AnnotationOrderSpec] = Field(default_factory=dict)
191
+ collections: dict[str, AnnotationCollectionSpec] = Field(default_factory=dict)
192
+ ordered_state_spaces: dict[str, AnnotationOrderedStateSpaceSpec] = Field(default_factory=dict)
193
+
194
+ model_config = {"extra": "forbid"}
195
+
196
+
197
+ class PlotOutputsSpec(BaseModel):
198
+ profile: str | None = None
199
+ include: list[str] = Field(default_factory=list)
200
+ exclude: list[str] = Field(default_factory=list)
201
+ views: dict[str, dict[str, Any]] = Field(default_factory=dict)
202
+
203
+ model_config = {"extra": "forbid"}
204
+
205
+
206
+ class ExportOutputsSpec(BaseModel):
207
+ include: list[str] = Field(default_factory=list)
208
+ exclude: list[str] = Field(default_factory=list)
209
+ artifacts: dict[str, dict[str, Any]] = Field(default_factory=dict)
210
+
211
+ model_config = {"extra": "forbid"}
212
+
213
+
214
+ class OutputsSpec(BaseModel):
215
+ plots: PlotOutputsSpec = Field(default_factory=PlotOutputsSpec)
216
+ exports: ExportOutputsSpec = Field(default_factory=ExportOutputsSpec)
217
+
218
+ model_config = {"extra": "forbid"}
219
+
220
+
221
+ class ProtocolBindingSpec(BaseModel):
222
+ id: str
223
+ inputs: dict[str, Any] = Field(default_factory=dict)
224
+ analysis: dict[str, Any] = Field(default_factory=dict)
225
+ outputs: OutputsSpec = Field(default_factory=OutputsSpec)
226
+
227
+ model_config = {"extra": "forbid"}
228
+
229
+ @field_validator("id", mode="after")
230
+ @classmethod
231
+ def _validate_id(cls, v: str) -> str:
232
+ if not isinstance(v, str) or not v.strip():
233
+ raise ConfigError("protocol.id must be a non-empty string")
234
+ return v
235
+
236
+
237
+ class ReaderSpec(BaseModel):
238
+ schema_: str = Field(alias="schema")
239
+ experiment: ExperimentSpec
240
+ protocol: ProtocolBindingSpec
241
+ paths: PathsSpec = Field(default_factory=PathsSpec)
242
+ plotting: PlottingSpec = Field(default_factory=PlottingSpec)
243
+ resources: ResourcesSpec = Field(default_factory=ResourcesSpec)
244
+ annotations: AnnotationSpec = Field(default_factory=AnnotationSpec)
245
+ evidence: EvidenceSpec | None = None
246
+
247
+ model_config = {"extra": "forbid"}
248
+
249
+ @field_validator("schema_", mode="after")
250
+ @classmethod
251
+ def _validate_schema(cls, v: str) -> str:
252
+ if v != "reader/v8":
253
+ raise ConfigError("Config schema must be 'reader/v8'. This repo only supports reader/v8.")
254
+ return v
255
+
256
+ @model_validator(mode="after")
257
+ def _validate_evidence_protocol_candidate(self) -> ReaderSpec:
258
+ if self.evidence is None:
259
+ return self
260
+
261
+ data_class = builtin_dop_registry().data_class(self.evidence.data_class)
262
+ protocol_id = self.protocol.id.strip()
263
+ if protocol_id not in data_class.protocol_candidates:
264
+ valid_candidates = ", ".join(data_class.protocol_candidates)
265
+ raise ValueError(
266
+ f"evidence.data_class {data_class.id!r} does not admit protocol.id {protocol_id!r}. "
267
+ f"Valid protocol candidates: {valid_candidates}"
268
+ )
269
+ return self
270
+
271
+ @classmethod
272
+ def load(cls, path: Path) -> ReaderSpec:
273
+ load_module = import_module("reader_workbench.workbench.config.load")
274
+ return load_module.load_reader_spec(path, cls=cls)
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from reader_workbench.plotting.style import PaletteBook
10
+ from reader_workbench.protocols import BoundProtocol
11
+ from reader_workbench.workbench.experiment import ExperimentSemantics
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class RunContext:
16
+ exp_dir: Path
17
+ outputs_dir: Path
18
+ artifacts_dir: Path
19
+ plots_dir: Path
20
+ exports_dir: Path
21
+ records_path: Path
22
+ logger: logging.Logger
23
+ palette_book: PaletteBook | None
24
+ experiment: ExperimentSemantics | None = None
25
+ protocol: BoundProtocol | None = None
26
+ config_digest: str = ""
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+
5
+ _EXPORTS = {
6
+ "build": {"build_workbench_decl", "load_workbench_decl"},
7
+ "model": {
8
+ "ExperimentDecl",
9
+ "FileInputDecl",
10
+ "InputBindingDecl",
11
+ "PipelineDecl",
12
+ "PluginStepDecl",
13
+ "RecipeSourceDecl",
14
+ "RecordCollectionInputDecl",
15
+ "RecordInputDecl",
16
+ "RecordOutputDecl",
17
+ "ResourceInputDecl",
18
+ "SurfaceDecl",
19
+ "WorkbenchDecl",
20
+ },
21
+ }
22
+
23
+ __all__ = tuple(sorted({name for names in _EXPORTS.values() for name in names}))
24
+
25
+
26
+ def __getattr__(name: str):
27
+ for module_name, names in _EXPORTS.items():
28
+ if name in names:
29
+ module = importlib.import_module(f"reader_workbench.workbench.decl.{module_name}")
30
+ return getattr(module, name)
31
+ raise AttributeError(name)
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from reader_workbench.errors import ConfigError
6
+ from reader_workbench.protocols.model import ProtocolBinding, ProtocolCatalog
7
+ from reader_workbench.workbench.config import (
8
+ FileResourceSpec,
9
+ ProtocolBindingSpec,
10
+ ReaderSpec,
11
+ RecordResourceSpec,
12
+ ResourceSpec,
13
+ reader_spec_digest,
14
+ )
15
+ from reader_workbench.workbench.experiment import (
16
+ AnnotationCollections,
17
+ AnnotationCollectionSpec,
18
+ AnnotationLabels,
19
+ AnnotationLabelSpec,
20
+ AnnotationOrders,
21
+ AnnotationOrderSpec,
22
+ AnnotationSemantics,
23
+ ExperimentEvidence,
24
+ ExperimentSemantics,
25
+ FileResourceEntry,
26
+ OrderedStateSpaces,
27
+ OrderedStateSpaceSpec,
28
+ OutputLayout,
29
+ RecordResourceEntry,
30
+ ResourceCatalog,
31
+ )
32
+ from reader_workbench.workbench.experiments import ExperimentCatalog
33
+ from reader_workbench.workbench.paths import resolve_path_within_root
34
+
35
+ from .model import ExperimentDecl, PipelineDecl, SurfaceDecl, WorkbenchDecl
36
+
37
+
38
+ def load_workbench_decl(path: Path, *, protocols: ProtocolCatalog) -> WorkbenchDecl:
39
+ spec = ReaderSpec.load(path)
40
+ return build_workbench_decl(spec, source_path=path, protocols=protocols)
41
+
42
+
43
+ def build_workbench_decl(
44
+ spec: ReaderSpec,
45
+ *,
46
+ source_path: Path,
47
+ protocols: ProtocolCatalog,
48
+ ) -> WorkbenchDecl:
49
+ root = source_path.parent.resolve()
50
+ experiment = ExperimentDecl(
51
+ id=spec.experiment.id,
52
+ title=spec.experiment.title or spec.experiment.id,
53
+ lifecycle=spec.experiment.lifecycle,
54
+ root=root,
55
+ )
56
+ layout = OutputLayout(
57
+ outputs_dir=_resolve_outputs_dir(spec.paths.outputs, root=root),
58
+ plots_subdir=_validate_output_subdir(spec.paths.plots, key="plots"),
59
+ exports_subdir=_validate_output_subdir(spec.paths.exports, key="exports"),
60
+ notebooks_subdir=_validate_output_subdir(spec.paths.notebooks, key="notebooks"),
61
+ )
62
+ resources = _bind_resources(spec.resources.by_id or {}, root=root)
63
+ protocol_binding = _bind_protocol(spec.protocol)
64
+ bound_protocol = protocols.bind(protocol_binding)
65
+ compiled = bound_protocol.compile()
66
+
67
+ experiment_semantics = ExperimentSemantics(
68
+ protocol=protocol_binding,
69
+ annotations=AnnotationSemantics(
70
+ labels=AnnotationLabels(
71
+ by_id={
72
+ key: AnnotationLabelSpec(
73
+ source=value.source,
74
+ values=dict(value.values or {}),
75
+ output=value.output,
76
+ )
77
+ for key, value in (spec.annotations.labels or {}).items()
78
+ }
79
+ ),
80
+ orders=AnnotationOrders(
81
+ by_id={
82
+ key: AnnotationOrderSpec(column=value.column, values=list(value.values or []))
83
+ for key, value in (spec.annotations.orders or {}).items()
84
+ }
85
+ ),
86
+ collections=AnnotationCollections(
87
+ by_id={
88
+ key: AnnotationCollectionSpec(
89
+ column=value.column,
90
+ items={item_key: list(item_values) for item_key, item_values in (value.items or {}).items()},
91
+ )
92
+ for key, value in (spec.annotations.collections or {}).items()
93
+ }
94
+ ),
95
+ ordered_state_spaces=OrderedStateSpaces(
96
+ by_id={
97
+ key: OrderedStateSpaceSpec(
98
+ column=value.column,
99
+ state_order=tuple(value.state_order),
100
+ source_values=dict(value.values),
101
+ case_sensitive=bool(value.case_sensitive),
102
+ )
103
+ for key, value in (spec.annotations.ordered_state_spaces or {}).items()
104
+ }
105
+ ),
106
+ ),
107
+ resources=resources,
108
+ layout=layout,
109
+ protocol_program=compiled.semantic_program,
110
+ evidence=(
111
+ ExperimentEvidence(
112
+ data_class=spec.evidence.data_class,
113
+ data_class_reason=spec.evidence.data_class_reason,
114
+ replicate_kind=spec.evidence.replicate_kind,
115
+ replicate_identity_field=spec.evidence.replicate_identity_field,
116
+ )
117
+ if spec.evidence is not None
118
+ else None
119
+ ),
120
+ )
121
+
122
+ return WorkbenchDecl(
123
+ experiment=experiment,
124
+ experiment_semantics=experiment_semantics,
125
+ plotting_palette=spec.plotting.palette if spec.plotting else None,
126
+ pipeline=PipelineDecl(runtime=dict(compiled.runtime or {}), steps=tuple(compiled.pipeline)),
127
+ plots=SurfaceDecl(specs=tuple(compiled.plots)),
128
+ exports=SurfaceDecl(specs=tuple(compiled.exports)),
129
+ config_digest=reader_spec_digest(spec),
130
+ )
131
+
132
+
133
+ def _resolve_outputs_dir(raw: str, *, root: Path) -> Path:
134
+ if not isinstance(raw, str) or not raw.strip():
135
+ raise ConfigError("paths.outputs must be a non-empty string path")
136
+ try:
137
+ return resolve_path_within_root(raw, root=root)
138
+ except ValueError as err:
139
+ raise ConfigError("paths.outputs must stay under the experiment root after resolving symlinks.") from err
140
+
141
+
142
+ def _validate_output_subdir(raw: str, *, key: str) -> str:
143
+ if raw is None:
144
+ raise ConfigError(f"paths.{key} must be a string subdirectory (use '.' to flatten).")
145
+ if not isinstance(raw, str):
146
+ raise ConfigError(f"paths.{key} must be a string subdirectory")
147
+ subdir = Path(raw)
148
+ if subdir.is_absolute():
149
+ raise ConfigError(f"paths.{key} must be relative to paths.outputs, not absolute.")
150
+ normalized = Path(".") / subdir
151
+ if any(part == ".." for part in normalized.parts):
152
+ raise ConfigError(f"paths.{key} must stay under paths.outputs and may not escape via '..'.")
153
+ return raw
154
+
155
+
156
+ def _bind_resources(resources: dict[str, ResourceSpec], *, root: Path) -> ResourceCatalog:
157
+ bound: dict[str, FileResourceEntry | RecordResourceEntry] = {}
158
+ experiment_catalog: ExperimentCatalog | None = None
159
+ for resource_id, resource in resources.items():
160
+ if isinstance(resource, FileResourceSpec):
161
+ try:
162
+ path = resolve_path_within_root(resource.path, root=root)
163
+ except ValueError as err:
164
+ raise ConfigError(
165
+ f"resources.{resource_id}.path must stay under the experiment root after resolving symlinks."
166
+ ) from err
167
+ bound[resource_id] = FileResourceEntry(kind="file", path=path)
168
+ continue
169
+ if isinstance(resource, RecordResourceSpec):
170
+ experiment_catalog = experiment_catalog or ExperimentCatalog.from_experiment_root(root)
171
+ location = experiment_catalog.resolve(resource.experiment)
172
+ bound[resource_id] = RecordResourceEntry(
173
+ kind="record",
174
+ experiment_id=location.id,
175
+ record_id=resource.record,
176
+ experiment_root=location.root,
177
+ outputs_dir=location.outputs_dir,
178
+ )
179
+ continue
180
+ raise ConfigError(f"resources.{resource_id} has unsupported kind {resource.kind!r}")
181
+ return ResourceCatalog(by_id=bound)
182
+
183
+
184
+ def _bind_protocol(spec: ProtocolBindingSpec) -> ProtocolBinding:
185
+ return ProtocolBinding(
186
+ id=spec.id,
187
+ inputs=dict(spec.inputs or {}),
188
+ analysis=dict(spec.analysis or {}),
189
+ outputs=spec.outputs.model_dump(exclude_none=True),
190
+ )
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from reader_workbench.workbench.experiment import ExperimentSemantics
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ExperimentDecl:
12
+ id: str
13
+ title: str
14
+ lifecycle: str
15
+ root: Path
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class RecordInputDecl:
20
+ record_id: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class FileInputDecl:
25
+ path: str
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ResourceInputDecl:
30
+ resource_id: str
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class RecordCollectionInputDecl:
35
+ resource_ids: tuple[str, ...]
36
+
37
+
38
+ InputBindingDecl = RecordInputDecl | FileInputDecl | ResourceInputDecl | RecordCollectionInputDecl
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class RecordOutputDecl:
43
+ record_id: str
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class RecipeSourceDecl:
48
+ recipe: str
49
+ with_: dict[str, Any] = field(default_factory=dict)
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class PluginStepDecl:
54
+ id: str
55
+ plugin: str
56
+ reads: dict[str, InputBindingDecl] = field(default_factory=dict)
57
+ writes: dict[str, RecordOutputDecl] = field(default_factory=dict)
58
+ with_: dict[str, Any] = field(default_factory=dict)
59
+ source_recipe: RecipeSourceDecl | None = None
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class PipelineDecl:
64
+ runtime: dict[str, Any] = field(default_factory=dict)
65
+ steps: tuple[PluginStepDecl, ...] = ()
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class SurfaceDecl:
70
+ specs: tuple[PluginStepDecl, ...] = ()
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class WorkbenchDecl:
75
+ experiment: ExperimentDecl
76
+ experiment_semantics: ExperimentSemantics
77
+ plotting_palette: str | None
78
+ pipeline: PipelineDecl
79
+ plots: SurfaceDecl
80
+ exports: SurfaceDecl
81
+ config_digest: str = ""
@@ -0,0 +1,12 @@
1
+ from .builtins import BUILTIN_DATA_CLASSES, BUILTIN_READY_SPECS, DOP_SCHEMA, builtin_dop_registry
2
+ from .model import DataClassSpec, DopRegistry, ReadySpec
3
+
4
+ __all__ = [
5
+ "BUILTIN_DATA_CLASSES",
6
+ "BUILTIN_READY_SPECS",
7
+ "DOP_SCHEMA",
8
+ "DataClassSpec",
9
+ "DopRegistry",
10
+ "ReadySpec",
11
+ "builtin_dop_registry",
12
+ ]