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,584 @@
1
+ """Durable local storage for visual-run events and immutable artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fcntl
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import secrets
10
+ import stat
11
+ from collections.abc import Callable, Iterator
12
+ from contextlib import contextmanager
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import BinaryIO
16
+
17
+ from mostlyright.data_harness.visual_run.contracts import (
18
+ DOCUMENT_HEADER_CELL_ID,
19
+ LEGACY_DOCUMENT_HEADER_CELL_ID_PREFIX,
20
+ ArtifactReference,
21
+ VisualRunDraft,
22
+ VisualRunError,
23
+ VisualRunEvent,
24
+ canonical_draft_bytes,
25
+ canonical_event_bytes,
26
+ parse_event,
27
+ )
28
+
29
+ MAX_LOG_BYTES = 64 * 1024 * 1024
30
+ MAX_EVENTS = 100_000
31
+ LOG_NAME = "events.jsonl"
32
+ LOCK_NAME = ".append.lock"
33
+ ARTIFACT_DIR = "artifacts"
34
+
35
+
36
+ def _command_bytes(draft: VisualRunDraft) -> bytes:
37
+ """Canonical command identity, excluding only the generated observation timestamp."""
38
+
39
+ value = draft.to_mapping()
40
+ value.pop("observed_at")
41
+ return (
42
+ json.dumps(
43
+ value, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(",", ":")
44
+ )
45
+ + "\n"
46
+ ).encode("utf-8")
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class EventPage:
51
+ events: tuple[VisualRunEvent, ...]
52
+ cursor: int
53
+ truncated: bool
54
+
55
+
56
+ class AuthorityLease:
57
+ """A held authority lock with one commit staged after final identity validation."""
58
+
59
+ def __init__(self, validate: Callable[[], None]) -> None:
60
+ self._validate = validate
61
+ self._commit: Callable[[], None] | None = None
62
+
63
+ def __call__(self) -> None:
64
+ self._validate()
65
+
66
+ def commit_after_validation(self, commit: Callable[[], None]) -> None:
67
+ if self._commit is not None:
68
+ raise RuntimeError("authority lease accepts one staged commit")
69
+ self._commit = commit
70
+
71
+ def _finish(self) -> None:
72
+ self._validate()
73
+ if self._commit is not None:
74
+ self._commit()
75
+
76
+
77
+ def _error(code: str, message: str) -> VisualRunError:
78
+ return VisualRunError(code, message)
79
+
80
+
81
+ def _regular_descriptor(descriptor: int, name: str) -> os.stat_result:
82
+ info = os.fstat(descriptor)
83
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
84
+ raise _error("VISUAL_RUN_STORE_IDENTITY", f"{name} must be one regular file")
85
+ return info
86
+
87
+
88
+ def _read_descriptor(descriptor: int, *, maximum: int, code: str, message: str) -> bytes:
89
+ os.lseek(descriptor, 0, os.SEEK_SET)
90
+ chunks: list[bytes] = []
91
+ total = 0
92
+ while True:
93
+ chunk = os.read(descriptor, min(64 * 1024, maximum + 1 - total))
94
+ if not chunk:
95
+ return b"".join(chunks)
96
+ chunks.append(chunk)
97
+ total += len(chunk)
98
+ if total > maximum:
99
+ raise _error(code, message)
100
+
101
+
102
+ @contextmanager
103
+ def _locked(directory_fd: int) -> Iterator[BinaryIO]:
104
+ descriptor = os.open(
105
+ LOCK_NAME,
106
+ os.O_RDWR | os.O_CREAT | os.O_CLOEXEC | os.O_NOFOLLOW,
107
+ 0o600,
108
+ dir_fd=directory_fd,
109
+ )
110
+ _regular_descriptor(descriptor, LOCK_NAME)
111
+ stream = os.fdopen(descriptor, "r+b", closefd=True)
112
+ try:
113
+ fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
114
+ yield stream
115
+ finally:
116
+ fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
117
+ stream.close()
118
+
119
+
120
+ @contextmanager
121
+ def visual_authority_lock(directory_fd: int, *, exclusive: bool) -> Iterator[AuthorityLease]:
122
+ """Serialize first visual authority installation with legacy viewer admission.
123
+
124
+ The already-pinned research directory inode is the lock object. Unlike a child lock pathname,
125
+ that descriptor-relative synchronization boundary cannot be substituted between validation
126
+ and commit. Visual writers take it exclusively while installing ``visual-run``; viewers take
127
+ it shared across the final absent observation and one staged legacy journal commit.
128
+ """
129
+
130
+ descriptor = os.open(
131
+ ".",
132
+ os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
133
+ dir_fd=directory_fd,
134
+ )
135
+ try:
136
+ info = os.fstat(descriptor)
137
+ pinned = os.fstat(directory_fd)
138
+ if not stat.S_ISDIR(info.st_mode) or (info.st_dev, info.st_ino) != (
139
+ pinned.st_dev,
140
+ pinned.st_ino,
141
+ ):
142
+ raise _error(
143
+ "VISUAL_RUN_STORE_IDENTITY",
144
+ "visual authority must lock the pinned research directory",
145
+ )
146
+ fcntl.flock(descriptor, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
147
+
148
+ def validate() -> None:
149
+ try:
150
+ current = os.fstat(descriptor)
151
+ pinned_now = os.fstat(directory_fd)
152
+ except OSError as exc:
153
+ raise _error(
154
+ "VISUAL_RUN_STORE_IDENTITY",
155
+ "pinned research authority became unavailable during the locked operation",
156
+ ) from exc
157
+ if (
158
+ (current.st_dev, current.st_ino) != (info.st_dev, info.st_ino)
159
+ or (pinned_now.st_dev, pinned_now.st_ino) != (info.st_dev, info.st_ino)
160
+ or not stat.S_ISDIR(current.st_mode)
161
+ ):
162
+ raise _error(
163
+ "VISUAL_RUN_STORE_IDENTITY",
164
+ "pinned research authority changed during the locked operation",
165
+ )
166
+
167
+ try:
168
+ lease = AuthorityLease(validate)
169
+ lease()
170
+ yield lease
171
+ lease._finish()
172
+ finally:
173
+ fcntl.flock(descriptor, fcntl.LOCK_UN)
174
+ finally:
175
+ os.close(descriptor)
176
+
177
+
178
+ class VisualRunStore:
179
+ """One run-bound append-only event log.
180
+
181
+ The root is supplied by the trusted viewer/coordinator. The store never accepts a path from an
182
+ event. Sequence allocation and idempotency checks happen under one kernel file lock.
183
+ """
184
+
185
+ def __init__(
186
+ self,
187
+ root: Path,
188
+ run_id: str,
189
+ *,
190
+ parent_fd: int | None = None,
191
+ root_fd: int | None = None,
192
+ ) -> None:
193
+ self.root = Path(root)
194
+ self.run_id = run_id
195
+ self._root_fd = -1
196
+ self._artifact_fd = -1
197
+ if parent_fd is not None and root_fd is not None:
198
+ raise ValueError("visual store accepts one pinned directory boundary")
199
+ if root_fd is not None:
200
+ self._root_fd = os.dup(root_fd)
201
+ elif parent_fd is not None:
202
+ if self.root.name != str(self.root) or self.root.name in {"", ".", ".."}:
203
+ raise ValueError("descriptor-relative visual store root must be one name")
204
+ with visual_authority_lock(parent_fd, exclusive=True) as validate_authority:
205
+ validate_authority()
206
+ try:
207
+ os.mkdir(self.root.name, mode=0o700, dir_fd=parent_fd)
208
+ os.fsync(parent_fd)
209
+ except FileExistsError:
210
+ pass
211
+ validate_authority()
212
+ self._root_fd = os.open(
213
+ self.root.name,
214
+ os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
215
+ dir_fd=parent_fd,
216
+ )
217
+ else:
218
+ parent = self.root.parent
219
+ parent.mkdir(mode=0o700, parents=True, exist_ok=True)
220
+ parent_fd = os.open(
221
+ parent,
222
+ os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
223
+ )
224
+ try:
225
+ with visual_authority_lock(parent_fd, exclusive=True) as validate_authority:
226
+ validate_authority()
227
+ try:
228
+ os.mkdir(self.root.name, mode=0o700, dir_fd=parent_fd)
229
+ os.fsync(parent_fd)
230
+ except FileExistsError:
231
+ pass
232
+ validate_authority()
233
+ self._root_fd = os.open(
234
+ self.root.name,
235
+ os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
236
+ dir_fd=parent_fd,
237
+ )
238
+ finally:
239
+ os.close(parent_fd)
240
+ root_info = os.fstat(self._root_fd)
241
+ path_info = self.root.stat(follow_symlinks=False)
242
+ if (root_info.st_dev, root_info.st_ino) != (path_info.st_dev, path_info.st_ino):
243
+ root_descriptor, self._root_fd = self._root_fd, -1
244
+ os.close(root_descriptor)
245
+ raise _error("VISUAL_RUN_STORE_IDENTITY", "store root changed while it was opened")
246
+ if not stat.S_ISDIR(os.fstat(self._root_fd).st_mode):
247
+ root_descriptor, self._root_fd = self._root_fd, -1
248
+ os.close(root_descriptor)
249
+ raise _error("VISUAL_RUN_STORE_IDENTITY", "store root must be a real directory")
250
+ self.artifact_root = self.root / ARTIFACT_DIR
251
+ try:
252
+ os.mkdir(ARTIFACT_DIR, mode=0o700, dir_fd=self._root_fd)
253
+ os.fsync(self._root_fd)
254
+ except FileExistsError:
255
+ pass
256
+ try:
257
+ self._artifact_fd = os.open(
258
+ ARTIFACT_DIR,
259
+ os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW,
260
+ dir_fd=self._root_fd,
261
+ )
262
+ except OSError as exc:
263
+ root_descriptor, self._root_fd = self._root_fd, -1
264
+ os.close(root_descriptor)
265
+ raise _error(
266
+ "VISUAL_RUN_STORE_IDENTITY", "artifact root must be a real directory"
267
+ ) from exc
268
+ if not stat.S_ISDIR(os.fstat(self._artifact_fd).st_mode):
269
+ artifact_descriptor, self._artifact_fd = self._artifact_fd, -1
270
+ root_descriptor, self._root_fd = self._root_fd, -1
271
+ os.close(artifact_descriptor)
272
+ os.close(root_descriptor)
273
+ raise _error("VISUAL_RUN_STORE_IDENTITY", "artifact root must be a real directory")
274
+ self.log_path = self.root / LOG_NAME
275
+ self.lock_path = self.root / LOCK_NAME
276
+
277
+ def close(self) -> None:
278
+ """Release the pinned directory descriptors; repeated calls are harmless."""
279
+
280
+ artifact_fd = getattr(self, "_artifact_fd", -1)
281
+ root_fd = getattr(self, "_root_fd", -1)
282
+ self._artifact_fd = -1
283
+ self._root_fd = -1
284
+ if artifact_fd >= 0:
285
+ os.close(artifact_fd)
286
+ if root_fd >= 0:
287
+ os.close(root_fd)
288
+
289
+ def __del__(self) -> None:
290
+ try:
291
+ self.close()
292
+ except OSError:
293
+ pass
294
+
295
+ def _open_log(self, flags: int, *, create: bool = False) -> tuple[int, bool]:
296
+ common = flags | os.O_CLOEXEC | os.O_NOFOLLOW
297
+ if not create:
298
+ descriptor = os.open(LOG_NAME, common, dir_fd=self._root_fd)
299
+ try:
300
+ _regular_descriptor(descriptor, LOG_NAME)
301
+ except BaseException:
302
+ os.close(descriptor)
303
+ raise
304
+ return descriptor, False
305
+ try:
306
+ descriptor = os.open(
307
+ LOG_NAME,
308
+ common | os.O_CREAT | os.O_EXCL,
309
+ 0o600,
310
+ dir_fd=self._root_fd,
311
+ )
312
+ try:
313
+ _regular_descriptor(descriptor, LOG_NAME)
314
+ except BaseException:
315
+ os.close(descriptor)
316
+ raise
317
+ return descriptor, True
318
+ except FileExistsError:
319
+ descriptor = os.open(LOG_NAME, common, dir_fd=self._root_fd)
320
+ try:
321
+ _regular_descriptor(descriptor, LOG_NAME)
322
+ except BaseException:
323
+ os.close(descriptor)
324
+ raise
325
+ return descriptor, False
326
+
327
+ def _parse_events(self, raw: bytes) -> tuple[VisualRunEvent, ...]:
328
+ if raw and not raw.endswith(b"\n"):
329
+ raise _error("VISUAL_RUN_LOG_TRUNCATED", "visual run log has a partial final record")
330
+ lines = raw.splitlines()
331
+ if len(lines) > MAX_EVENTS:
332
+ raise _error("VISUAL_RUN_LOG_LIMIT", "visual run log exceeds its event ceiling")
333
+ events: list[VisualRunEvent] = []
334
+ seen_keys: dict[str, bytes] = {}
335
+ for index, line in enumerate(lines, 1):
336
+ event = parse_event(line)
337
+ if event.run_id != self.run_id:
338
+ raise _error("VISUAL_RUN_RUN_MISMATCH", "event belongs to another run")
339
+ if event.sequence != index:
340
+ raise _error("VISUAL_RUN_SEQUENCE_GAP", "event sequence is not contiguous")
341
+ encoded = canonical_draft_bytes(VisualRunDraft.from_event(event))
342
+ previous = seen_keys.setdefault(event.idempotency_key, encoded)
343
+ if previous != encoded:
344
+ raise _error("VISUAL_RUN_IDEMPOTENCY_CONFLICT", "idempotency key was reused")
345
+ events.append(event)
346
+ return tuple(events)
347
+
348
+ def _events_from_descriptor(self, descriptor: int) -> tuple[VisualRunEvent, ...]:
349
+ _regular_descriptor(descriptor, LOG_NAME)
350
+ raw = _read_descriptor(
351
+ descriptor,
352
+ maximum=MAX_LOG_BYTES,
353
+ code="VISUAL_RUN_LOG_LIMIT",
354
+ message="visual run log exceeds its byte ceiling",
355
+ )
356
+ return self._parse_events(raw)
357
+
358
+ def _read_events(self) -> tuple[VisualRunEvent, ...]:
359
+ try:
360
+ descriptor, _ = self._open_log(os.O_RDONLY)
361
+ except FileNotFoundError:
362
+ return ()
363
+ try:
364
+ return self._events_from_descriptor(descriptor)
365
+ finally:
366
+ os.close(descriptor)
367
+
368
+ def events(self) -> tuple[VisualRunEvent, ...]:
369
+ with _locked(self._root_fd):
370
+ return self._read_events()
371
+
372
+ def event_for_key(self, key: str) -> VisualRunEvent | None:
373
+ """Return the durable result for an idempotent command key, when present."""
374
+
375
+ with _locked(self._root_fd):
376
+ return next(
377
+ (event for event in self._read_events() if event.idempotency_key == key), None
378
+ )
379
+
380
+ def append(self, draft: VisualRunDraft) -> VisualRunEvent:
381
+ if draft.run_id != self.run_id:
382
+ raise _error("VISUAL_RUN_RUN_MISMATCH", "draft belongs to another run")
383
+ # Validate the complete draft byte bound before comparing its command identity.
384
+ canonical_draft_bytes(draft)
385
+ command_bytes = _command_bytes(draft)
386
+ with _locked(self._root_fd):
387
+ descriptor, created = self._open_log(os.O_RDWR | os.O_APPEND, create=True)
388
+ try:
389
+ existing = self._events_from_descriptor(descriptor)
390
+ for event in existing:
391
+ if event.idempotency_key != draft.idempotency_key:
392
+ continue
393
+ if _command_bytes(VisualRunDraft.from_event(event)) == command_bytes:
394
+ return event
395
+ raise _error("VISUAL_RUN_IDEMPOTENCY_CONFLICT", "idempotency key was reused")
396
+ projection_reserved = draft.object_id == DOCUMENT_HEADER_CELL_ID or (
397
+ draft.object_id == LEGACY_DOCUMENT_HEADER_CELL_ID_PREFIX
398
+ or draft.object_id.startswith(f"{LEGACY_DOCUMENT_HEADER_CELL_ID_PREFIX}-")
399
+ )
400
+ if draft.event_type == "cell.created" and projection_reserved:
401
+ raise _error(
402
+ "VISUAL_RUN_RESERVED_CELL_ID",
403
+ "cell id is reserved for projected document structure",
404
+ )
405
+ if len(existing) >= MAX_EVENTS:
406
+ raise _error("VISUAL_RUN_LOG_LIMIT", "visual run log exceeds its event ceiling")
407
+ event = draft.with_sequence(len(existing) + 1)
408
+ # Syntax-valid is not enough: invalid transitions never enter the durable log.
409
+ from mostlyright.data_harness.visual_run.materialize import notebook_bytes
410
+ from mostlyright.data_harness.visual_run.reducer import apply_event, reduce_events
411
+
412
+ before = reduce_events(existing)
413
+ prospective = apply_event(before, event)
414
+ prospective_cell = prospective.cells.get(event.object_id)
415
+ if (
416
+ event.actor == "agent"
417
+ and event.event_type
418
+ in {"cell.source_extended", "cell.source_revised", "cell.completed"}
419
+ and prospective_cell is not None
420
+ and prospective_cell.cell_type == "markdown"
421
+ ):
422
+ from mostlyright.data_harness.visual_run.authoring import (
423
+ validate_agent_authoring_event,
424
+ )
425
+
426
+ validate_agent_authoring_event(before, event, prospective, self.read_artifact)
427
+ try:
428
+ notebook_bytes(prospective, self.read_artifact)
429
+ except ValueError as exc:
430
+ raise _error(
431
+ "VISUAL_RUN_PROJECTION_LIMIT",
432
+ "event would exceed the materialized research notebook limit",
433
+ ) from exc
434
+ encoded = canonical_event_bytes(event)
435
+ current_size = os.fstat(descriptor).st_size
436
+ if current_size + len(encoded) > MAX_LOG_BYTES:
437
+ raise _error("VISUAL_RUN_LOG_LIMIT", "visual run log exceeds its byte ceiling")
438
+ written = 0
439
+ while written < len(encoded):
440
+ progress = os.write(descriptor, encoded[written:])
441
+ if progress <= 0:
442
+ raise _error("VISUAL_RUN_LOG_WRITE", "event log write made no progress")
443
+ written += progress
444
+ os.fsync(descriptor)
445
+ if created:
446
+ os.fsync(self._root_fd)
447
+ return event
448
+ finally:
449
+ os.close(descriptor)
450
+
451
+ def page_after(self, cursor: int, *, limit: int = 256) -> EventPage:
452
+ if isinstance(cursor, bool) or not isinstance(cursor, int) or cursor < 0:
453
+ raise _error("VISUAL_RUN_CURSOR_INVALID", "cursor must be a non-negative integer")
454
+ if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 1024:
455
+ raise _error("VISUAL_RUN_CURSOR_INVALID", "limit is outside the closed range")
456
+ events = self.events()
457
+ if cursor > len(events):
458
+ raise _error("VISUAL_RUN_CURSOR_FUTURE", "cursor is beyond the durable log")
459
+ selected = events[cursor : cursor + limit]
460
+ next_cursor = cursor + len(selected)
461
+ return EventPage(selected, next_cursor, next_cursor < len(events))
462
+
463
+ def install_artifact(self, reference: ArtifactReference, content: bytes) -> Path:
464
+ if not isinstance(content, bytes):
465
+ raise _error("VISUAL_RUN_ARTIFACT_INVALID", "artifact content must be bytes")
466
+ if len(content) != reference.byte_size:
467
+ raise _error("VISUAL_RUN_ARTIFACT_SIZE", "artifact byte size differs from reference")
468
+ if hashlib.sha256(content).hexdigest() != reference.sha256:
469
+ raise _error("VISUAL_RUN_ARTIFACT_DIGEST", "artifact digest differs from reference")
470
+ target = self.artifact_root / reference.sha256
471
+ with _locked(self._root_fd):
472
+ try:
473
+ existing_fd = os.open(
474
+ reference.sha256,
475
+ os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW,
476
+ dir_fd=self._artifact_fd,
477
+ )
478
+ except FileNotFoundError:
479
+ existing_fd = -1
480
+ if existing_fd >= 0:
481
+ try:
482
+ _regular_descriptor(existing_fd, reference.sha256)
483
+ existing = _read_descriptor(
484
+ existing_fd,
485
+ maximum=reference.byte_size,
486
+ code="VISUAL_RUN_ARTIFACT_SIZE",
487
+ message="installed artifact size differs",
488
+ )
489
+ finally:
490
+ os.close(existing_fd)
491
+ if existing != content:
492
+ raise _error("VISUAL_RUN_ARTIFACT_CONFLICT", "artifact digest path conflicts")
493
+ return target
494
+ temporary = f".{reference.sha256}.{secrets.token_hex(12)}.tmp"
495
+ descriptor = os.open(
496
+ temporary,
497
+ os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | os.O_NOFOLLOW,
498
+ 0o400,
499
+ dir_fd=self._artifact_fd,
500
+ )
501
+ try:
502
+ try:
503
+ written = 0
504
+ while written < len(content):
505
+ progress = os.write(descriptor, content[written:])
506
+ if progress <= 0:
507
+ raise _error(
508
+ "VISUAL_RUN_ARTIFACT_WRITE", "artifact write made no progress"
509
+ )
510
+ written += progress
511
+ os.fsync(descriptor)
512
+ installed = _read_descriptor(
513
+ descriptor,
514
+ maximum=reference.byte_size,
515
+ code="VISUAL_RUN_ARTIFACT_SIZE",
516
+ message="staged artifact size differs",
517
+ )
518
+ if installed != content:
519
+ raise _error("VISUAL_RUN_ARTIFACT_DIGEST", "staged artifact differs")
520
+ finally:
521
+ os.close(descriptor)
522
+ except BaseException:
523
+ try:
524
+ os.unlink(temporary, dir_fd=self._artifact_fd)
525
+ except FileNotFoundError:
526
+ pass
527
+ raise
528
+ try:
529
+ os.replace(
530
+ temporary,
531
+ reference.sha256,
532
+ src_dir_fd=self._artifact_fd,
533
+ dst_dir_fd=self._artifact_fd,
534
+ )
535
+ os.fsync(self._artifact_fd)
536
+ except BaseException:
537
+ try:
538
+ os.unlink(temporary, dir_fd=self._artifact_fd)
539
+ except FileNotFoundError:
540
+ pass
541
+ raise
542
+ return target
543
+
544
+ def read_artifact(self, reference: ArtifactReference) -> bytes:
545
+ try:
546
+ descriptor = os.open(
547
+ reference.sha256,
548
+ os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW,
549
+ dir_fd=self._artifact_fd,
550
+ )
551
+ except FileNotFoundError as exc:
552
+ raise _error("VISUAL_RUN_ARTIFACT_MISSING", "artifact is not installed") from exc
553
+ try:
554
+ _regular_descriptor(descriptor, reference.sha256)
555
+ content = _read_descriptor(
556
+ descriptor,
557
+ maximum=reference.byte_size,
558
+ code="VISUAL_RUN_ARTIFACT_SIZE",
559
+ message="installed artifact size differs",
560
+ )
561
+ finally:
562
+ os.close(descriptor)
563
+ if len(content) != reference.byte_size:
564
+ raise _error("VISUAL_RUN_ARTIFACT_SIZE", "installed artifact size differs")
565
+ if hashlib.sha256(content).hexdigest() != reference.sha256:
566
+ raise _error("VISUAL_RUN_ARTIFACT_DIGEST", "installed artifact digest differs")
567
+ return content
568
+
569
+
570
+ def snapshot_json(page: EventPage) -> bytes:
571
+ return (
572
+ json.dumps(
573
+ {
574
+ "cursor": page.cursor,
575
+ "events": [event.to_mapping() for event in page.events],
576
+ "truncated": page.truncated,
577
+ },
578
+ ensure_ascii=False,
579
+ allow_nan=False,
580
+ sort_keys=True,
581
+ separators=(",", ":"),
582
+ )
583
+ + "\n"
584
+ ).encode("utf-8")