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,538 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from collections.abc import Iterable
5
+ from copy import deepcopy
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+ from rich.theme import Theme
11
+
12
+ from reader_workbench.errors import ConfigError, ContractError, RecordError
13
+ from reader_workbench.protocols import ProtocolBinding
14
+ from reader_workbench.runtime import ReaderRuntime, builtin_runtime
15
+ from reader_workbench.workbench.config import ReaderSpec
16
+ from reader_workbench.workbench.decl import build_workbench_decl
17
+ from reader_workbench.workbench.engine import normalize_log_level, run_spec, validation_summary
18
+ from reader_workbench.workbench.graph import resolve_workbench, select_workbench_specs
19
+ from reader_workbench.workbench.inspection.catalogs import workbench_surface_specs_payload
20
+ from reader_workbench.workbench.inspection.experiments import (
21
+ experiment_explain_payload,
22
+ experiment_identity_payload,
23
+ experiment_inspect_payload,
24
+ )
25
+ from reader_workbench.workbench.inspection.results import record_catalog_payload
26
+ from reader_workbench.workbench.inspection.runtime import workbench_record_ids, workbench_record_verification_scope
27
+ from reader_workbench.workbench.notebooks import CANONICAL_NOTEBOOK_ID, write_experiment_notebook
28
+ from reader_workbench.workbench.paths import resolve_confined_sink_root
29
+ from reader_workbench.workbench.records import (
30
+ DataFrameArtifactRecord,
31
+ verify_record_store,
32
+ )
33
+
34
+ from ._record_reads import resolve_record_revision
35
+ from .models import (
36
+ DataFrameRecordResult,
37
+ Experiment,
38
+ ExperimentEvidence,
39
+ ExperimentIdentity,
40
+ InspectionResult,
41
+ NotebookResult,
42
+ PlanResult,
43
+ PluginCatalogResult,
44
+ PluginDescriptorResult,
45
+ PluginPort,
46
+ PluginSummary,
47
+ RecordCatalogResult,
48
+ RecordRevision,
49
+ RunResult,
50
+ SelectedSteps,
51
+ SurfaceCatalogResult,
52
+ ValidationResult,
53
+ VerificationResult,
54
+ )
55
+
56
+ _SILENT_CONSOLE_THEME = Theme(
57
+ {
58
+ "title": "bold cyan",
59
+ "accent": "cyan",
60
+ "ok": "bold green",
61
+ "warn": "bold yellow",
62
+ "error": "bold red",
63
+ "muted": "dim",
64
+ "path": "magenta",
65
+ }
66
+ )
67
+
68
+
69
+ def open_experiment(path: str | Path, *, runtime: ReaderRuntime | None = None) -> Experiment:
70
+ """Load and compile one experiment without creating experiment outputs."""
71
+
72
+ config_path = Path(path).expanduser().resolve()
73
+ if config_path.is_dir():
74
+ config_path = config_path / "config.yaml"
75
+ elif config_path.is_file() and config_path.name != "config.yaml":
76
+ config_path = next(
77
+ (base / "config.yaml" for base in config_path.parents if (base / "config.yaml").is_file()),
78
+ config_path,
79
+ )
80
+ if not config_path.exists() or not config_path.is_file():
81
+ raise ConfigError(f"Experiment config not found: {config_path}")
82
+ active_runtime = runtime or builtin_runtime()
83
+ spec = ReaderSpec.load(config_path)
84
+ declaration = build_workbench_decl(spec, source_path=config_path, protocols=active_runtime.protocols)
85
+ return Experiment(
86
+ config_path=config_path,
87
+ _spec=spec,
88
+ _declaration=declaration,
89
+ _runtime=active_runtime,
90
+ )
91
+
92
+
93
+ def inspect(experiment: Experiment) -> InspectionResult:
94
+ payload = experiment_inspect_payload(
95
+ job_path=experiment.config_path,
96
+ spec=experiment._spec,
97
+ decl=experiment._declaration,
98
+ runtime=experiment._runtime,
99
+ )
100
+ return InspectionResult(
101
+ experiment=_identity(payload["experiment"]),
102
+ authoring=deepcopy(payload["authoring"]),
103
+ semantics=deepcopy(payload["semantics"]),
104
+ implementation=deepcopy(payload["implementation"]),
105
+ )
106
+
107
+
108
+ def validate(experiment: Experiment, *, check_files: bool = True) -> ValidationResult:
109
+ summary = validation_summary(
110
+ experiment._declaration,
111
+ check_files=check_files,
112
+ exp_root=experiment._declaration.experiment.root,
113
+ runtime=experiment._runtime,
114
+ )
115
+ summary_copy = deepcopy(summary)
116
+ status = str(summary_copy.pop("status"))
117
+ summary_fields = {
118
+ "checks": summary_copy.pop("checks"),
119
+ "counts": summary_copy.pop("counts"),
120
+ }
121
+ return ValidationResult(
122
+ experiment=experiment.identity,
123
+ check_files=check_files,
124
+ status=status,
125
+ summary=summary_fields,
126
+ validation=summary_copy,
127
+ )
128
+
129
+
130
+ def plan(experiment: Experiment) -> PlanResult:
131
+ payload = experiment_explain_payload(
132
+ job_path=experiment.config_path,
133
+ spec=experiment._spec,
134
+ decl=experiment._declaration,
135
+ runtime=experiment._runtime,
136
+ )
137
+ implementation = payload["implementation"]
138
+ return PlanResult(
139
+ experiment=_identity(payload["experiment"]),
140
+ plan=deepcopy(implementation["plan"]),
141
+ compiled=deepcopy(implementation["compiled"]),
142
+ semantics=deepcopy(payload["semantics"]),
143
+ )
144
+
145
+
146
+ def notebook(
147
+ experiment: Experiment,
148
+ *,
149
+ name: str | None = None,
150
+ overwrite: bool = False,
151
+ ) -> NotebookResult:
152
+ """Generate Reader's canonical notebook inside the experiment outputs."""
153
+
154
+ decl = experiment._declaration
155
+ target_name = name or f"EDA_{datetime.now().strftime('%Y%m%d')}.py"
156
+ candidate = Path(target_name)
157
+ if (
158
+ not target_name
159
+ or target_name != target_name.strip()
160
+ or candidate.is_absolute()
161
+ or len(candidate.parts) != 1
162
+ or candidate.suffix != ".py"
163
+ ):
164
+ raise ConfigError("Notebook name must be one non-empty .py filename")
165
+ layout = decl.experiment_semantics.layout
166
+ notebooks_root = (
167
+ layout.outputs_dir
168
+ if layout.notebooks_subdir in {"", ".", "./"}
169
+ else layout.outputs_dir / layout.notebooks_subdir
170
+ )
171
+ try:
172
+ notebooks_root = resolve_confined_sink_root(
173
+ notebooks_root,
174
+ root=decl.experiment.root,
175
+ label="notebooks",
176
+ )
177
+ except ValueError as exc:
178
+ raise ConfigError(str(exc)) from exc
179
+ path, created = write_experiment_notebook(
180
+ notebooks_root / target_name,
181
+ experiment_root=decl.experiment.root,
182
+ notebooks_root=notebooks_root,
183
+ overwrite=overwrite,
184
+ )
185
+ return NotebookResult(
186
+ experiment=experiment.identity,
187
+ path=str(path),
188
+ template=CANONICAL_NOTEBOOK_ID,
189
+ created=created,
190
+ )
191
+
192
+
193
+ def plots(
194
+ experiment: Experiment,
195
+ *,
196
+ only: Iterable[str] = (),
197
+ exclude: Iterable[str] = (),
198
+ ) -> SurfaceCatalogResult:
199
+ only_ids = list(only)
200
+ exclude_ids = list(exclude)
201
+ workbench = resolve_workbench(experiment._declaration)
202
+ selected = select_workbench_specs(
203
+ list(workbench.plots),
204
+ only=only_ids,
205
+ exclude=exclude_ids,
206
+ kind_label="plot",
207
+ )
208
+ bound_protocol = experiment._runtime.bind_protocol(experiment._declaration.experiment_semantics.protocol)
209
+ payload = workbench_surface_specs_payload(
210
+ job_path=experiment.config_path,
211
+ decl=experiment._declaration,
212
+ runtime=experiment._runtime,
213
+ bound_protocol=bound_protocol,
214
+ selected=selected,
215
+ kind="plot",
216
+ only=only_ids,
217
+ exclude=exclude_ids,
218
+ )
219
+ return SurfaceCatalogResult(
220
+ experiment=_identity(payload["experiment"]),
221
+ kind="plot",
222
+ protocol=str(payload["catalog"]["protocol"]),
223
+ selection=deepcopy(payload["selection"]),
224
+ summary=deepcopy(payload["summary"]),
225
+ entries=tuple(deepcopy(payload["plots"])),
226
+ )
227
+
228
+
229
+ def records(experiment: Experiment, *, include_history: bool = False) -> RecordCatalogResult:
230
+ decl = experiment._declaration
231
+ layout = decl.experiment_semantics.layout
232
+ outputs_dir = layout.outputs_dir
233
+ store = experiment._runtime.record_store(
234
+ outputs_dir,
235
+ plots_subdir=layout.plots_subdir,
236
+ exports_subdir=layout.exports_subdir,
237
+ experiment_root=decl.experiment.root,
238
+ create=False,
239
+ )
240
+ if not store.catalog_exists():
241
+ return RecordCatalogResult(
242
+ experiment=experiment.identity,
243
+ catalog_exists=False,
244
+ catalog={
245
+ "path": str(store.records_path),
246
+ "outputs_root": str(outputs_dir),
247
+ "schema_version": None,
248
+ "provenance_epoch_id": None,
249
+ "active_invocation_ledger": None,
250
+ },
251
+ include_history=include_history,
252
+ summary={
253
+ "records": 0,
254
+ "history": {"included": include_history, "revisions": 0 if include_history else None},
255
+ "by_kind": {},
256
+ "by_producer": {},
257
+ },
258
+ entries=(),
259
+ )
260
+ current_record_ids = frozenset()
261
+ if not include_history:
262
+ workbench = resolve_workbench(decl)
263
+ current_record_ids = workbench_record_ids(workbench, runtime=experiment._runtime)
264
+ payload = record_catalog_payload(
265
+ experiment=experiment_identity_payload(job_path=experiment.config_path, decl=decl),
266
+ store=store,
267
+ outputs_dir=outputs_dir,
268
+ runtime=experiment._runtime,
269
+ include_history=include_history,
270
+ current_config_digest=decl.config_digest,
271
+ declared_record_ids=current_record_ids,
272
+ )
273
+ return RecordCatalogResult(
274
+ experiment=_identity(payload["experiment"]),
275
+ catalog_exists=True,
276
+ catalog=deepcopy(payload["catalog"]),
277
+ include_history=include_history,
278
+ summary=deepcopy(payload["summary"]),
279
+ entries=tuple(deepcopy(payload["records"])),
280
+ )
281
+
282
+
283
+ def read_dataframe(
284
+ experiment: Experiment,
285
+ record_id: str,
286
+ *,
287
+ revision: int | None = None,
288
+ revision_digest: str | None = None,
289
+ row_limit: int | None = None,
290
+ ) -> DataFrameRecordResult:
291
+ """Load and validate an exact dataframe revision, optionally bounded by rows."""
292
+
293
+ store, record, record_revision = resolve_record_revision(
294
+ experiment,
295
+ record_id,
296
+ revision=revision,
297
+ revision_digest=revision_digest,
298
+ missing_label="Dataframe record",
299
+ )
300
+ if not isinstance(record, DataFrameArtifactRecord):
301
+ raise RecordError(f"Record {record_id!r} exists but is not a dataframe artifact")
302
+ dataframe = record.load_dataframe() if row_limit is None else record.load_dataframe(row_limit=row_limit)
303
+ try:
304
+ store.contracts.validate(
305
+ dataframe,
306
+ contract_id=record.contract_id,
307
+ where=f"public-api:{record.record_id}",
308
+ )
309
+ except ContractError as exc:
310
+ raise RecordError(
311
+ f"Dataframe record {record.record_id!r} violates recorded contract {record.contract_id!r}: {exc}"
312
+ ) from exc
313
+ return DataFrameRecordResult(
314
+ experiment=experiment.identity,
315
+ record=record_revision,
316
+ contract_id=record.contract_id,
317
+ content_digest=record.content_digest,
318
+ dataframe=dataframe,
319
+ )
320
+
321
+
322
+ def verify(experiment: Experiment) -> VerificationResult:
323
+ """Verify the current catalog without creating or changing experiment outputs."""
324
+ decl = experiment._declaration
325
+ workbench = resolve_workbench(decl)
326
+ layout = decl.experiment_semantics.layout
327
+ store = experiment._runtime.record_store(
328
+ layout.outputs_dir,
329
+ plots_subdir=layout.plots_subdir,
330
+ exports_subdir=layout.exports_subdir,
331
+ experiment_root=decl.experiment.root,
332
+ create=False,
333
+ )
334
+ report = verify_record_store(
335
+ store,
336
+ experiment_root=decl.experiment.root,
337
+ expected_config_digest=decl.config_digest,
338
+ scope=workbench_record_verification_scope(workbench, runtime=experiment._runtime),
339
+ )
340
+ return VerificationResult(
341
+ experiment=experiment.identity,
342
+ status=str(report["status"]),
343
+ summary=deepcopy(report["summary"]),
344
+ issues=tuple(deepcopy(report["issues"])),
345
+ records=tuple(deepcopy(report["records"])),
346
+ )
347
+
348
+
349
+ def run(
350
+ experiment: Experiment,
351
+ *,
352
+ from_step: str | None = None,
353
+ until_step: str | None = None,
354
+ only: str | None = None,
355
+ reset_records: bool = False,
356
+ dry_run: bool = False,
357
+ log_level: str = "INFO",
358
+ ) -> RunResult:
359
+ """Run an experiment pipeline through the same execution path as the CLI."""
360
+
361
+ from_step = _step_selector(from_step, field="from_step")
362
+ until_step = _step_selector(until_step, field="until_step")
363
+ only = _step_selector(only, field="only")
364
+ if only is not None and (from_step is not None or until_step is not None):
365
+ raise ConfigError("only cannot be combined with from_step or until_step")
366
+ if reset_records and (only is not None or from_step is not None or until_step is not None):
367
+ raise ConfigError("reset_records requires a complete run without from_step, until_step, or only")
368
+ if reset_records and dry_run:
369
+ raise ConfigError("reset_records cannot be combined with dry_run")
370
+ normalize_log_level(log_level)
371
+ if not dry_run and experiment._declaration.experiment.lifecycle != "active":
372
+ lifecycle = experiment._declaration.experiment.lifecycle
373
+ raise ConfigError(f"Experiment lifecycle {lifecycle!r} is not runnable")
374
+
375
+ execution = run_spec(
376
+ experiment._declaration,
377
+ resume_from=only if only is not None else from_step,
378
+ until=only if only is not None else until_step,
379
+ reset_records=reset_records,
380
+ dry_run=dry_run,
381
+ log_level=log_level,
382
+ verbose=False,
383
+ console=Console(quiet=True, theme=_SILENT_CONSOLE_THEME),
384
+ include_pipeline=True,
385
+ include_plots=False,
386
+ include_exports=False,
387
+ show_next_steps=False,
388
+ runtime=experiment._runtime,
389
+ )
390
+ return RunResult(
391
+ experiment=experiment.identity,
392
+ invocation_id=execution.invocation_id,
393
+ provenance_epoch_id=execution.provenance_epoch_id,
394
+ operation=execution.operation,
395
+ status=execution.status,
396
+ dry_run=execution.dry_run,
397
+ selected_steps=SelectedSteps(
398
+ pipeline=execution.selected_steps.pipeline,
399
+ plots=execution.selected_steps.plots,
400
+ exports=execution.selected_steps.exports,
401
+ ),
402
+ produced_record_revisions=tuple(
403
+ RecordRevision(
404
+ record_id=revision.record_id,
405
+ revision=revision.revision,
406
+ revision_digest=revision.revision_digest,
407
+ )
408
+ for revision in execution.produced_record_revisions
409
+ ),
410
+ ledger_path=str(execution.ledger_path) if execution.ledger_path is not None else None,
411
+ )
412
+
413
+
414
+ def _step_selector(value: str | None, *, field: str) -> str | None:
415
+ if value is None:
416
+ return None
417
+ if not isinstance(value, str) or not value.strip():
418
+ raise ConfigError(f"{field} must be a non-empty step id")
419
+ return value.strip()
420
+
421
+
422
+ def plugins(
423
+ *,
424
+ category: str | None = None,
425
+ domain: str | None = None,
426
+ family: str | None = None,
427
+ protocol: str | None = None,
428
+ runtime: ReaderRuntime | None = None,
429
+ ) -> PluginCatalogResult:
430
+ active_runtime = runtime or builtin_runtime()
431
+ descriptors = list(active_runtime.plugins.catalog().filter(category=category, domain=domain, family=family))
432
+ if protocol is not None:
433
+ bound_protocol = active_runtime.bind_protocol(ProtocolBinding(id=protocol))
434
+ compiled = bound_protocol.compile()
435
+ allowed = {step.plugin for step in (*compiled.pipeline, *compiled.plots, *compiled.exports)}
436
+ descriptors = [descriptor for descriptor in descriptors if descriptor.plugin in allowed]
437
+ plugin_items = tuple(_plugin_summary(descriptor) for descriptor in descriptors)
438
+ return PluginCatalogResult(
439
+ selection={"category": category, "domain": domain, "family": family, "protocol": protocol},
440
+ summary={
441
+ "plugins": len(plugin_items),
442
+ "by_category": _counts(item.category for item in plugin_items),
443
+ "by_domain": _counts(item.domain for item in plugin_items),
444
+ "by_family": _counts(item.family for item in plugin_items),
445
+ },
446
+ plugins=plugin_items,
447
+ )
448
+
449
+
450
+ def describe_plugin(
451
+ plugin_id: str,
452
+ *,
453
+ runtime: ReaderRuntime | None = None,
454
+ ) -> PluginDescriptorResult:
455
+ active_runtime = runtime or builtin_runtime()
456
+ descriptor = active_runtime.plugins.resolve_descriptor(plugin_id)
457
+ plugin_cls = descriptor.cls
458
+ return PluginDescriptorResult(
459
+ plugin=_plugin_summary(descriptor),
460
+ config_schema=deepcopy(plugin_cls.ConfigModel.model_json_schema()),
461
+ input_ports=tuple(
462
+ PluginPort(
463
+ name=port.name,
464
+ kind=port.kind,
465
+ optional=port.optional,
466
+ contract=port.contract,
467
+ )
468
+ for _, port in sorted(plugin_cls.input_ports().items())
469
+ ),
470
+ output_ports=tuple(
471
+ PluginPort(
472
+ name=port.name,
473
+ kind=port.kind,
474
+ optional=False,
475
+ contract=port.contract,
476
+ contract_surface=_contract_surface(port),
477
+ )
478
+ for _, port in sorted(plugin_cls.output_ports().items())
479
+ ),
480
+ )
481
+
482
+
483
+ def _identity(payload: object) -> ExperimentIdentity:
484
+ if not isinstance(payload, dict):
485
+ raise TypeError("Experiment identity payload must be a mapping")
486
+ evidence_payload = payload.get("evidence")
487
+ evidence = None
488
+ if evidence_payload is not None:
489
+ if not isinstance(evidence_payload, dict):
490
+ raise TypeError("Experiment evidence payload must be a mapping when present")
491
+ evidence = ExperimentEvidence(
492
+ data_class=str(evidence_payload["data_class"]),
493
+ data_class_reason=str(evidence_payload["data_class_reason"]),
494
+ replicate_kind=str(evidence_payload["replicate_kind"]),
495
+ replicate_identity_field=(
496
+ str(evidence_payload["replicate_identity_field"])
497
+ if evidence_payload.get("replicate_identity_field") is not None
498
+ else None
499
+ ),
500
+ )
501
+ return ExperimentIdentity(
502
+ id=str(payload["id"]),
503
+ title=str(payload["title"]),
504
+ lifecycle=str(payload["lifecycle"]),
505
+ protocol=str(payload["protocol"]),
506
+ config=str(payload["config"]),
507
+ root=str(payload["root"]),
508
+ evidence=evidence,
509
+ )
510
+
511
+
512
+ def _plugin_summary(descriptor) -> PluginSummary:
513
+ return PluginSummary(
514
+ plugin=descriptor.plugin,
515
+ key=descriptor.key,
516
+ category=str(descriptor.category),
517
+ domain=descriptor.domain,
518
+ family=descriptor.family,
519
+ summary=descriptor.summary,
520
+ implementation=f"{descriptor.cls.__module__}.{descriptor.cls.__name__}",
521
+ )
522
+
523
+
524
+ def _contract_surface(port) -> dict[str, object] | None:
525
+ surface = port.contract_surface
526
+ if surface is None:
527
+ return None
528
+ return {
529
+ "minimum": surface.minimum,
530
+ "runtime_mode": surface.runtime_mode,
531
+ "promoted": list(surface.promoted),
532
+ "note": surface.note,
533
+ "rendered": surface.render(),
534
+ }
535
+
536
+
537
+ def _counts(values: Iterable[str]) -> dict[str, int]:
538
+ return dict(sorted(Counter(value for value in values if value).items()))