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,534 @@
1
+ """Unsealed run-progress events for the live hosted timeline (ADR 0021).
2
+
3
+ WHAT THIS IS NOT. This is not the Build event vocabulary. `events` holds that one, and it holds an
4
+ invariant this module deliberately does not: every BUILD name there must be re-derivable from
5
+ sealed bytes by `events.project_sealed_run`, which is why a time-throttled heartbeat is refused
6
+ there and why the module says so in its own docstring. Widening that vocabulary to move a progress
7
+ bar would trade a proof for a spinner. So progress got its own vocabulary instead, modelled on the
8
+ one exception that vocabulary already carries -- the post-seal notebook sidecar, three names that
9
+ narrate an unsealed convenience write and are separated so a consumer cannot mistake them for Build
10
+ evidence.
11
+
12
+ THE BOUNDARY, stated as the five things a reader has to be able to rely on:
13
+
14
+ * **Disjoint names.** `PROGRESS_EVENT_NAMES` shares not one member with `events.EVENT_NAMES` or
15
+ `events.RESERVED_EVENT_NAMES`. Every progress name that mirrors a Build name carries the
16
+ ``build_`` prefix precisely so the two can never be confused in a log line, a dashboard row or a
17
+ grep. A test asserts the disjointness rather than trusting the prefix.
18
+ * **Blind to projection.** `events.project_sealed_run` has no branch for any name here and must
19
+ never grow one. A projection of a sealed Build is the same list of records whether or not a
20
+ progress stream existed, because a progress record is not derived from sealed bytes and could
21
+ not be re-derived from them afterwards. A test projects a real Build and asserts the projection
22
+ names are exactly the sealed vocabulary.
23
+ * **Outside attestation.** No progress record reaches a candidate member, a manifest, a digest that
24
+ anything is verified against, or a receipt. The only digest a progress record produces is
25
+ `progress_payload_digest`, which is a content address for the record's OWN payload on the wire --
26
+ it attests the payload has not been rewritten in transit, and it attests nothing about the Build.
27
+ A verifier never reads this stream, and the verifier lane emits nothing here: it holds a signing
28
+ key and stays job-per-run (ADR 0021).
29
+ * **Never load-bearing.** Emission is best effort by contract. `emit` with no sink armed is a
30
+ ContextVar read and a return. A sink that raises is caught by the caller of record and the run
31
+ proceeds. A run that emitted zero progress events and a run that emitted all of them produce
32
+ byte-identical Builds.
33
+ * **Bounded, not chatty.** The vocabulary covers stage boundaries and per-source, per-artifact and
34
+ per-stage milestones. It has no per-row, per-byte, per-chunk or wall-clock heartbeat name, and
35
+ `MAX_PROGRESS_EVENTS_PER_ATTEMPT` is a hard per-attempt ceiling on top of that, so the stream
36
+ stays bounded by the shape of one run even if a caller loops.
37
+
38
+ THE WIRE. Studio's V3 producer append path (`POST /internal/v3/producer/attempts/{id}/events`,
39
+ `ProducerEventCommand`) carries `event_type` from a CLOSED six-value enum plus a `payload_digest`.
40
+ `STUDIO_EVENT_TYPE` is therefore part of this vocabulary and not an implementation detail: it is
41
+ the declared, total mapping from a progress name to the contract enum value it travels as, and it
42
+ lands inside `UNSEALED_STUDIO_EVENT_TYPES` only. The two values the hosted producer's sealed bulk
43
+ appends already own are reserved against exactly this vocabulary, so a consumer reading the wire
44
+ can still tell a proof from a progress tick with no new field.
45
+
46
+ The wire ALSO carries the payload body now, in the additive optional `payload` object Studio added
47
+ beside `payload_digest`. Studio stores it as the UNSEALED adjunct of the log record -- beside the
48
+ sealed record, never inside it, out of every sealed-bytes projection and every attestation -- and
49
+ refuses the append unless the payload's canonical-JSON SHA-256 equals `payload_digest`, so the
50
+ digest still binds exactly the bytes the worker claims. `progress_payload_and_digest` is what the
51
+ emitter calls: it returns the body and its content address from ONE canonicalization, so the two
52
+ cannot disagree at the call site. Sending the body is what makes the fact values below reach a
53
+ timeline instead of dying in the worker; a consumer that reads only `payload_digest` sees exactly
54
+ what it saw before.
55
+
56
+ A PROBE IS NOT AN ACQUISITION, which is why the pair below is its own two names rather than the
57
+ `source_acquire_*` ones a session worker first borrowed. Those two carry `index` and `total`,
58
+ because a Build acquires source i of a set the recipe fixes and a progress rail can render the
59
+ fraction. A session's probes are a conversation: how many there will be is not knowable when any of
60
+ them runs, so a worker reusing that shape had to send `index == total` and every probe read "1 of
61
+ 1", "2 of 2", "3 of 3" -- a fraction that is always whole, which is worse than no fraction because
62
+ a rail will draw it. `source_probe_started` and `source_probe_settled` carry Studio's own `ordinal`
63
+ instead: a counter, monotonic and meaningful on its own, and no denominator that would have to be
64
+ invented.
65
+
66
+ TERMINAL RECORDS, and why a failure is a COMPLETION here rather than a diagnostic. A watcher on
67
+ this stream pairs a start with an end; a start with no end is indistinguishable from a probe still
68
+ running, forever. So every terminal outcome of a probe emits `source_probe_settled`, and it travels
69
+ as `source_probe_completed` -- the same contract enum value a successful one travels as -- because
70
+ that enum value means "a probe settled" and a refused probe settled. Routing failures to
71
+ `diagnostic_emitted` instead was the tempting reading of the two names, and it is the wrong one: a
72
+ consumer would then have to know two event types to answer "did this probe finish", and one of them
73
+ is the type an attempt-level narration also uses (`attempt_failed`), so counting probe endings would
74
+ have counted attempt diagnostics too. `diagnostic_emitted` stays what it already was -- narration
75
+ ABOUT a failure -- and the outcome lives in a typed fact on the completion, where a watcher reads it
76
+ without parsing a second stream. See `PROBE_OUTCOMES` for the values and what separates them.
77
+
78
+ THE BRIDGE. `bridge_build_events` watches the sealed feed through `events.observer` and republishes
79
+ a bounded subset of Build records as progress records. It exists because the hosted producer calls
80
+ `execute_recipe` as one opaque step: without it the timeline is silent for the whole build. It is a
81
+ one-way read -- sealed to progress, never the reverse -- with a hand-written translation table
82
+ (`SEALED_BUILD_EVENT_BRIDGE`), a per-event allowlist of the fact keys that cross
83
+ (`BRIDGED_FACT_KEYS`), and a value guard (`_safe_value`) that drops any record carrying a value
84
+ outside integers, booleans and short token strings. The guard is what keeps this side placeless:
85
+ the sealed feed redacts locations in its own writer, downstream of `emit`, so a watcher sees raw
86
+ facts and must do its own refusing.
87
+ """
88
+
89
+ from __future__ import annotations
90
+
91
+ import contextlib
92
+ import re
93
+ from collections.abc import Callable, Iterator, Mapping
94
+ from contextvars import ContextVar
95
+ from typing import Any
96
+
97
+ from mostlyright.data_harness import events
98
+ from mostlyright.data_harness.canonical import canonical_json_bytes, sha256_bytes
99
+
100
+ PROGRESS_SCHEMA_VERSION = "mr-progress-events.v1"
101
+
102
+ # A hard per-attempt ceiling, enforced by the emitter rather than by hope. The vocabulary alone
103
+ # already bounds the stream by the shape of one run -- one record per source, per artifact and per
104
+ # stage -- so reaching this number means a caller is looping, and a looping caller must cost the
105
+ # run nothing rather than cost Studio an unbounded append.
106
+ MAX_PROGRESS_EVENTS_PER_ATTEMPT = 512
107
+
108
+ # The ordered producer stages a live timeline draws its skeleton from. `stage_started` and
109
+ # `stage_completed` carry an index into this tuple, so a dashboard can render the whole shape of a
110
+ # run from its first event instead of discovering stages as they arrive.
111
+ PRODUCER_STAGES: tuple[str, ...] = ("acquire", "build", "upload", "submit", "handoff")
112
+
113
+ # The six values Studio's `ProducerEventCommand.event_type` enum admits, spelled here as LITERALS
114
+ # on the `events._RECIPE_MANIFEST_VERSION` precedent: this module imports no generated client, so
115
+ # it stays loadable wherever the vocabulary is needed, and a test asserts the set equals the
116
+ # generated `ContractProducerEventCommandEventType` so the two spellings cannot drift.
117
+ STUDIO_EVENT_TYPES = frozenset(
118
+ {
119
+ "attempt_progress",
120
+ "source_probe_completed",
121
+ "artifact_upload_completed",
122
+ "candidate_build_completed",
123
+ "diagnostic_emitted",
124
+ "run_stage_reported",
125
+ }
126
+ )
127
+
128
+ # Two of those six belong to the hosted producer's SEALED bulk appends and are off limits here.
129
+ # The wire carries no sealed/unsealed flag -- one enum value plus one opaque digest -- so if a
130
+ # progress record travelled as `artifact_upload_completed`, a consumer counting artifacts would see
131
+ # nine completions for eight artifacts and have no way to tell which one was evidence. Reserving
132
+ # these two is what keeps the wire unambiguous without asking Studio for a new field.
133
+ SEALED_STUDIO_EVENT_TYPES = frozenset({"artifact_upload_completed", "candidate_build_completed"})
134
+ UNSEALED_STUDIO_EVENT_TYPES = STUDIO_EVENT_TYPES - SEALED_STUDIO_EVENT_TYPES
135
+
136
+ # The authoritative declaration of what each progress event asserts, in the shape `events`
137
+ # declares its own: a name maps to its exact fact keys, and a producer that drifts is refused
138
+ # rather than emitted. Facts are numbers and short tokens only -- see `_safe_value`.
139
+ #
140
+ # The `build_*` half is written by the bridge and never by hand; the rest is written by the hosted
141
+ # producer. Both halves are one vocabulary because both are unsealed and both travel the same wire.
142
+ PROGRESS_EVENT_FACTS: Mapping[str, tuple[str, ...]] = {
143
+ # -- producer lifecycle ----------------------------------------------------------------
144
+ "attempt_started": ("role", "stage_total"),
145
+ "stage_started": ("stage", "index", "total"),
146
+ "stage_completed": ("stage", "index", "total"),
147
+ "source_acquire_started": ("source_id", "index", "total"),
148
+ "source_acquire_completed": ("source_id", "index", "total", "bytes"),
149
+ "source_probe_started": ("source_id", "probe_kind", "ordinal"),
150
+ "source_probe_settled": (
151
+ "source_id",
152
+ "probe_kind",
153
+ "ordinal",
154
+ "outcome",
155
+ "code",
156
+ "display_code",
157
+ "retry_after_seconds",
158
+ "bytes",
159
+ ),
160
+ "artifact_upload_started": ("artifact_id", "kind", "index", "total", "bytes"),
161
+ "artifact_upload_sealed": ("artifact_id", "kind", "index", "total"),
162
+ "candidate_submitted": ("candidate_digest", "artifact_count"),
163
+ "verify_handoff": ("candidate_digest", "admission_id"),
164
+ "attempt_finished": ("outcome",),
165
+ "attempt_failed": ("code",),
166
+ # -- bridged from the sealed Build feed ------------------------------------------------
167
+ "build_sources_read_started": ("total",),
168
+ "build_source_read": ("source_id", "bytes"),
169
+ "build_sources_parse_started": ("total",),
170
+ "build_source_parsed": ("source_id", "rows", "columns"),
171
+ "build_cleaning_started": ("total",),
172
+ "build_cleaning_applied": ("step_index", "step_total", "source_id", "operation"),
173
+ "build_join_completed": ("left_rows", "right_rows", "output_rows"),
174
+ "build_rows_selected": ("rows", "columns"),
175
+ "build_checks_started": ("total",),
176
+ "build_checks_completed": ("passed", "total"),
177
+ "build_table_written": ("rows", "columns"),
178
+ "build_candidate_sealed": ("candidate_digest", "table_sha256", "row_count"),
179
+ }
180
+
181
+ PROGRESS_EVENT_NAMES = frozenset(PROGRESS_EVENT_FACTS)
182
+
183
+ # How a settled probe ended, as a CLOSED set rather than as whatever token the emitter happened to
184
+ # hold. A watcher branches on this value -- it is the whole reason `source_probe_settled` exists --
185
+ # and a value it has never seen is worse than no record, because the record still terminates the
186
+ # probe on the timeline while saying nothing about how.
187
+ #
188
+ # These four REFINE the two Studio's `ContractProbeResultCommand.outcome` enum admits; they do not
189
+ # contradict them. `refused` and `timed_out` are both `failed` on the wire, because the wire is
190
+ # Studio's enum and widening it would be a contract change. The refinement lives here because this
191
+ # record is unsealed and harness-owned, which is exactly the licence this vocabulary was given:
192
+ #
193
+ # * `succeeded` -- the probe was answered and the answer was reported.
194
+ # * `refused` -- the probe was refused rather than answered: its request was outside the probe
195
+ # vocabulary, or named a source the session may not look at or a column the source
196
+ # does not have, or the source channel would not serve it at all. Re-asking the
197
+ # same probe refuses identically; the fix is a different probe, or a channel.
198
+ # * `timed_out` -- the probe was attempted and did not finish inside a ceiling. A post-acquisition
199
+ # `SANDBOX_TIMEOUT` may yield to a smaller scan; a source-acquisition timeout
200
+ # does not, because bytes never arrived.
201
+ # * `throttled` -- an acquisition admission limit, not a source or probe refusal. The worker keeps
202
+ # its wire code, emits a display code, and carries Studio's retry window only
203
+ # when Studio supplied one; it never guesses a wait. This is unrelated to model
204
+ # tokens, billing, authentication, or source credentials.
205
+ # * `failed` -- attempted, and broke for a reason that is none of the above.
206
+ PROBE_OUTCOMES: tuple[str, ...] = ("succeeded", "refused", "throttled", "failed", "timed_out")
207
+
208
+ # The `code` fact is a failure code, and a succeeded probe has none. It carries this sentinel rather
209
+ # than being absent, because a fact set is exact here and an optional key would be the one thing
210
+ # `validate_progress_facts` exists to refuse. `OK` is spelled in the code namespace's own case so a
211
+ # reader grepping a timeline for a code sees one value per record and never an empty column.
212
+ PROBE_OUTCOME_OK_CODE = "OK"
213
+
214
+ # Fact values that are a closed enumeration rather than an open token. `_safe_value` bounds the
215
+ # SHAPE of every fact; this bounds the VALUE of the few that are a decision a consumer branches on.
216
+ # Keyed by (name, fact) so adding one is a two-line edit and forgetting to declare one is not
217
+ # silently permissive -- a fact absent from here is an ordinary token, which is the honest default.
218
+ PROGRESS_ENUM_FACT_VALUES: Mapping[tuple[str, str], tuple[str, ...]] = {
219
+ ("source_probe_settled", "outcome"): PROBE_OUTCOMES,
220
+ }
221
+
222
+ # Which contract enum value each progress name travels as. TOTAL over the vocabulary -- a name
223
+ # without an entry cannot be emitted, which is checked at import rather than at the first send, so
224
+ # a new name that nobody mapped fails the test run and never the production attempt.
225
+ STUDIO_EVENT_TYPE: Mapping[str, str] = {
226
+ "attempt_started": "run_stage_reported",
227
+ "stage_started": "run_stage_reported",
228
+ "stage_completed": "run_stage_reported",
229
+ "source_acquire_started": "attempt_progress",
230
+ "source_acquire_completed": "source_probe_completed",
231
+ "source_probe_started": "attempt_progress",
232
+ "source_probe_settled": "source_probe_completed",
233
+ "artifact_upload_started": "attempt_progress",
234
+ "artifact_upload_sealed": "attempt_progress",
235
+ "candidate_submitted": "run_stage_reported",
236
+ "verify_handoff": "run_stage_reported",
237
+ "attempt_finished": "run_stage_reported",
238
+ "attempt_failed": "diagnostic_emitted",
239
+ "build_sources_read_started": "run_stage_reported",
240
+ "build_source_read": "attempt_progress",
241
+ "build_sources_parse_started": "run_stage_reported",
242
+ "build_source_parsed": "attempt_progress",
243
+ "build_cleaning_started": "run_stage_reported",
244
+ "build_cleaning_applied": "attempt_progress",
245
+ "build_join_completed": "attempt_progress",
246
+ "build_rows_selected": "attempt_progress",
247
+ "build_checks_started": "run_stage_reported",
248
+ "build_checks_completed": "attempt_progress",
249
+ "build_table_written": "attempt_progress",
250
+ "build_candidate_sealed": "attempt_progress",
251
+ }
252
+
253
+ # Sealed Build name -> progress name. Hand written, and deliberately PARTIAL. What is left out and
254
+ # why:
255
+ #
256
+ # * `member_sealed`, `member_verified` and `check_completed` are per-item over sets whose size the
257
+ # plan fixes but does not bound tightly; a timeline gains a spinner from them and nothing else.
258
+ # * `run_started`, `rights_checked`, `evidence_matched`, `sources_profiled`,
259
+ # `members_sealed_started`, `snapshot_verify_started`, `snapshot_verified` and
260
+ # `candidate_installed` are Build evidence whose value is that they are PROVABLE. A live copy of
261
+ # a provable record is the weaker of the two and invites a reader to cite the copy.
262
+ # * `build_failed` and `run_interrupted` are terminal Build records. The hosted producer narrates
263
+ # its own failure through `attempt_failed`, from the error it is about to raise, with a code it
264
+ # controls -- rather than through a mirrored record whose `message` fact is free prose.
265
+ SEALED_BUILD_EVENT_BRIDGE: Mapping[str, str] = {
266
+ "sources_read_started": "build_sources_read_started",
267
+ "source_read": "build_source_read",
268
+ "sources_parsed_started": "build_sources_parse_started",
269
+ "source_parsed": "build_source_parsed",
270
+ "cleaning_started": "build_cleaning_started",
271
+ "cleaning_applied": "build_cleaning_applied",
272
+ "join_completed": "build_join_completed",
273
+ "rows_selected": "build_rows_selected",
274
+ "checks_started": "build_checks_started",
275
+ "checks_completed": "build_checks_completed",
276
+ "table_written": "build_table_written",
277
+ "build_sealed": "build_candidate_sealed",
278
+ }
279
+
280
+ # The exact sealed fact keys that cross the bridge, per event. Everything else is dropped, including
281
+ # `evidence_member`: a member path is a Build pointer, and a progress record that carried one would
282
+ # be inviting a reader to resolve it against a candidate that may not exist yet.
283
+ BRIDGED_FACT_KEYS: Mapping[str, tuple[str, ...]] = {
284
+ "sources_read_started": ("total",),
285
+ "source_read": ("source_id", "bytes"),
286
+ "sources_parsed_started": ("total",),
287
+ "source_parsed": ("source_id", "rows", "columns"),
288
+ "cleaning_started": ("total",),
289
+ "cleaning_applied": ("step_index", "step_total", "source_id", "operation"),
290
+ "join_completed": ("left_rows", "right_rows", "output_rows"),
291
+ "rows_selected": ("rows", "columns"),
292
+ "checks_started": ("total",),
293
+ "checks_completed": ("passed", "total"),
294
+ "table_written": ("rows", "columns"),
295
+ "build_sealed": ("candidate_digest", "table_sha256", "row_count"),
296
+ }
297
+
298
+ # A fact string is a short token: identifiers, digests, enum words and dotted names. It is NOT
299
+ # prose and NOT a path. The class excludes the space, the backslash and every quote, so a sentence,
300
+ # a Windows drive prefix and a rooted POSIX path all fail it, and the record carrying one is dropped
301
+ # whole rather than half redacted.
302
+ _TOKEN_RE = re.compile(r"\A[A-Za-z0-9_.:+-]{1,128}\Z")
303
+
304
+ _MAX_FACT_INT = 2**53 - 1
305
+
306
+
307
+ class ProgressVocabularyError(ValueError):
308
+ """A progress record does not match its declared name, fact set or value domain."""
309
+
310
+
311
+ def _safe_value(value: Any) -> bool:
312
+ """Return whether one fact value is inside the progress value domain.
313
+
314
+ ``bool`` is tested before ``int`` on purpose: in Python a bool IS an int, and a magnitude bound
315
+ applied to ``True`` reads as an accident rather than as a rule.
316
+ """
317
+
318
+ if isinstance(value, bool):
319
+ return True
320
+ if isinstance(value, int):
321
+ return -_MAX_FACT_INT <= value <= _MAX_FACT_INT
322
+ if isinstance(value, str):
323
+ return _TOKEN_RE.match(value) is not None
324
+ return False
325
+
326
+
327
+ def is_progress_fact_value(value: Any) -> bool:
328
+ """Return whether ``value`` may travel as a progress fact, for a caller building one.
329
+
330
+ The public view of `_safe_value`, and the only supported way to ask the question outside this
331
+ module. An emitter that has a value from somewhere else -- a failure code off an exception, say
332
+ -- can check it here and substitute a known-good one, rather than handing over a value that
333
+ makes the whole record get dropped at `emit`.
334
+ """
335
+
336
+ return _safe_value(value)
337
+
338
+
339
+ def validate_progress_facts(name: str, facts: Mapping[str, Any]) -> None:
340
+ """Enforce the declared name, fact set and value domain; raise naming the difference.
341
+
342
+ Raises ``ProgressVocabularyError``. Callers on the emission path never let it escape -- the
343
+ point of raising is that a drifted producer fails a test run, not a hosted attempt.
344
+ """
345
+
346
+ declared = PROGRESS_EVENT_FACTS.get(name)
347
+ if declared is None:
348
+ raise ProgressVocabularyError(f"progress name is outside the closed vocabulary: {name!r}")
349
+ if not isinstance(facts, Mapping):
350
+ raise ProgressVocabularyError(f"facts for {name!r} is not a mapping")
351
+ present = set(facts)
352
+ expected = set(declared)
353
+ if present != expected:
354
+ missing = sorted(expected - present)
355
+ extra = sorted(present - expected)
356
+ raise ProgressVocabularyError(
357
+ f"facts for {name!r} do not match the declared set: "
358
+ f"missing={missing} extra={extra} declared={list(declared)}"
359
+ )
360
+ for key, value in facts.items():
361
+ if not _safe_value(value):
362
+ raise ProgressVocabularyError(
363
+ f"fact {key!r} of {name!r} is outside the progress value domain: {type(value)}"
364
+ )
365
+ admitted = PROGRESS_ENUM_FACT_VALUES.get((name, key))
366
+ if admitted is not None and value not in admitted:
367
+ raise ProgressVocabularyError(
368
+ f"fact {key!r} of {name!r} is not one of {list(admitted)}: {value!r}"
369
+ )
370
+
371
+
372
+ def progress_payload(name: str, facts: Mapping[str, Any]) -> dict[str, Any]:
373
+ """Return the canonical payload one progress record commits to.
374
+
375
+ ``unsealed`` is a literal field rather than a comment. A payload that ever reaches a store
376
+ beside sealed evidence says what it is in its own bytes, so a reader that has only the payload
377
+ still cannot mistake it for Build evidence.
378
+ """
379
+
380
+ validate_progress_facts(name, facts)
381
+ return {
382
+ "schema_version": PROGRESS_SCHEMA_VERSION,
383
+ "event": name,
384
+ "unsealed": True,
385
+ "studio_event_type": STUDIO_EVENT_TYPE[name],
386
+ "facts": {key: facts[key] for key in PROGRESS_EVENT_FACTS[name]},
387
+ }
388
+
389
+
390
+ def progress_payload_and_digest(name: str, facts: Mapping[str, Any]) -> tuple[dict[str, Any], str]:
391
+ """Return one progress payload and its content address, from a single canonicalization.
392
+
393
+ The wire carries BOTH halves -- the body in ``payload`` and the address in ``payload_digest``
394
+ -- and Studio refuses the append when they disagree. Deriving them separately at a call site is
395
+ therefore the one mistake worth designing out, so this is the only place that pairs them, and
396
+ :func:`progress_payload_digest` is the digest-only view of it rather than a second formula.
397
+ """
398
+
399
+ payload = progress_payload(name, facts)
400
+ return payload, "sha256:" + sha256_bytes(canonical_json_bytes(payload))
401
+
402
+
403
+ def progress_payload_digest(name: str, facts: Mapping[str, Any]) -> str:
404
+ """Return the ``sha256:``-prefixed content address of one progress payload.
405
+
406
+ This is the ONLY digest this module produces and it attests one thing: the payload bytes. It is
407
+ not evidence about a Build, it is not an input to any manifest, and nothing verifies a candidate
408
+ against it.
409
+ """
410
+
411
+ return progress_payload_and_digest(name, facts)[1]
412
+
413
+
414
+ ProgressSink = Callable[[str, Mapping[str, Any]], None]
415
+
416
+ # A ContextVar for the same reason `events._EVENTS` is one, and a SEPARATE one so that arming or
417
+ # disarming either stream can never touch the other. A thread starts from an empty context, so one
418
+ # hosted attempt can never emit into another's stream.
419
+ _PROGRESS: ContextVar[ProgressSink | None] = ContextVar("mostlyright_run_progress", default=None)
420
+
421
+
422
+ def emit(name: str, facts: Mapping[str, Any]) -> None:
423
+ """Emit one progress record. Returns nothing and, with no sink armed, does nothing.
424
+
425
+ On ANY exception from the sink the sink is DISARMED for the rest of the arming context rather
426
+ than retried, exactly as `events.emit` does: a progress stream missing its tail is a loud
427
+ failure, and a progress stream that fails a Build is not a tradeoff anyone agreed to.
428
+ """
429
+
430
+ sink = _PROGRESS.get()
431
+ if sink is None:
432
+ return None
433
+ try:
434
+ sink(name, facts)
435
+ except Exception:
436
+ _PROGRESS.set(None)
437
+ return None
438
+
439
+
440
+ @contextlib.contextmanager
441
+ def progress_sink(sink: ProgressSink) -> Iterator[None]:
442
+ """Arm ``sink`` for the duration of the block and restore the previous one on exit."""
443
+
444
+ token = _PROGRESS.set(sink)
445
+ try:
446
+ yield
447
+ finally:
448
+ _PROGRESS.reset(token)
449
+
450
+
451
+ def translate_build_event(
452
+ event: str, facts: Mapping[str, Any]
453
+ ) -> tuple[str, dict[str, Any]] | None:
454
+ """Return the progress record for one sealed Build record, or ``None`` to drop it.
455
+
456
+ PURE. Four reasons to return ``None``, all of them silent by design -- a bridge that raised
457
+ would surface a sealed-feed detail as a progress-stream failure:
458
+
459
+ * the Build name is not in `SEALED_BUILD_EVENT_BRIDGE`;
460
+ * a fact the translation needs is absent (a sealed producer that drifted);
461
+ * a fact value is outside the progress value domain, which is the guard that keeps a path or a
462
+ free-prose message from crossing;
463
+ * the translated record does not validate, which cannot happen while the tables agree and is
464
+ checked anyway because the tables are hand written.
465
+ """
466
+
467
+ name = SEALED_BUILD_EVENT_BRIDGE.get(event)
468
+ if name is None:
469
+ return None
470
+ carried = BRIDGED_FACT_KEYS[event]
471
+ if any(key not in facts for key in carried):
472
+ return None
473
+ translated = {key: facts[key] for key in carried}
474
+ try:
475
+ validate_progress_facts(name, translated)
476
+ except ProgressVocabularyError:
477
+ return None
478
+ return name, translated
479
+
480
+
481
+ @contextlib.contextmanager
482
+ def bridge_build_events() -> Iterator[None]:
483
+ """Republish the bridged subset of live Build records onto the armed progress sink.
484
+
485
+ One way only. This watches `events` and writes to `_PROGRESS`; nothing here can write a record
486
+ into the Build feed, and `events.observer` calls the previously armed Build sink first, so the
487
+ feed is unaffected whether or not this is installed.
488
+ """
489
+
490
+ def _watch(
491
+ event: str, facts: Mapping[str, Any], evidence: Mapping[str, Any] | None = None
492
+ ) -> None:
493
+ translated = translate_build_event(event, facts)
494
+ if translated is not None:
495
+ emit(*translated)
496
+
497
+ with events.observer(_watch):
498
+ yield
499
+
500
+
501
+ def _check_tables() -> None:
502
+ """Refuse to import a module whose hand-written tables disagree.
503
+
504
+ Every one of these is a table an author edits by hand, and every one of them is cheap to check
505
+ once at import. The alternative -- discovering a missing `STUDIO_EVENT_TYPE` entry when a hosted
506
+ attempt tries to send it -- turns an editing mistake into a production one.
507
+ """
508
+
509
+ if set(STUDIO_EVENT_TYPE) != PROGRESS_EVENT_NAMES:
510
+ raise ProgressVocabularyError("STUDIO_EVENT_TYPE is not total over the progress vocabulary")
511
+ if not UNSEALED_STUDIO_EVENT_TYPES.issuperset(STUDIO_EVENT_TYPE.values()):
512
+ raise ProgressVocabularyError(
513
+ "a progress name maps outside the unsealed half of the Studio event-type enum"
514
+ )
515
+ for enum_name, enum_fact in PROGRESS_ENUM_FACT_VALUES:
516
+ if enum_fact not in PROGRESS_EVENT_FACTS.get(enum_name, ()):
517
+ raise ProgressVocabularyError(
518
+ f"PROGRESS_ENUM_FACT_VALUES names {enum_fact!r} of {enum_name!r}, "
519
+ "which is not a declared fact of a declared name"
520
+ )
521
+ for admitted in PROGRESS_ENUM_FACT_VALUES.values():
522
+ for value in admitted:
523
+ if not _safe_value(value):
524
+ raise ProgressVocabularyError(
525
+ f"an admitted enum value is outside the progress value domain: {value!r}"
526
+ )
527
+ if set(SEALED_BUILD_EVENT_BRIDGE) != set(BRIDGED_FACT_KEYS):
528
+ raise ProgressVocabularyError("the bridge and its fact allowlist cover different events")
529
+ for event, name in SEALED_BUILD_EVENT_BRIDGE.items():
530
+ if set(BRIDGED_FACT_KEYS[event]) != set(PROGRESS_EVENT_FACTS[name]):
531
+ raise ProgressVocabularyError(f"bridged fact keys for {event!r} are not {name!r}'s set")
532
+
533
+
534
+ _check_tables()
@@ -0,0 +1,46 @@
1
+ """The Toolbox: the closed set of certified Readers this product ships with.
2
+
3
+ A Reader is the code that turns fetched bytes into rows. ``docs/VOCABULARY.md`` states
4
+ what this package is in one sentence -- "the Toolbox, the set of certified Readers this
5
+ product ships with. A table in the software; nothing is uploaded to it" -- and that
6
+ sentence is the whole design. The table is closed: membership is exactly the family
7
+ instances written into ``registry.py``, resolution is on an exact ``(family_id,
8
+ family_version)`` key with no fallback to a nearby version, and there is no plugin
9
+ discovery, no import-by-name, and no environment override. Widening the table is a source
10
+ edit that goes through review, and two versions of one family ship side by side so that a
11
+ sealed recipe keeps resolving its own pin forever.
12
+
13
+ Import discipline
14
+ -----------------
15
+ This package may import ``formats``, ``canonical``, the standard library, and a family's
16
+ own pinned decoder library. It must not import ``acquisition``, ``sources``, ``recipe``,
17
+ or ``pipeline``. Those layers import this one -- the sandbox worker resolves a Reader
18
+ family from this table -- and a back-edge would close an import cycle whose failure is
19
+ order-dependent, which is worse than a hard error: a test run that imported ``readers``
20
+ first would fail while the application, which imports the other way round, stayed green.
21
+
22
+ The rule that keeps the discipline enforceable is that anything both ``acquisition`` and
23
+ ``readers`` need lives in ``formats``, which is a true leaf and says so in its own
24
+ docstring. Never satisfy a shared need with an import from ``acquisition``. A test walks
25
+ this package and fails on any import of a layer above it.
26
+ """
27
+
28
+ from mostlyright.data_harness.readers.contracts import (
29
+ DECODE_FLAGS,
30
+ READER_ERROR_CODES,
31
+ ReaderBudgets,
32
+ ReaderError,
33
+ ReaderFamily,
34
+ ReaderPin,
35
+ ReaderResult,
36
+ )
37
+
38
+ __all__ = [
39
+ "DECODE_FLAGS",
40
+ "READER_ERROR_CODES",
41
+ "ReaderBudgets",
42
+ "ReaderError",
43
+ "ReaderFamily",
44
+ "ReaderPin",
45
+ "ReaderResult",
46
+ ]