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,309 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess # noqa: F401
4
+ import sys # noqa: F401
5
+ from collections.abc import Callable, Mapping, Sequence
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich import box
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich.theme import Theme
14
+ from rich.traceback import install as rich_tracebacks
15
+
16
+ from reader_workbench._version import package_version
17
+ from reader_workbench.errors import ReaderError
18
+
19
+ from .automation import (
20
+ emit_document,
21
+ error_envelope,
22
+ json_requested,
23
+ reader_error_details,
24
+ success_envelope,
25
+ )
26
+ from .pagination import Page, PageRequestError, page_collection
27
+
28
+ THEME = Theme(
29
+ {
30
+ "title": "bold cyan",
31
+ "accent": "cyan",
32
+ "ok": "bold green",
33
+ "warn": "bold yellow",
34
+ "error": "bold red",
35
+ "muted": "dim",
36
+ "path": "magenta",
37
+ }
38
+ )
39
+
40
+ app = typer.Typer(
41
+ add_completion=False,
42
+ invoke_without_command=True,
43
+ help=(
44
+ "reader — experimental workbench.\n\n"
45
+ "Discover assays and experiments, inspect workflow plans, validate YAML, "
46
+ "run pipelines, and produce plots, exports, or notebooks. "
47
+ "Start with 'reader demo', 'reader ls', or 'reader protocols'."
48
+ ),
49
+ )
50
+ console = Console(theme=THEME)
51
+ rich_tracebacks(show_locals=False)
52
+
53
+ JOB_INDEX_SCOPE_NOTE = "resolved against the nearest experiments/ root from the current working directory"
54
+ JOB_ARG_HELP = (
55
+ f"Path to config.yaml • experiment directory • or numeric index from the default 'reader ls' inventory, "
56
+ f"{JOB_INDEX_SCOPE_NOTE}."
57
+ )
58
+ JOB_ARG_HELP_WITH_DEFAULT = f"{JOB_ARG_HELP[:-1]} (defaults to nearest ./config.yaml)"
59
+ JOB_ARG_HELP_SHORT = (
60
+ f"Experiment config path, directory, or index from the default 'reader ls' inventory, {JOB_INDEX_SCOPE_NOTE}."
61
+ )
62
+
63
+ PLOT_ONLY_OPTION = typer.Option(None, "--only", help="Run only the specified plot id (repeatable).")
64
+ PLOT_EXCLUDE_OPTION = typer.Option(None, "--exclude", help="Exclude the specified plot id (repeatable).")
65
+ PLOT_INPUT_OPTION = typer.Option(
66
+ None,
67
+ "--input",
68
+ metavar="KEY=VALUE",
69
+ help="Override reads bindings for selected plot specs (repeatable).",
70
+ )
71
+ PLOT_SET_OPTION = typer.Option(
72
+ None,
73
+ "--set",
74
+ metavar="PATH=VALUE",
75
+ help="Patch spec fields for selected plots (reads.*, with.*, writes.*). Repeatable.",
76
+ )
77
+ EXPORT_ONLY_OPTION = typer.Option(None, "--only", help="Run only the specified export id (repeatable).")
78
+ EXPORT_EXCLUDE_OPTION = typer.Option(None, "--exclude", help="Exclude the specified export id (repeatable).")
79
+ EXPORT_INPUT_OPTION = typer.Option(
80
+ None,
81
+ "--input",
82
+ metavar="KEY=VALUE",
83
+ help="Override reads bindings for selected export specs (repeatable).",
84
+ )
85
+ EXPORT_SET_OPTION = typer.Option(
86
+ None,
87
+ "--set",
88
+ metavar="PATH=VALUE",
89
+ help="Patch spec fields for selected exports (reads.*, with.*, writes.*). Repeatable.",
90
+ )
91
+ NOTEBOOK_MODE_OPTION = typer.Option(
92
+ "edit",
93
+ "--mode",
94
+ help="Launch mode: edit | run | none (default: edit).",
95
+ )
96
+
97
+
98
+ @app.callback(invoke_without_command=True)
99
+ def _main(
100
+ ctx: typer.Context,
101
+ version: bool = typer.Option(False, "--version", is_eager=True, help="Show the installed Reader version."),
102
+ ) -> None:
103
+ """Show help when no command is provided."""
104
+ if version:
105
+ typer.echo(package_version())
106
+ raise typer.Exit()
107
+ if ctx.invoked_subcommand is None:
108
+ typer.echo(ctx.get_help())
109
+ raise typer.Exit()
110
+
111
+
112
+ def checkmark(cond: bool) -> str:
113
+ return "[ok]✓[/ok]" if cond else "[muted]—[/muted]"
114
+
115
+
116
+ def table(title: str) -> Table:
117
+ return Table(
118
+ title=f"[title]{title}[/title]",
119
+ title_justify="left",
120
+ header_style="bold",
121
+ box=box.ROUNDED,
122
+ expand=True,
123
+ show_lines=False,
124
+ show_edge=True,
125
+ )
126
+
127
+
128
+ def abort(msg: str, *, code: int = 1) -> None:
129
+ if json_requested():
130
+ emit_document(
131
+ error_envelope(
132
+ code="command_rejected",
133
+ field="command",
134
+ reason=msg,
135
+ remediation="Correct the reported command state, then retry.",
136
+ retryable=False,
137
+ )
138
+ )
139
+ raise typer.Exit(code=code)
140
+ console.print(Panel.fit(f"[error]✗ {msg}[/error]", border_style="error", box=box.ROUNDED))
141
+ raise typer.Exit(code=code)
142
+
143
+
144
+ def handle_reader_error(err: ReaderError) -> None:
145
+ if json_requested():
146
+ emit_document(error_envelope(**reader_error_details(err)))
147
+ raise typer.Exit(code=1)
148
+ abort(str(err))
149
+
150
+
151
+ def normalize_output_format(
152
+ value: str | None,
153
+ *,
154
+ default: str = "table",
155
+ allowed: tuple[str, ...] = ("table", "json"),
156
+ ) -> str:
157
+ if not isinstance(value, str):
158
+ return default
159
+ fmt = value.strip().lower() or default
160
+ if fmt not in allowed:
161
+ raise typer.BadParameter(f"format must be one of: {', '.join(allowed)}")
162
+ return fmt
163
+
164
+
165
+ def normalize_semantic_section(
166
+ value: str | None,
167
+ *,
168
+ allowed: Sequence[str],
169
+ format: str,
170
+ ) -> str | None:
171
+ if not isinstance(value, str):
172
+ return None
173
+ section = value.strip().lower()
174
+ if format != "json":
175
+ raise typer.BadParameter("--section requires --format json", param_hint="--section")
176
+ if section not in allowed:
177
+ raise typer.BadParameter(
178
+ f"section must be one of: {', '.join(allowed)}",
179
+ param_hint="--section",
180
+ )
181
+ return section
182
+
183
+
184
+ def normalize_paging_options(
185
+ limit: int | object,
186
+ continuation: str | object,
187
+ ) -> tuple[int | None, str | None]:
188
+ normalized_limit = limit if type(limit) is int else None
189
+ normalized_continuation = continuation.strip() if isinstance(continuation, str) else None
190
+ return normalized_limit, normalized_continuation or None
191
+
192
+
193
+ def page_json_collection[T](
194
+ items: Sequence[T],
195
+ *,
196
+ key: Callable[[T], str],
197
+ surface: str,
198
+ selection: Mapping[str, object],
199
+ limit: int | None,
200
+ continuation: str | None,
201
+ ) -> Page[T]:
202
+ try:
203
+ return page_collection(
204
+ items,
205
+ key=key,
206
+ surface=surface,
207
+ selection=selection,
208
+ limit=limit,
209
+ continuation=continuation,
210
+ )
211
+ except PageRequestError as exc:
212
+ option = f"--{exc.field.replace('_', '-')}"
213
+ raise typer.BadParameter(str(exc), param_hint=option) from exc
214
+
215
+
216
+ def require_json_paging(*, format: str, limit: int | None, continuation: str | None) -> None:
217
+ if format == "json" or (limit is None and continuation is None):
218
+ return
219
+ option = "--continuation" if continuation is not None else "--limit"
220
+ raise typer.BadParameter(f"{option} requires --format json", param_hint=option)
221
+
222
+
223
+ def normalize_flag(value: bool | object, *, default: bool = False) -> bool:
224
+ return value if isinstance(value, bool) else default
225
+
226
+
227
+ def normalize_status_filter(value: str | None) -> str | None:
228
+ if not isinstance(value, str):
229
+ return None
230
+ normalized = value.strip().lower()
231
+ if not normalized:
232
+ return None
233
+ allowed = {"ok", "config_error"}
234
+ if normalized not in allowed:
235
+ raise typer.BadParameter(f"status must be one of: {', '.join(sorted(allowed))}")
236
+ return normalized
237
+
238
+
239
+ def normalize_lifecycle_filter(value: str | None) -> str | None:
240
+ if not isinstance(value, str):
241
+ return None
242
+ normalized = value.strip().lower()
243
+ if not normalized:
244
+ return None
245
+ allowed = {"active", "draft", "template"}
246
+ if normalized not in allowed:
247
+ raise typer.BadParameter(f"lifecycle must be one of: {', '.join(sorted(allowed))}")
248
+ return normalized
249
+
250
+
251
+ def _is_protocol_binding_value_ref(value: object) -> bool:
252
+ value_type = type(value)
253
+ # Keep plain CLI imports off the protocol bootstrap path.
254
+ return (
255
+ value_type.__module__ == "reader_workbench.protocols.model" and value_type.__name__ == "ProtocolBindingValueRef"
256
+ )
257
+
258
+
259
+ def json_friendly(value):
260
+ if _is_protocol_binding_value_ref(value):
261
+ payload = {"binding_value": value.key}
262
+ if value.has_default:
263
+ payload["default"] = json_friendly(value.default)
264
+ return payload
265
+ if isinstance(value, Path):
266
+ return str(value)
267
+ if isinstance(value, dict):
268
+ return {str(key): json_friendly(item) for key, item in value.items()}
269
+ if isinstance(value, tuple):
270
+ return [json_friendly(item) for item in value]
271
+ if isinstance(value, list):
272
+ return [json_friendly(item) for item in value]
273
+ return value
274
+
275
+
276
+ def emit_json(
277
+ payload: object,
278
+ *,
279
+ projection: str = "full",
280
+ truncated: bool = False,
281
+ continuation: str | None = None,
282
+ ) -> None:
283
+ emit_document(
284
+ success_envelope(
285
+ json_friendly(payload),
286
+ projection=projection,
287
+ truncated=truncated,
288
+ continuation=continuation,
289
+ )
290
+ )
291
+
292
+
293
+ def emit_json_error(
294
+ *,
295
+ code: str,
296
+ field: str,
297
+ reason: str,
298
+ remediation: str,
299
+ retryable: bool = False,
300
+ ) -> None:
301
+ emit_document(
302
+ error_envelope(
303
+ code=code,
304
+ field=field,
305
+ reason=reason,
306
+ remediation=remediation,
307
+ retryable=retryable,
308
+ )
309
+ )