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,117 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import json
6
+ from bisect import bisect_right
7
+ from collections.abc import Callable, Mapping, Sequence
8
+ from dataclasses import dataclass
9
+
10
+ DEFAULT_PAGE_LIMIT = 25
11
+ MAX_PAGE_LIMIT = 100
12
+ _TOKEN_SCHEMA = "reader.page/v1"
13
+ _MAX_TOKEN_LENGTH = 4096
14
+
15
+
16
+ class PageRequestError(ValueError):
17
+ def __init__(self, field: str, reason: str) -> None:
18
+ super().__init__(reason)
19
+ self.field = field
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Page[T]:
24
+ items: tuple[T, ...]
25
+ limit: int
26
+ truncated: bool
27
+ continuation: str | None
28
+
29
+
30
+ def _selection_digest(selection: Mapping[str, object]) -> str:
31
+ encoded = json.dumps(selection, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
32
+ return "sha256:" + hashlib.sha256(encoded).hexdigest()
33
+
34
+
35
+ def _encode_token(*, surface: str, selection_digest: str, after: str) -> str:
36
+ payload = {
37
+ "after": after,
38
+ "schema": _TOKEN_SCHEMA,
39
+ "selection": selection_digest,
40
+ "surface": surface,
41
+ }
42
+ encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
43
+ return base64.urlsafe_b64encode(encoded).decode("ascii").rstrip("=")
44
+
45
+
46
+ def _decode_token(token: str) -> dict[str, str]:
47
+ if not token or len(token) > _MAX_TOKEN_LENGTH:
48
+ raise PageRequestError("continuation", "continuation must be a nonempty Reader page token")
49
+ try:
50
+ padding = "=" * (-len(token) % 4)
51
+ raw = base64.b64decode(token + padding, altchars=b"-_", validate=True)
52
+ payload = json.loads(raw.decode("utf-8"))
53
+ except (UnicodeDecodeError, ValueError, json.JSONDecodeError) as exc:
54
+ raise PageRequestError("continuation", "continuation is not a valid Reader page token") from exc
55
+ expected_keys = {"after", "schema", "selection", "surface"}
56
+ if not isinstance(payload, dict) or set(payload) != expected_keys:
57
+ raise PageRequestError("continuation", "continuation is not a valid Reader page token")
58
+ if any(not isinstance(payload[key], str) or not payload[key] for key in expected_keys):
59
+ raise PageRequestError("continuation", "continuation is not a valid Reader page token")
60
+ if payload["schema"] != _TOKEN_SCHEMA:
61
+ raise PageRequestError("continuation", "continuation uses an unsupported Reader page-token schema")
62
+ return payload
63
+
64
+
65
+ def _normalize_limit(limit: int | None) -> int:
66
+ if limit is None:
67
+ return DEFAULT_PAGE_LIMIT
68
+ if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= MAX_PAGE_LIMIT:
69
+ raise PageRequestError("limit", f"limit must be between 1 and {MAX_PAGE_LIMIT}")
70
+ return limit
71
+
72
+
73
+ def page_collection[T](
74
+ items: Sequence[T],
75
+ *,
76
+ key: Callable[[T], str],
77
+ surface: str,
78
+ selection: Mapping[str, object],
79
+ limit: int | None,
80
+ continuation: str | None,
81
+ ) -> Page[T]:
82
+ """Return one deterministic keyset page bound to a semantic selection."""
83
+
84
+ effective_limit = _normalize_limit(limit)
85
+ keyed = sorted(((key(item), item) for item in items), key=lambda pair: pair[0])
86
+ keys = [item_key for item_key, _ in keyed]
87
+ if any(not item_key for item_key in keys):
88
+ raise ValueError(f"{surface} collection contains an empty pagination key")
89
+ if len(keys) != len(set(keys)):
90
+ raise ValueError(f"{surface} collection contains duplicate pagination keys")
91
+
92
+ fingerprint = _selection_digest(selection)
93
+ start = 0
94
+ if continuation is not None:
95
+ token = _decode_token(continuation)
96
+ if token["surface"] != surface or token["selection"] != fingerprint:
97
+ raise PageRequestError(
98
+ "continuation",
99
+ "continuation does not match this collection and filter selection",
100
+ )
101
+ start = bisect_right(keys, token["after"])
102
+
103
+ selected = keyed[start : start + effective_limit]
104
+ truncated = start + effective_limit < len(keyed)
105
+ next_token = None
106
+ if truncated and selected:
107
+ next_token = _encode_token(
108
+ surface=surface,
109
+ selection_digest=fingerprint,
110
+ after=selected[-1][0],
111
+ )
112
+ return Page(
113
+ items=tuple(item for _, item in selected),
114
+ limit=effective_limit,
115
+ truncated=truncated,
116
+ continuation=next_token,
117
+ )
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich import box
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from reader_workbench.errors import ConfigError
11
+ from reader_workbench.workbench.commands import reader_command
12
+
13
+ from . import shared
14
+ from ._lazy import load as _load
15
+ from .shared import app, emit_json, normalize_output_format, table
16
+
17
+
18
+ @app.command(help="List built-in protocols or describe one.")
19
+ def protocols(
20
+ name: str | None = typer.Argument(
21
+ None,
22
+ metavar="[NAME]",
23
+ help=(
24
+ "Optional protocol id to describe "
25
+ "(e.g., plate_reader/dual_reporter_screen or plate_reader/single_reporter_screen)."
26
+ ),
27
+ ),
28
+ domain: str | None = typer.Option(None, "--domain", metavar="NAME", help="Filter protocols by domain."),
29
+ family: str | None = typer.Option(None, "--family", metavar="NAME", help="Filter protocols by family."),
30
+ example_config: bool = typer.Option(
31
+ False,
32
+ "--example-config",
33
+ help="Print a starter YAML outline for the named protocol.",
34
+ ),
35
+ format: str = typer.Option(
36
+ "table",
37
+ "--format",
38
+ metavar="FMT",
39
+ help="Output format: table | json (default: table).",
40
+ ),
41
+ section: str | None = typer.Option(
42
+ None,
43
+ "--section",
44
+ metavar="NAME",
45
+ help="Named-protocol JSON projection: identity | authoring | semantics | defaults | compiled.",
46
+ ),
47
+ ):
48
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
49
+ inspection_protocols = _load("reader_workbench.workbench.inspection.protocols")
50
+ inspection_runtime = _load("reader_workbench.workbench.inspection.runtime")
51
+ inspection_semantics = _load("reader_workbench.workbench.inspection.semantics")
52
+ try:
53
+ fmt = normalize_output_format(format)
54
+ selected_section = shared.normalize_semantic_section(
55
+ section,
56
+ allowed=inspection_protocols.PROTOCOL_DESCRIPTOR_SECTIONS,
57
+ format=fmt,
58
+ )
59
+ if example_config and not name:
60
+ raise typer.BadParameter("--example-config requires a protocol name.")
61
+ if selected_section is not None and not name:
62
+ raise typer.BadParameter("--section requires a protocol name", param_hint="--section")
63
+ if name:
64
+ descriptor = runtime.protocols.resolve(name)
65
+ if fmt == "json":
66
+ payload = inspection_protocols.protocol_descriptor_payload(descriptor, runtime=runtime)
67
+ if selected_section is not None:
68
+ payload = inspection_protocols.protocol_descriptor_section_payload(
69
+ payload,
70
+ section=selected_section,
71
+ )
72
+ emit_json(
73
+ payload,
74
+ projection=f"section:{selected_section}" if selected_section is not None else "full",
75
+ )
76
+ return
77
+ compilation_error = None
78
+ try:
79
+ bound_protocol = runtime.bind_protocol(
80
+ _load("reader_workbench.protocols").ProtocolBinding(id=descriptor.protocol)
81
+ )
82
+ except ConfigError as exc:
83
+ bound_protocol = None
84
+ compiled_plan = None
85
+ semantic_program = descriptor.semantic_program()
86
+ compilation_error = str(exc)
87
+ else:
88
+ compiled_plan = bound_protocol.compile()
89
+ semantic_program = compiled_plan.semantic_program
90
+ summary = table(f"Protocol: {descriptor.protocol}")
91
+ summary.add_column("Section", style="accent")
92
+ summary.add_column("Details")
93
+ summary.add_row("Domain", descriptor.domain)
94
+ summary.add_row("Family", descriptor.family)
95
+ summary.add_row("Summary", descriptor.summary)
96
+ if descriptor.tags:
97
+ summary.add_row("Tags", ", ".join(descriptor.tags))
98
+ if descriptor.resources:
99
+ summary.add_row(
100
+ "Input resources",
101
+ ", ".join(f"{item.id} → {item.path}" for item in descriptor.resources),
102
+ )
103
+ if descriptor.semantic_profiles:
104
+ summary.add_row("Profiles", ", ".join(profile.id for profile in descriptor.semantic_profiles))
105
+ if descriptor.factors:
106
+ summary.add_row("Factors", ", ".join(f"{item.name} ({item.role})" for item in descriptor.factors))
107
+ if descriptor.windows:
108
+ summary.add_row("Windows", ", ".join(item.id for item in descriptor.windows))
109
+ if descriptor.metrics:
110
+ summary.add_row("Metrics", ", ".join(item.id for item in descriptor.metrics))
111
+ if descriptor.ranking is not None:
112
+ summary.add_row("Primary ranking", descriptor.ranking.primary_metric)
113
+ if semantic_program.has_nodes:
114
+ summary.add_row(
115
+ "Semantic nodes",
116
+ str(
117
+ len(semantic_program.controls)
118
+ + len(semantic_program.windows)
119
+ + len(semantic_program.metrics)
120
+ + (1 if semantic_program.ranking is not None else 0)
121
+ ),
122
+ )
123
+ if descriptor.default_plot_profile is not None:
124
+ summary.add_row("Default plot profile", descriptor.default_plot_profile)
125
+ if compilation_error is not None:
126
+ summary.add_row("Default compilation", f"Requires authoring: {compilation_error}")
127
+ shared.console.print(Panel(summary, border_style="accent", box=box.ROUNDED))
128
+
129
+ input_rows = inspection_protocols.protocol_surface_rows(descriptor.input_fields)
130
+ if input_rows:
131
+ shared.console.print(
132
+ Panel(
133
+ inspection_protocols.protocol_surface_table("Inputs", input_rows),
134
+ border_style="accent",
135
+ box=box.ROUNDED,
136
+ )
137
+ )
138
+ analysis_rows = inspection_protocols.protocol_surface_rows(descriptor.analysis_fields)
139
+ if analysis_rows:
140
+ shared.console.print(
141
+ Panel(
142
+ inspection_protocols.protocol_surface_table("Analysis", analysis_rows),
143
+ border_style="accent",
144
+ box=box.ROUNDED,
145
+ )
146
+ )
147
+ if semantic_program.has_nodes:
148
+ shared.console.print(
149
+ Panel(
150
+ inspection_semantics.semantic_program_table(semantic_program, include_execution=False),
151
+ border_style="accent",
152
+ box=box.ROUNDED,
153
+ )
154
+ )
155
+ if semantic_program.has_nodes:
156
+ shared.console.print(
157
+ Panel(
158
+ inspection_semantics.semantic_program_table(
159
+ semantic_program,
160
+ title="Compiled Semantic Execution",
161
+ ),
162
+ border_style="accent",
163
+ box=box.ROUNDED,
164
+ )
165
+ )
166
+ if descriptor.plot_profiles:
167
+ shared.console.print(
168
+ Panel(
169
+ inspection_protocols.protocol_plot_profiles_table(descriptor),
170
+ border_style="accent",
171
+ box=box.ROUNDED,
172
+ )
173
+ )
174
+ if descriptor.figures:
175
+ shared.console.print(
176
+ Panel(
177
+ inspection_protocols.protocol_plot_outputs_table(descriptor),
178
+ border_style="accent",
179
+ box=box.ROUNDED,
180
+ )
181
+ )
182
+ if descriptor.artifacts:
183
+ shared.console.print(
184
+ Panel(
185
+ inspection_protocols.protocol_artifacts_table(descriptor),
186
+ border_style="accent",
187
+ box=box.ROUNDED,
188
+ )
189
+ )
190
+ if compiled_plan is not None and compiled_plan.pipeline:
191
+ shared.console.print(
192
+ Panel(
193
+ inspection_protocols.protocol_pipeline_table(compiled_plan.pipeline),
194
+ border_style="accent",
195
+ box=box.ROUNDED,
196
+ )
197
+ )
198
+ if compiled_plan is not None and bound_protocol is not None and compiled_plan.plots:
199
+ shared.console.print(
200
+ Panel(
201
+ inspection_protocols.protocol_surface_impl_table(
202
+ "Plot Implementations",
203
+ compiled_plan.plots,
204
+ inspection_runtime.plot_output_summaries(bound_protocol),
205
+ binding_display=inspection_runtime.binding_display,
206
+ ),
207
+ border_style="accent",
208
+ box=box.ROUNDED,
209
+ )
210
+ )
211
+ if compiled_plan is not None and bound_protocol is not None and compiled_plan.exports:
212
+ shared.console.print(
213
+ Panel(
214
+ inspection_protocols.protocol_surface_impl_table(
215
+ "Export Implementations",
216
+ compiled_plan.exports,
217
+ inspection_runtime.export_output_summaries(bound_protocol),
218
+ binding_display=inspection_runtime.binding_display,
219
+ ),
220
+ border_style="accent",
221
+ box=box.ROUNDED,
222
+ )
223
+ )
224
+ if example_config:
225
+ shared.console.print(
226
+ Panel(
227
+ inspection_protocols.protocol_example_config(descriptor),
228
+ title="Starter YAML",
229
+ border_style="accent",
230
+ box=box.ROUNDED,
231
+ )
232
+ )
233
+ return
234
+
235
+ if fmt == "json":
236
+ emit_json(
237
+ {
238
+ "protocols": [
239
+ {
240
+ "protocol": descriptor.protocol,
241
+ "domain": descriptor.domain,
242
+ "family": descriptor.family,
243
+ "summary": descriptor.summary,
244
+ "tags": list(descriptor.tags),
245
+ }
246
+ for descriptor in runtime.protocols.all()
247
+ if (not domain or descriptor.domain == domain) and (not family or descriptor.family == family)
248
+ ]
249
+ }
250
+ )
251
+ return
252
+
253
+ listing = table("Protocols")
254
+ listing.add_column("Name", style="accent", min_width=24)
255
+ listing.add_column("Domain")
256
+ listing.add_column("Family")
257
+ listing.add_column("Description")
258
+ for descriptor in runtime.protocols.all():
259
+ if domain and descriptor.domain != domain:
260
+ continue
261
+ if family and descriptor.family != family:
262
+ continue
263
+ listing.add_row(descriptor.protocol, descriptor.domain, descriptor.family, descriptor.summary)
264
+ shared.console.print(Panel(listing, border_style="accent", box=box.ROUNDED))
265
+ except ConfigError as err:
266
+ raise typer.BadParameter(str(err)) from err
267
+
268
+
269
+ @app.command(help="Scaffold a new experiment directory from a protocol starter config.")
270
+ def init(
271
+ target: str = typer.Argument(..., metavar="DIR", help="Experiment directory to create."),
272
+ protocol: str = typer.Option(
273
+ ...,
274
+ "--protocol",
275
+ "-p",
276
+ metavar="ID",
277
+ help=(
278
+ "Protocol id to bind, for example: plate_reader/dual_reporter_screen "
279
+ "or plate_reader/single_reporter_screen."
280
+ ),
281
+ ),
282
+ title: str | None = typer.Option(None, "--title", metavar="TEXT", help="Optional human-readable experiment title."),
283
+ force: bool = typer.Option(
284
+ False,
285
+ "--force",
286
+ help="Overwrite config.yaml when the target directory already contains one.",
287
+ ),
288
+ ):
289
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
290
+ try:
291
+ descriptor = runtime.protocols.resolve(protocol)
292
+ except ConfigError as err:
293
+ raise typer.BadParameter(str(err)) from err
294
+ target_dir = Path(target).expanduser()
295
+ if target_dir.suffix:
296
+ raise typer.BadParameter("init expects a directory path, not a file path")
297
+ target_dir = target_dir.resolve()
298
+ config_path = target_dir / "config.yaml"
299
+ if config_path.exists() and not force:
300
+ shared.abort(f"{config_path} already exists. Pass --force to overwrite it.")
301
+ target_dir.mkdir(parents=True, exist_ok=True)
302
+ (target_dir / "inputs").mkdir(exist_ok=True)
303
+ (target_dir / "notebooks").mkdir(exist_ok=True)
304
+ example_document = _load("reader_workbench.workbench.inspection.protocols").protocol_example_document(descriptor)
305
+ experiment_block = dict(example_document["experiment"])
306
+ experiment_block["id"] = target_dir.name
307
+ if title:
308
+ experiment_block["title"] = title
309
+ example_document["experiment"] = experiment_block
310
+ config_path.write_text(_load("yaml").safe_dump(example_document, sort_keys=False), encoding="utf-8")
311
+
312
+ summary = Table(box=box.ROUNDED, expand=True, show_header=False)
313
+ summary.add_column("Field", style="accent", no_wrap=True)
314
+ summary.add_column("Value")
315
+ summary.add_row("Protocol", descriptor.protocol)
316
+ summary.add_row("Experiment", experiment_block["id"])
317
+ summary.add_row("Config", str(config_path))
318
+ summary.add_row("Inputs dir", str(target_dir / "inputs"))
319
+ summary.add_row("Notebook dir", str(target_dir / "notebooks"))
320
+ for resource in descriptor.resources:
321
+ summary.add_row(f"Resource: {resource.id}", str((target_dir / resource.path).resolve()))
322
+ shared.console.print(Panel(summary, title="Experiment scaffolded", border_style="accent", box=box.ROUNDED))
323
+ shared.console.print(
324
+ Panel(
325
+ "\n".join(
326
+ [
327
+ reader_command("inspect", config_path),
328
+ reader_command("validate", config_path, "--no-files"),
329
+ reader_command("protocols", descriptor.protocol),
330
+ ]
331
+ ),
332
+ title="Next steps",
333
+ border_style="accent",
334
+ box=box.ROUNDED,
335
+ )
336
+ )