mostlyright-data 0.9.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 (314) hide show
  1. mostlyright/data_harness/__init__.py +158 -0
  2. mostlyright/data_harness/acquisition/__init__.py +55 -0
  3. mostlyright/data_harness/acquisition/http.py +2773 -0
  4. mostlyright/data_harness/acquisition/parsing.py +809 -0
  5. mostlyright/data_harness/acquisition/ranges.py +495 -0
  6. mostlyright/data_harness/acquisition/result_download.py +360 -0
  7. mostlyright/data_harness/acquisition/retention_admission.py +248 -0
  8. mostlyright/data_harness/acquisition/sandbox.py +4888 -0
  9. mostlyright/data_harness/acquisition/url_policy.py +530 -0
  10. mostlyright/data_harness/agent_runtime.py +2743 -0
  11. mostlyright/data_harness/assets/logo-ink.svg +31 -0
  12. mostlyright/data_harness/backends/__init__.py +28 -0
  13. mostlyright/data_harness/backends/pandas_backend.py +350 -0
  14. mostlyright/data_harness/backends/polars_backend.py +366 -0
  15. mostlyright/data_harness/backends/protocol.py +124 -0
  16. mostlyright/data_harness/backends/reference.py +83 -0
  17. mostlyright/data_harness/backends/registry.py +55 -0
  18. mostlyright/data_harness/backends/restrictions.py +126 -0
  19. mostlyright/data_harness/canonical.py +333 -0
  20. mostlyright/data_harness/catalog_job.py +625 -0
  21. mostlyright/data_harness/cli.py +5398 -0
  22. mostlyright/data_harness/contracts.py +53 -0
  23. mostlyright/data_harness/coordinator.py +1307 -0
  24. mostlyright/data_harness/deploy.py +924 -0
  25. mostlyright/data_harness/deploy_target.py +312 -0
  26. mostlyright/data_harness/deployment_evidence.py +1067 -0
  27. mostlyright/data_harness/event_presentation.py +576 -0
  28. mostlyright/data_harness/events.py +2152 -0
  29. mostlyright/data_harness/fast_delimited.py +239 -0
  30. mostlyright/data_harness/fleet.py +237 -0
  31. mostlyright/data_harness/formats.py +236 -0
  32. mostlyright/data_harness/governors.py +1163 -0
  33. mostlyright/data_harness/hosted_bootstrap.py +972 -0
  34. mostlyright/data_harness/hosted_crawler.py +1115 -0
  35. mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
  36. mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
  37. mostlyright/data_harness/hosted_crawler_job.py +1277 -0
  38. mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
  39. mostlyright/data_harness/hosted_dataset.py +1500 -0
  40. mostlyright/data_harness/hosted_deploy.py +3037 -0
  41. mostlyright/data_harness/hosted_handoff.py +62 -0
  42. mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
  43. mostlyright/data_harness/hosted_ingestion_job.py +356 -0
  44. mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
  45. mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
  46. mostlyright/data_harness/hosted_session_worker.py +3554 -0
  47. mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
  48. mostlyright/data_harness/hosted_worker.py +6784 -0
  49. mostlyright/data_harness/ingestion/__init__.py +56 -0
  50. mostlyright/data_harness/ingestion/contracts.py +461 -0
  51. mostlyright/data_harness/ingestion/faults.py +42 -0
  52. mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
  53. mostlyright/data_harness/ingestion/spool.py +130 -0
  54. mostlyright/data_harness/ingestion/store.py +885 -0
  55. mostlyright/data_harness/key_seam.py +434 -0
  56. mostlyright/data_harness/linux_process_boundary.py +262 -0
  57. mostlyright/data_harness/local_contracts.py +2880 -0
  58. mostlyright/data_harness/local_search/__init__.py +5 -0
  59. mostlyright/data_harness/local_search/build_index.py +1087 -0
  60. mostlyright/data_harness/local_search/contracts.py +920 -0
  61. mostlyright/data_harness/local_search/query_trace.py +266 -0
  62. mostlyright/data_harness/local_search/retrieval.py +700 -0
  63. mostlyright/data_harness/local_search/sealed.py +474 -0
  64. mostlyright/data_harness/local_search/service.py +784 -0
  65. mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
  66. mostlyright/data_harness/nbrender/__init__.py +12 -0
  67. mostlyright/data_harness/nbrender/chrome.py +359 -0
  68. mostlyright/data_harness/nbrender/code_body.py +266 -0
  69. mostlyright/data_harness/nbrender/document.py +407 -0
  70. mostlyright/data_harness/nbrender/frame.py +275 -0
  71. mostlyright/data_harness/nbrender/interactive.py +337 -0
  72. mostlyright/data_harness/nbrender/markdown_body.py +477 -0
  73. mostlyright/data_harness/nbrender/mr_components.py +134 -0
  74. mostlyright/data_harness/nbrender/outputs_data.py +595 -0
  75. mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
  76. mostlyright/data_harness/nbrender/outputs_source.py +260 -0
  77. mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
  78. mostlyright/data_harness/nbrender/outputs_text.py +400 -0
  79. mostlyright/data_harness/nbrender/parse.py +394 -0
  80. mostlyright/data_harness/nbrender/status.py +40 -0
  81. mostlyright/data_harness/nbrender/tokens.py +1295 -0
  82. mostlyright/data_harness/notebook.py +1710 -0
  83. mostlyright/data_harness/offline.py +2049 -0
  84. mostlyright/data_harness/operation_registry.py +1007 -0
  85. mostlyright/data_harness/operator_setup.py +239 -0
  86. mostlyright/data_harness/pipeline.py +6428 -0
  87. mostlyright/data_harness/plan_graph.py +2026 -0
  88. mostlyright/data_harness/preparation/__init__.py +104 -0
  89. mostlyright/data_harness/preparation/contracts.py +1017 -0
  90. mostlyright/data_harness/preparation/engine.py +221 -0
  91. mostlyright/data_harness/preparation/errors.py +14 -0
  92. mostlyright/data_harness/preparation/gates.py +751 -0
  93. mostlyright/data_harness/preparation/joins.py +574 -0
  94. mostlyright/data_harness/preparation/profile.py +384 -0
  95. mostlyright/data_harness/preparation/table.py +217 -0
  96. mostlyright/data_harness/preparation/transforms.py +568 -0
  97. mostlyright/data_harness/progress_events.py +534 -0
  98. mostlyright/data_harness/readers/__init__.py +46 -0
  99. mostlyright/data_harness/readers/containers.py +963 -0
  100. mostlyright/data_harness/readers/contracts.py +542 -0
  101. mostlyright/data_harness/readers/delimited.py +257 -0
  102. mostlyright/data_harness/readers/grib2/__init__.py +33 -0
  103. mostlyright/data_harness/readers/grib2/admission.py +722 -0
  104. mostlyright/data_harness/readers/grib2/decode.py +1009 -0
  105. mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
  106. mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
  107. mostlyright/data_harness/readers/json_tabular.py +485 -0
  108. mostlyright/data_harness/readers/registry.py +514 -0
  109. mostlyright/data_harness/readers/samples/README.md +110 -0
  110. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
  111. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
  112. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
  113. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
  114. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
  115. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
  116. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
  117. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
  118. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
  119. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
  120. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  121. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
  122. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  123. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
  124. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
  125. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
  126. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
  127. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
  128. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
  129. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
  130. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
  131. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
  132. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
  133. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
  134. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
  135. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
  136. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  137. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  138. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  139. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  140. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
  141. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
  142. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
  143. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
  144. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  145. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
  146. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
  147. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
  148. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  149. mostlyright/data_harness/readers/samples.py +582 -0
  150. mostlyright/data_harness/readers/spreadsheet.py +803 -0
  151. mostlyright/data_harness/readers/tabular.py +510 -0
  152. mostlyright/data_harness/recipe.py +5321 -0
  153. mostlyright/data_harness/repair/__init__.py +78 -0
  154. mostlyright/data_harness/repair/adapters.py +274 -0
  155. mostlyright/data_harness/repair/contracts.py +872 -0
  156. mostlyright/data_harness/repair/coordinator.py +1099 -0
  157. mostlyright/data_harness/repair/errors.py +16 -0
  158. mostlyright/data_harness/review.py +2533 -0
  159. mostlyright/data_harness/rowset.py +283 -0
  160. mostlyright/data_harness/serving.py +1975 -0
  161. mostlyright/data_harness/serving_edge.py +590 -0
  162. mostlyright/data_harness/serving_http.py +1031 -0
  163. mostlyright/data_harness/session_probes.py +759 -0
  164. mostlyright/data_harness/signing.py +101 -0
  165. mostlyright/data_harness/source_discovery.py +898 -0
  166. mostlyright/data_harness/sources/__init__.py +209 -0
  167. mostlyright/data_harness/sources/_adapter_steps.py +213 -0
  168. mostlyright/data_harness/sources/adapters.py +1214 -0
  169. mostlyright/data_harness/sources/cadence.py +1428 -0
  170. mostlyright/data_harness/sources/cadence_emission.py +453 -0
  171. mostlyright/data_harness/sources/cadence_history.py +546 -0
  172. mostlyright/data_harness/sources/catalog/__init__.py +17 -0
  173. mostlyright/data_harness/sources/catalog/admission.py +477 -0
  174. mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
  175. mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
  176. mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
  177. mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
  178. mostlyright/data_harness/sources/catalog/channel.py +523 -0
  179. mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
  180. mostlyright/data_harness/sources/catalog/contracts.py +825 -0
  181. mostlyright/data_harness/sources/catalog/coverage.py +137 -0
  182. mostlyright/data_harness/sources/catalog/delta.py +1340 -0
  183. mostlyright/data_harness/sources/catalog/embedding.py +532 -0
  184. mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
  185. mostlyright/data_harness/sources/catalog/fill.py +3889 -0
  186. mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
  187. mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
  188. mostlyright/data_harness/sources/catalog/gating.py +374 -0
  189. mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
  190. mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
  191. mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
  192. mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
  193. mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
  194. mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
  195. mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
  196. mostlyright/data_harness/sources/catalog/health.py +447 -0
  197. mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
  198. mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
  199. mostlyright/data_harness/sources/catalog/neural.py +1618 -0
  200. mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
  201. mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
  202. mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
  203. mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
  204. mostlyright/data_harness/sources/catalog/recommend.py +171 -0
  205. mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
  206. mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
  207. mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
  208. mostlyright/data_harness/sources/catalog/sealed.py +560 -0
  209. mostlyright/data_harness/sources/catalog/search.py +230 -0
  210. mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
  211. mostlyright/data_harness/sources/catalog/update.py +891 -0
  212. mostlyright/data_harness/sources/collections.py +815 -0
  213. mostlyright/data_harness/sources/contracts.py +2223 -0
  214. mostlyright/data_harness/sources/deletion.py +761 -0
  215. mostlyright/data_harness/sources/fitness.py +162 -0
  216. mostlyright/data_harness/sources/governance.py +163 -0
  217. mostlyright/data_harness/sources/hosted.py +173 -0
  218. mostlyright/data_harness/sources/integration.py +218 -0
  219. mostlyright/data_harness/sources/range_reader.py +418 -0
  220. mostlyright/data_harness/sources/registry.py +514 -0
  221. mostlyright/data_harness/sources/rights_rule.py +59 -0
  222. mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
  223. mostlyright/data_harness/sources/sports.py +521 -0
  224. mostlyright/data_harness/sources/stream.py +524 -0
  225. mostlyright/data_harness/sources/stream_connector.py +418 -0
  226. mostlyright/data_harness/sources/stream_recorder.py +1404 -0
  227. mostlyright/data_harness/studio_boundary.py +2019 -0
  228. mostlyright/data_harness/thin/__init__.py +37 -0
  229. mostlyright/data_harness/thin/acquire.py +1137 -0
  230. mostlyright/data_harness/thin/acquire_cancel.py +579 -0
  231. mostlyright/data_harness/thin/approvals.py +617 -0
  232. mostlyright/data_harness/thin/commands.py +406 -0
  233. mostlyright/data_harness/thin/download.py +194 -0
  234. mostlyright/data_harness/thin/narrative.py +589 -0
  235. mostlyright/data_harness/thin/parity.py +1070 -0
  236. mostlyright/data_harness/thin/propose.py +2759 -0
  237. mostlyright/data_harness/thin/research.py +1663 -0
  238. mostlyright/data_harness/thin/router.py +924 -0
  239. mostlyright/data_harness/thin/runs.py +519 -0
  240. mostlyright/data_harness/thin/session.py +281 -0
  241. mostlyright/data_harness/thin/stream.py +501 -0
  242. mostlyright/data_harness/thin/transport.py +187 -0
  243. mostlyright/data_harness/thin/vocabulary.py +368 -0
  244. mostlyright/data_harness/thin/workers.py +164 -0
  245. mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
  246. mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
  247. mostlyright/data_harness/unit_flow.py +927 -0
  248. mostlyright/data_harness/units.py +572 -0
  249. mostlyright/data_harness/ux/__init__.py +9 -0
  250. mostlyright/data_harness/ux/approve.py +485 -0
  251. mostlyright/data_harness/ux/author_yaml.py +597 -0
  252. mostlyright/data_harness/ux/cloud_auth.py +447 -0
  253. mostlyright/data_harness/ux/commands/__init__.py +260 -0
  254. mostlyright/data_harness/ux/commands/approve.py +136 -0
  255. mostlyright/data_harness/ux/commands/auth.py +744 -0
  256. mostlyright/data_harness/ux/commands/author.py +79 -0
  257. mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
  258. mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
  259. mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
  260. mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
  261. mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
  262. mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
  263. mostlyright/data_harness/ux/commands/deploy.py +134 -0
  264. mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
  265. mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
  266. mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
  267. mostlyright/data_harness/ux/commands/diff.py +74 -0
  268. mostlyright/data_harness/ux/commands/index.py +84 -0
  269. mostlyright/data_harness/ux/commands/inventory.py +47 -0
  270. mostlyright/data_harness/ux/commands/list_builds.py +143 -0
  271. mostlyright/data_harness/ux/commands/login.py +63 -0
  272. mostlyright/data_harness/ux/commands/peek.py +236 -0
  273. mostlyright/data_harness/ux/commands/plan_check.py +90 -0
  274. mostlyright/data_harness/ux/commands/preflight.py +97 -0
  275. mostlyright/data_harness/ux/commands/record.py +107 -0
  276. mostlyright/data_harness/ux/commands/review_setup.py +47 -0
  277. mostlyright/data_harness/ux/commands/search.py +440 -0
  278. mostlyright/data_harness/ux/commands/show.py +61 -0
  279. mostlyright/data_harness/ux/commands/whoami.py +37 -0
  280. mostlyright/data_harness/ux/credential_native.py +551 -0
  281. mostlyright/data_harness/ux/credential_store.py +1055 -0
  282. mostlyright/data_harness/ux/credentials.py +631 -0
  283. mostlyright/data_harness/ux/diffing.py +444 -0
  284. mostlyright/data_harness/ux/headline.py +671 -0
  285. mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
  286. mostlyright/data_harness/ux/hosted_run_status.py +619 -0
  287. mostlyright/data_harness/ux/inventory.py +427 -0
  288. mostlyright/data_harness/ux/local_review.py +375 -0
  289. mostlyright/data_harness/ux/login.py +691 -0
  290. mostlyright/data_harness/ux/path_kind.py +147 -0
  291. mostlyright/data_harness/ux/peek.py +1000 -0
  292. mostlyright/data_harness/ux/plain_file.py +178 -0
  293. mostlyright/data_harness/ux/plan_check.py +311 -0
  294. mostlyright/data_harness/ux/preflight.py +918 -0
  295. mostlyright/data_harness/ux/readers.py +1124 -0
  296. mostlyright/data_harness/ux/remediation.py +2195 -0
  297. mostlyright/data_harness/ux/render.py +657 -0
  298. mostlyright/data_harness/ux/workload.py +1077 -0
  299. mostlyright/data_harness/viewer.py +3713 -0
  300. mostlyright/data_harness/visual_run/__init__.py +83 -0
  301. mostlyright/data_harness/visual_run/authoring.py +235 -0
  302. mostlyright/data_harness/visual_run/contracts.py +673 -0
  303. mostlyright/data_harness/visual_run/materialize.py +486 -0
  304. mostlyright/data_harness/visual_run/observations.py +874 -0
  305. mostlyright/data_harness/visual_run/query.py +259 -0
  306. mostlyright/data_harness/visual_run/reducer.py +280 -0
  307. mostlyright/data_harness/visual_run/sdk.py +892 -0
  308. mostlyright/data_harness/visual_run/store.py +584 -0
  309. mostlyright/data_harness/visual_run/transport.py +239 -0
  310. mostlyright/data_harness/watch.py +2999 -0
  311. mostlyright_data-0.9.0.dist-info/METADATA +607 -0
  312. mostlyright_data-0.9.0.dist-info/RECORD +314 -0
  313. mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
  314. mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
@@ -0,0 +1,673 @@
1
+ """Strict, data-only contracts for durable visual run events.
2
+
3
+ The records in this module describe persisted work. They do not authorize a Build, dataset
4
+ handoff, approval, publication, or release. Sequence is the only ordering coordinate; timestamps
5
+ are observability metadata. Large values live in immutable artifacts and events carry references.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import datetime as dt
11
+ import json
12
+ import re
13
+ from collections.abc import Mapping
14
+ from dataclasses import dataclass
15
+ from types import MappingProxyType
16
+ from typing import Any, Literal
17
+
18
+ EVENT_SCHEMA_VERSION = "visual-run-event.v2"
19
+ ARTIFACT_SCHEMA_VERSION = "visual-run-artifact-ref.v2"
20
+ MAX_EVENT_BYTES = 32 * 1024
21
+ MAX_IDENTIFIER_CHARS = 128
22
+ MAX_STAGE_CHARS = 64
23
+ MAX_TEXT_CHARS = 512
24
+ MAX_ARTIFACT_BYTES = 1 << 40
25
+
26
+ ACTORS = frozenset({"agent", "worker", "coordinator", "viewer"})
27
+ DISPLAY_DISPOSITIONS = frozenset({"inline", "summary", "download-only", "redacted"})
28
+ TERMINAL_STATUSES = frozenset({"completed", "failed", "interrupted"})
29
+ DOCUMENT_HEADER_CELL_ID = "research-report"
30
+ LEGACY_DOCUMENT_HEADER_CELL_ID_PREFIX = "legacy-research-report"
31
+ RESERVED_CELL_IDS = frozenset({"build", "table", "quality"})
32
+ RESERVED_CELL_PREFIXES = ("attempt:", "notebook:", "run:", "source:", "stage:")
33
+
34
+ _IDENTIFIER = re.compile(r"\A[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,127})\Z")
35
+ _COORDINATE = re.compile(r"\A[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*\.v[1-9][0-9]*\Z")
36
+ _MEDIA_TYPE = re.compile(r"\A[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+\Z")
37
+ _DIGEST = re.compile(r"\A[0-9a-f]{64}\Z")
38
+ _ERROR_CODE = re.compile(r"\A[A-Z][A-Z0-9_]{0,63}\Z")
39
+ _TIMESTAMP = re.compile(
40
+ r"\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,6})?Z\Z"
41
+ )
42
+
43
+ _EVENT_FIELDS = frozenset(
44
+ {
45
+ "schema_version",
46
+ "run_id",
47
+ "sequence",
48
+ "event_type",
49
+ "event_version",
50
+ "actor",
51
+ "phase",
52
+ "stage",
53
+ "object_id",
54
+ "object_version",
55
+ "previous_object_version",
56
+ "observed_at",
57
+ "idempotency_key",
58
+ "payload",
59
+ "artifact",
60
+ "status",
61
+ "error",
62
+ }
63
+ )
64
+ _ARTIFACT_FIELDS = frozenset(
65
+ {
66
+ "schema_version",
67
+ "artifact_id",
68
+ "sha256",
69
+ "media_type",
70
+ "artifact_schema",
71
+ "byte_size",
72
+ "truncated",
73
+ "original_byte_size",
74
+ "display",
75
+ }
76
+ )
77
+ _ERROR_FIELDS = frozenset({"code", "message", "retryable"})
78
+
79
+
80
+ class VisualRunError(ValueError):
81
+ """A typed contract refusal safe for presentation at the protocol boundary."""
82
+
83
+ def __init__(self, code: str, message: str) -> None:
84
+ super().__init__(message)
85
+ self.code = code
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class PayloadField:
90
+ kind: Literal["text", "integer", "boolean", "digest"]
91
+ choices: frozenset[str] = frozenset()
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class EventSpec:
96
+ version: int
97
+ payload: Mapping[str, PayloadField]
98
+ object_rule: Literal["create", "update"]
99
+ artifact_policy: Literal["forbidden", "optional", "required"] = "forbidden"
100
+ artifact_schemas: frozenset[str] = frozenset()
101
+ terminal_status: str | None = None
102
+ error_policy: Literal["forbidden", "optional", "required"] = "forbidden"
103
+
104
+
105
+ def _fields(**values: PayloadField) -> Mapping[str, PayloadField]:
106
+ return MappingProxyType(values)
107
+
108
+
109
+ TEXT = PayloadField("text")
110
+ INTEGER = PayloadField("integer")
111
+ BOOLEAN = PayloadField("boolean")
112
+ DIGEST = PayloadField("digest")
113
+ CELL_TYPE = PayloadField("text", frozenset({"markdown", "code"}))
114
+ OUTPUT_KIND = PayloadField("text", frozenset({"stream", "display_data", "execute_result", "error"}))
115
+
116
+ _CELL_OUTPUT_SCHEMA = "mostlyright.cell-output.v1"
117
+ _CELL_SOURCE_SCHEMA = "mostlyright.cell-source.v1"
118
+ _SOURCE_OBSERVATION_SCHEMA = "mostlyright.source-observation.v1"
119
+ _STAGE_OBSERVATION_SCHEMA = "stage-observation.v1"
120
+ _STAGE_OBSERVATION_SCHEMA_V2 = "stage-observation.v2"
121
+ _TABLE_SCHEMA = "mostlyright.table-reference.v1"
122
+ _NOTEBOOK_SCHEMA = "mostlyright.notebook.v1"
123
+ _QUALITY_OBSERVATION_SCHEMA = "mostlyright.quality-observation.v1"
124
+
125
+ ARTIFACT_SCHEMAS: Mapping[str, frozenset[str]] = MappingProxyType(
126
+ {
127
+ _CELL_OUTPUT_SCHEMA: frozenset({"application/json", "text/plain"}),
128
+ _CELL_SOURCE_SCHEMA: frozenset({"text/markdown", "text/x-python"}),
129
+ _SOURCE_OBSERVATION_SCHEMA: frozenset({"application/json"}),
130
+ _STAGE_OBSERVATION_SCHEMA: frozenset({"application/json"}),
131
+ _STAGE_OBSERVATION_SCHEMA_V2: frozenset({"application/json"}),
132
+ _TABLE_SCHEMA: frozenset({"application/json"}),
133
+ _NOTEBOOK_SCHEMA: frozenset({"application/x-ipynb+json"}),
134
+ _QUALITY_OBSERVATION_SCHEMA: frozenset({"application/json"}),
135
+ }
136
+ )
137
+
138
+
139
+ def _spec(
140
+ payload: Mapping[str, PayloadField],
141
+ object_rule: Literal["create", "update"],
142
+ *,
143
+ artifact_policy: Literal["forbidden", "optional", "required"] = "forbidden",
144
+ artifact_schemas: frozenset[str] = frozenset(),
145
+ terminal_status: str | None = None,
146
+ error_policy: Literal["forbidden", "optional", "required"] = "forbidden",
147
+ ) -> EventSpec:
148
+ return EventSpec(
149
+ 1,
150
+ payload,
151
+ object_rule,
152
+ artifact_policy,
153
+ artifact_schemas,
154
+ terminal_status,
155
+ error_policy,
156
+ )
157
+
158
+
159
+ # One declaration owns the initial vocabulary, payload coordinate, object-version rule, artifact
160
+ # policy, and terminal shape. Consumers inspect this table; they do not restate it.
161
+ EVENT_REGISTRY: Mapping[str, EventSpec] = MappingProxyType(
162
+ {
163
+ "run.started": _spec(_fields(), "create"),
164
+ "run.completed": _spec(_fields(), "update", terminal_status="completed"),
165
+ "run.failed": _spec(_fields(), "update", terminal_status="failed", error_policy="required"),
166
+ "run.interrupted": _spec(
167
+ _fields(), "update", terminal_status="interrupted", error_policy="optional"
168
+ ),
169
+ "phase.started": _spec(_fields(), "create"),
170
+ "phase.completed": _spec(_fields(), "update"),
171
+ "phase.failed": _spec(_fields(), "update", error_policy="required"),
172
+ "cell.created": _spec(_fields(cell_type=CELL_TYPE), "create"),
173
+ "cell.source_extended": _spec(
174
+ _fields(source_length=INTEGER),
175
+ "update",
176
+ artifact_policy="required",
177
+ artifact_schemas=frozenset({_CELL_SOURCE_SCHEMA}),
178
+ ),
179
+ "cell.source_revised": _spec(
180
+ _fields(source_length=INTEGER),
181
+ "update",
182
+ artifact_policy="required",
183
+ artifact_schemas=frozenset({_CELL_SOURCE_SCHEMA}),
184
+ ),
185
+ "cell.moved": _spec(_fields(after_cell_id=TEXT), "update"),
186
+ "cell.execution_started": _spec(_fields(execution_count=INTEGER), "update"),
187
+ "cell.output_appended": _spec(
188
+ _fields(output_index=INTEGER, output_kind=OUTPUT_KIND),
189
+ "update",
190
+ artifact_policy="required",
191
+ artifact_schemas=frozenset({_CELL_OUTPUT_SCHEMA}),
192
+ ),
193
+ "cell.output_replaced": _spec(
194
+ _fields(output_index=INTEGER, output_kind=OUTPUT_KIND),
195
+ "update",
196
+ artifact_policy="required",
197
+ artifact_schemas=frozenset({_CELL_OUTPUT_SCHEMA}),
198
+ ),
199
+ "cell.completed": _spec(_fields(), "update"),
200
+ "cell.failed": _spec(_fields(), "update", error_policy="required"),
201
+ "source.selected": _spec(_fields(source_kind=TEXT), "create"),
202
+ "source.rejected": _spec(_fields(reason_code=TEXT), "update"),
203
+ "source.schema_observed": _spec(
204
+ _fields(column_count=INTEGER),
205
+ "update",
206
+ artifact_policy="required",
207
+ artifact_schemas=frozenset({_SOURCE_OBSERVATION_SCHEMA}),
208
+ ),
209
+ "source.observation_ready": _spec(
210
+ _fields(observation_kind=TEXT),
211
+ "update",
212
+ artifact_policy="required",
213
+ artifact_schemas=frozenset({_SOURCE_OBSERVATION_SCHEMA}),
214
+ ),
215
+ "transformation.started": _spec(_fields(operation=TEXT), "create"),
216
+ "transformation.completed": _spec(_fields(operation=TEXT, row_count=INTEGER), "update"),
217
+ "stage.observation_ready": _spec(
218
+ _fields(operation=TEXT, row_count=INTEGER),
219
+ "update",
220
+ artifact_policy="required",
221
+ artifact_schemas=frozenset({_STAGE_OBSERVATION_SCHEMA, _STAGE_OBSERVATION_SCHEMA_V2}),
222
+ ),
223
+ "quality.completed": _spec(
224
+ _fields(checks_passed=INTEGER, checks_total=INTEGER),
225
+ "create",
226
+ artifact_policy="required",
227
+ artifact_schemas=frozenset({_QUALITY_OBSERVATION_SCHEMA}),
228
+ ),
229
+ "table_written": _spec(
230
+ _fields(row_count=INTEGER, column_count=INTEGER),
231
+ "create",
232
+ artifact_policy="required",
233
+ artifact_schemas=frozenset({_TABLE_SCHEMA}),
234
+ ),
235
+ "build.sealed": _spec(_fields(candidate_digest=DIGEST), "create"),
236
+ "build.verified": _spec(_fields(candidate_digest=DIGEST), "update"),
237
+ "notebook.research_materialized": _spec(
238
+ _fields(notebook_digest=DIGEST),
239
+ "create",
240
+ artifact_policy="required",
241
+ artifact_schemas=frozenset({_NOTEBOOK_SCHEMA}),
242
+ ),
243
+ "notebook.table_materialized": _spec(
244
+ _fields(notebook_digest=DIGEST),
245
+ "create",
246
+ artifact_policy="required",
247
+ artifact_schemas=frozenset({_NOTEBOOK_SCHEMA}),
248
+ ),
249
+ }
250
+ )
251
+
252
+
253
+ def _refuse(code: str, message: str) -> None:
254
+ raise VisualRunError(code, message)
255
+
256
+
257
+ def _exact_fields(value: Mapping[str, Any], expected: frozenset[str]) -> None:
258
+ actual = set(value)
259
+ if actual != expected:
260
+ unknown = sorted(actual - expected)
261
+ missing = sorted(expected - actual)
262
+ code = "VISUAL_RUN_UNKNOWN_FIELD" if unknown else "VISUAL_RUN_MISSING_FIELD"
263
+ _refuse(code, f"unknown={unknown!r}, missing={missing!r}")
264
+
265
+
266
+ def _identifier(value: Any, name: str, *, max_chars: int = MAX_IDENTIFIER_CHARS) -> str:
267
+ if not isinstance(value, str) or len(value) > max_chars or _IDENTIFIER.fullmatch(value) is None:
268
+ _refuse("VISUAL_RUN_INVALID_IDENTIFIER", f"{name} is not a bounded identifier")
269
+ return value
270
+
271
+
272
+ def _integer(value: Any, code: str, *, minimum: int = 0, maximum: int = 2**63 - 1) -> int:
273
+ if isinstance(value, bool) or not isinstance(value, int) or not minimum <= value <= maximum:
274
+ _refuse(code, "integer is outside its closed range")
275
+ return value
276
+
277
+
278
+ def _timestamp(value: Any) -> str:
279
+ if not isinstance(value, str) or _TIMESTAMP.fullmatch(value) is None:
280
+ _refuse("VISUAL_RUN_INVALID_TIMESTAMP", "observed_at must be an RFC 3339 UTC timestamp")
281
+ try:
282
+ parsed = dt.datetime.fromisoformat(value.removesuffix("Z") + "+00:00")
283
+ except ValueError:
284
+ _refuse("VISUAL_RUN_INVALID_TIMESTAMP", "observed_at is not a real UTC timestamp")
285
+ if parsed.utcoffset() != dt.timedelta(0):
286
+ _refuse("VISUAL_RUN_INVALID_TIMESTAMP", "observed_at must use UTC")
287
+ return value
288
+
289
+
290
+ def _freeze_json(value: Any) -> Any:
291
+ if value is None or isinstance(value, (str, bool, int)):
292
+ return value
293
+ if isinstance(value, list):
294
+ return tuple(_freeze_json(item) for item in value)
295
+ if isinstance(value, Mapping):
296
+ return MappingProxyType({key: _freeze_json(item) for key, item in value.items()})
297
+ _refuse("VISUAL_RUN_INVALID_JSON_VALUE", "contract data must contain JSON values only")
298
+
299
+
300
+ def _thaw_json(value: Any) -> Any:
301
+ if isinstance(value, Mapping):
302
+ return {key: _thaw_json(item) for key, item in value.items()}
303
+ if isinstance(value, tuple):
304
+ return [_thaw_json(item) for item in value]
305
+ return value
306
+
307
+
308
+ @dataclass(frozen=True)
309
+ class ArtifactReference:
310
+ schema_version: str
311
+ artifact_id: str
312
+ sha256: str
313
+ media_type: str
314
+ artifact_schema: str
315
+ byte_size: int
316
+ truncated: bool
317
+ original_byte_size: int
318
+ display: str
319
+
320
+ @classmethod
321
+ def from_mapping(cls, value: Mapping[str, Any]) -> ArtifactReference:
322
+ if not isinstance(value, Mapping):
323
+ _refuse("VISUAL_RUN_INVALID_ARTIFACT", "artifact must be an object")
324
+ _exact_fields(value, _ARTIFACT_FIELDS)
325
+ if value["schema_version"] != ARTIFACT_SCHEMA_VERSION:
326
+ _refuse("VISUAL_RUN_UNKNOWN_SCHEMA", "unknown artifact reference schema")
327
+ artifact_id = _identifier(value["artifact_id"], "artifact_id")
328
+ digest = value["sha256"]
329
+ media_type = value["media_type"]
330
+ artifact_schema = value["artifact_schema"]
331
+ byte_size = value["byte_size"]
332
+ truncated = value["truncated"]
333
+ original_byte_size = value["original_byte_size"]
334
+ display = value["display"]
335
+ if not isinstance(digest, str) or _DIGEST.fullmatch(digest) is None:
336
+ _refuse("VISUAL_RUN_INVALID_ARTIFACT", "artifact digest must be lowercase SHA-256")
337
+ if not isinstance(media_type, str) or _MEDIA_TYPE.fullmatch(media_type) is None:
338
+ _refuse("VISUAL_RUN_INVALID_ARTIFACT", "artifact media type is not canonical")
339
+ admitted_media = ARTIFACT_SCHEMAS.get(artifact_schema)
340
+ if admitted_media is None or media_type not in admitted_media:
341
+ _refuse("VISUAL_RUN_INVALID_ARTIFACT", "unknown artifact schema/media coordinate")
342
+ byte_size = _integer(
343
+ byte_size, "VISUAL_RUN_INVALID_ARTIFACT", minimum=0, maximum=MAX_ARTIFACT_BYTES
344
+ )
345
+ original_byte_size = _integer(
346
+ original_byte_size,
347
+ "VISUAL_RUN_INVALID_ARTIFACT",
348
+ minimum=0,
349
+ maximum=MAX_ARTIFACT_BYTES,
350
+ )
351
+ if not isinstance(truncated, bool) or (truncated and original_byte_size <= byte_size):
352
+ _refuse("VISUAL_RUN_INVALID_ARTIFACT", "truncation facts are inconsistent")
353
+ if not truncated and original_byte_size != byte_size:
354
+ _refuse("VISUAL_RUN_INVALID_ARTIFACT", "untruncated sizes must be identical")
355
+ if display not in DISPLAY_DISPOSITIONS:
356
+ _refuse("VISUAL_RUN_INVALID_ARTIFACT", "unknown display disposition")
357
+ return cls(
358
+ ARTIFACT_SCHEMA_VERSION,
359
+ artifact_id,
360
+ digest,
361
+ media_type,
362
+ artifact_schema,
363
+ byte_size,
364
+ truncated,
365
+ original_byte_size,
366
+ display,
367
+ )
368
+
369
+ def to_mapping(self) -> dict[str, Any]:
370
+ return {
371
+ "schema_version": self.schema_version,
372
+ "artifact_id": self.artifact_id,
373
+ "sha256": self.sha256,
374
+ "media_type": self.media_type,
375
+ "artifact_schema": self.artifact_schema,
376
+ "byte_size": self.byte_size,
377
+ "truncated": self.truncated,
378
+ "original_byte_size": self.original_byte_size,
379
+ "display": self.display,
380
+ }
381
+
382
+
383
+ @dataclass(frozen=True)
384
+ class TerminalError:
385
+ code: str
386
+ message: str
387
+ retryable: bool
388
+
389
+ @classmethod
390
+ def from_mapping(cls, value: Mapping[str, Any]) -> TerminalError:
391
+ if not isinstance(value, Mapping):
392
+ _refuse("VISUAL_RUN_INVALID_TERMINAL", "error must be an object")
393
+ _exact_fields(value, _ERROR_FIELDS)
394
+ code, message, retryable = value["code"], value["message"], value["retryable"]
395
+ if not isinstance(code, str) or _ERROR_CODE.fullmatch(code) is None:
396
+ _refuse("VISUAL_RUN_INVALID_TERMINAL", "error code is outside the closed grammar")
397
+ if not isinstance(message, str) or not message or len(message) > MAX_TEXT_CHARS:
398
+ _refuse("VISUAL_RUN_INVALID_TERMINAL", "error message is empty or oversized")
399
+ if not isinstance(retryable, bool):
400
+ _refuse("VISUAL_RUN_INVALID_TERMINAL", "retryable must be boolean")
401
+ return cls(code, message, retryable)
402
+
403
+ def to_mapping(self) -> dict[str, Any]:
404
+ return {"code": self.code, "message": self.message, "retryable": self.retryable}
405
+
406
+
407
+ def _validate_payload(spec: EventSpec, value: Any) -> Mapping[str, Any]:
408
+ if not isinstance(value, Mapping) or set(value) != set(spec.payload):
409
+ _refuse("VISUAL_RUN_INVALID_PAYLOAD", "payload fields differ from the event registry")
410
+ normalized: dict[str, Any] = {}
411
+ for key, field in spec.payload.items():
412
+ item = value[key]
413
+ if field.kind == "integer":
414
+ normalized[key] = _integer(item, "VISUAL_RUN_INVALID_PAYLOAD")
415
+ elif field.kind == "boolean":
416
+ if not isinstance(item, bool):
417
+ _refuse("VISUAL_RUN_INVALID_PAYLOAD", f"{key} must be boolean")
418
+ normalized[key] = item
419
+ elif field.kind == "digest":
420
+ if not isinstance(item, str) or _DIGEST.fullmatch(item) is None:
421
+ _refuse("VISUAL_RUN_INVALID_PAYLOAD", f"{key} must be lowercase SHA-256")
422
+ normalized[key] = item
423
+ else:
424
+ if not isinstance(item, str) or not item or len(item) > MAX_TEXT_CHARS:
425
+ _refuse("VISUAL_RUN_INVALID_PAYLOAD", f"{key} must be bounded text")
426
+ if field.choices and item not in field.choices:
427
+ _refuse("VISUAL_RUN_INVALID_PAYLOAD", f"{key} is outside the closed vocabulary")
428
+ normalized[key] = item
429
+ return MappingProxyType(normalized)
430
+
431
+
432
+ def _validate_event_mapping(value: Mapping[str, Any], *, sequence_required: bool) -> dict[str, Any]:
433
+ expected = _EVENT_FIELDS if sequence_required else _EVENT_FIELDS - {"sequence"}
434
+ _exact_fields(value, frozenset(expected))
435
+ if value["schema_version"] != EVENT_SCHEMA_VERSION:
436
+ _refuse("VISUAL_RUN_UNKNOWN_SCHEMA", "unknown visual event schema")
437
+ event_type = value["event_type"]
438
+ if not isinstance(event_type, str) or event_type not in EVENT_REGISTRY:
439
+ _refuse("VISUAL_RUN_UNKNOWN_EVENT", "event type is outside the closed registry")
440
+ spec = EVENT_REGISTRY[event_type]
441
+ if value["event_version"] != spec.version:
442
+ _refuse("VISUAL_RUN_UNKNOWN_EVENT_VERSION", "unknown event coordinate version")
443
+ actor = value["actor"]
444
+ if actor not in ACTORS:
445
+ _refuse("VISUAL_RUN_UNKNOWN_ACTOR", "actor is outside the closed vocabulary")
446
+ object_version = _integer(
447
+ value["object_version"], "VISUAL_RUN_INVALID_OBJECT_VERSION", minimum=1
448
+ )
449
+ previous = value["previous_object_version"]
450
+ if spec.object_rule == "create":
451
+ if object_version != 1 or previous is not None:
452
+ _refuse("VISUAL_RUN_INVALID_OBJECT_VERSION", "creation must start at object version 1")
453
+ else:
454
+ previous = _integer(previous, "VISUAL_RUN_INVALID_OBJECT_VERSION", minimum=1)
455
+ if object_version != previous + 1:
456
+ _refuse("VISUAL_RUN_INVALID_OBJECT_VERSION", "update must name the exact predecessor")
457
+ artifact_value = value["artifact"]
458
+ artifact = None if artifact_value is None else ArtifactReference.from_mapping(artifact_value)
459
+ if spec.artifact_policy == "required" and artifact is None:
460
+ _refuse("VISUAL_RUN_ARTIFACT_REQUIRED", "event requires an immutable artifact reference")
461
+ if spec.artifact_policy == "forbidden" and artifact is not None:
462
+ _refuse("VISUAL_RUN_ARTIFACT_FORBIDDEN", "event cannot carry an artifact reference")
463
+ if artifact is not None and artifact.artifact_schema not in spec.artifact_schemas:
464
+ _refuse("VISUAL_RUN_INVALID_ARTIFACT", "artifact schema is invalid for this event")
465
+ status = value["status"]
466
+ error_value = value["error"]
467
+ error = None if error_value is None else TerminalError.from_mapping(error_value)
468
+ if status != spec.terminal_status:
469
+ _refuse("VISUAL_RUN_INVALID_TERMINAL", "status differs from the registry")
470
+ if spec.error_policy == "required" and error is None:
471
+ _refuse("VISUAL_RUN_INVALID_TERMINAL", "event requires a terminal error")
472
+ if spec.error_policy == "forbidden" and error is not None:
473
+ _refuse("VISUAL_RUN_INVALID_TERMINAL", "event cannot carry a terminal error")
474
+ object_id = _identifier(value["object_id"], "object_id")
475
+ if event_type.startswith("cell.") and (
476
+ object_id == value["run_id"]
477
+ or object_id in RESERVED_CELL_IDS
478
+ or object_id.startswith(RESERVED_CELL_PREFIXES)
479
+ ):
480
+ _refuse("VISUAL_RUN_RESERVED_CELL_ID", "cell id is reserved for worker protocol objects")
481
+ result = {
482
+ "schema_version": EVENT_SCHEMA_VERSION,
483
+ "run_id": _identifier(value["run_id"], "run_id"),
484
+ "event_type": event_type,
485
+ "event_version": spec.version,
486
+ "actor": actor,
487
+ "phase": _identifier(value["phase"], "phase", max_chars=MAX_STAGE_CHARS),
488
+ "stage": _identifier(value["stage"], "stage", max_chars=MAX_STAGE_CHARS),
489
+ "object_id": object_id,
490
+ "object_version": object_version,
491
+ "previous_object_version": previous,
492
+ "observed_at": _timestamp(value["observed_at"]),
493
+ "idempotency_key": _identifier(value["idempotency_key"], "idempotency_key"),
494
+ "payload": _validate_payload(spec, value["payload"]),
495
+ "artifact": artifact,
496
+ "status": status,
497
+ "error": error,
498
+ }
499
+ if sequence_required:
500
+ result["sequence"] = _integer(value["sequence"], "VISUAL_RUN_INVALID_SEQUENCE", minimum=1)
501
+ return result
502
+
503
+
504
+ @dataclass(frozen=True)
505
+ class VisualRunEvent:
506
+ schema_version: str
507
+ run_id: str
508
+ sequence: int
509
+ event_type: str
510
+ event_version: int
511
+ actor: str
512
+ phase: str
513
+ stage: str
514
+ object_id: str
515
+ object_version: int
516
+ previous_object_version: int | None
517
+ observed_at: str
518
+ idempotency_key: str
519
+ payload: Mapping[str, Any]
520
+ artifact: ArtifactReference | None
521
+ status: str | None
522
+ error: TerminalError | None
523
+
524
+ @classmethod
525
+ def from_mapping(cls, value: Mapping[str, Any]) -> VisualRunEvent:
526
+ if not isinstance(value, Mapping):
527
+ _refuse("VISUAL_RUN_INVALID_JSON", "visual event must be an object")
528
+ normalized = _validate_event_mapping(value, sequence_required=True)
529
+ event = cls(**normalized)
530
+ encoded = _canonical_json(event.to_mapping())
531
+ if len(encoded) > MAX_EVENT_BYTES:
532
+ _refuse("VISUAL_RUN_EVENT_TOO_LARGE", "visual event exceeds the byte ceiling")
533
+ return event
534
+
535
+ def to_mapping(self) -> dict[str, Any]:
536
+ return {
537
+ "schema_version": self.schema_version,
538
+ "run_id": self.run_id,
539
+ "sequence": self.sequence,
540
+ "event_type": self.event_type,
541
+ "event_version": self.event_version,
542
+ "actor": self.actor,
543
+ "phase": self.phase,
544
+ "stage": self.stage,
545
+ "object_id": self.object_id,
546
+ "object_version": self.object_version,
547
+ "previous_object_version": self.previous_object_version,
548
+ "observed_at": self.observed_at,
549
+ "idempotency_key": self.idempotency_key,
550
+ "payload": _thaw_json(self.payload),
551
+ "artifact": None if self.artifact is None else self.artifact.to_mapping(),
552
+ "status": self.status,
553
+ "error": None if self.error is None else self.error.to_mapping(),
554
+ }
555
+
556
+
557
+ @dataclass(frozen=True)
558
+ class VisualRunDraft:
559
+ schema_version: str
560
+ run_id: str
561
+ event_type: str
562
+ event_version: int
563
+ actor: str
564
+ phase: str
565
+ stage: str
566
+ object_id: str
567
+ object_version: int
568
+ previous_object_version: int | None
569
+ observed_at: str
570
+ idempotency_key: str
571
+ payload: Mapping[str, Any]
572
+ artifact: ArtifactReference | None
573
+ status: str | None
574
+ error: TerminalError | None
575
+
576
+ @classmethod
577
+ def from_event(cls, event: VisualRunEvent) -> VisualRunDraft:
578
+ mapping = event.to_mapping()
579
+ mapping.pop("sequence")
580
+ return cls.from_mapping(mapping)
581
+
582
+ @classmethod
583
+ def from_mapping(cls, value: Mapping[str, Any]) -> VisualRunDraft:
584
+ normalized = _validate_event_mapping(value, sequence_required=False)
585
+ return cls(**normalized)
586
+
587
+ def to_mapping(self) -> dict[str, Any]:
588
+ mapping = {
589
+ "schema_version": self.schema_version,
590
+ "run_id": self.run_id,
591
+ "event_type": self.event_type,
592
+ "event_version": self.event_version,
593
+ "actor": self.actor,
594
+ "phase": self.phase,
595
+ "stage": self.stage,
596
+ "object_id": self.object_id,
597
+ "object_version": self.object_version,
598
+ "previous_object_version": self.previous_object_version,
599
+ "observed_at": self.observed_at,
600
+ "idempotency_key": self.idempotency_key,
601
+ "payload": _thaw_json(self.payload),
602
+ "artifact": None if self.artifact is None else self.artifact.to_mapping(),
603
+ "status": self.status,
604
+ "error": None if self.error is None else self.error.to_mapping(),
605
+ }
606
+ return mapping
607
+
608
+ def with_sequence(self, sequence: int) -> VisualRunEvent:
609
+ mapping = self.to_mapping()
610
+ mapping["sequence"] = sequence
611
+ return VisualRunEvent.from_mapping(mapping)
612
+
613
+
614
+ def _canonical_json(value: Mapping[str, Any]) -> bytes:
615
+ return (
616
+ json.dumps(
617
+ value, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(",", ":")
618
+ )
619
+ + "\n"
620
+ ).encode("utf-8")
621
+
622
+
623
+ def canonical_event_bytes(event: VisualRunEvent) -> bytes:
624
+ encoded = _canonical_json(event.to_mapping())
625
+ if len(encoded) > MAX_EVENT_BYTES:
626
+ _refuse("VISUAL_RUN_EVENT_TOO_LARGE", "visual event exceeds the byte ceiling")
627
+ return encoded
628
+
629
+
630
+ def canonical_draft_bytes(draft: VisualRunDraft) -> bytes:
631
+ encoded = _canonical_json(draft.to_mapping())
632
+ if len(encoded) > MAX_EVENT_BYTES:
633
+ _refuse("VISUAL_RUN_EVENT_TOO_LARGE", "visual event draft exceeds the byte ceiling")
634
+ return encoded
635
+
636
+
637
+ def _pairs_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
638
+ result: dict[str, Any] = {}
639
+ for key, value in pairs:
640
+ if key in result:
641
+ _refuse("VISUAL_RUN_DUPLICATE_KEY", f"duplicate JSON key: {key!r}")
642
+ result[key] = value
643
+ return result
644
+
645
+
646
+ def parse_event(raw: str | bytes | bytearray) -> VisualRunEvent:
647
+ if isinstance(raw, str):
648
+ try:
649
+ encoded = raw.encode("utf-8")
650
+ except UnicodeEncodeError:
651
+ _refuse("VISUAL_RUN_INVALID_JSON", "event is not UTF-8 encodable")
652
+ elif isinstance(raw, (bytes, bytearray)):
653
+ encoded = bytes(raw)
654
+ else:
655
+ _refuse("VISUAL_RUN_INVALID_JSON", "event input must be UTF-8 text or bytes")
656
+ if len(encoded) > MAX_EVENT_BYTES:
657
+ _refuse("VISUAL_RUN_EVENT_TOO_LARGE", "visual event exceeds the byte ceiling")
658
+ try:
659
+ text = encoded.decode("utf-8")
660
+ value = json.loads(
661
+ text,
662
+ object_pairs_hook=_pairs_object,
663
+ parse_constant=lambda token: _refuse(
664
+ "VISUAL_RUN_INVALID_JSON", f"non-finite JSON token: {token}"
665
+ ),
666
+ )
667
+ except VisualRunError:
668
+ raise
669
+ except (UnicodeDecodeError, ValueError, RecursionError):
670
+ _refuse("VISUAL_RUN_INVALID_JSON", "event is not bounded strict JSON")
671
+ if not isinstance(value, dict):
672
+ _refuse("VISUAL_RUN_INVALID_JSON", "visual event must be a JSON object")
673
+ return VisualRunEvent.from_mapping(value)