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,360 @@
1
+ """Pure validation for generation-pinned hosted crawler result downloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import hmac
8
+ import re
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+ from datetime import UTC, datetime, timedelta
12
+ from typing import Any, NoReturn
13
+ from urllib.parse import parse_qsl, urlsplit
14
+ from uuid import UUID
15
+
16
+ from mostlyright.data_harness.hosted_crawler_protocol import (
17
+ CRAWLER_RESULT_MEDIA_TYPE,
18
+ MAX_CRAWLER_RESULT_BYTES,
19
+ )
20
+
21
+ RESULT_DOWNLOAD_SCHEMA = "mostlyright-public-crawl-result-download.v1"
22
+ RESULT_MEDIA_TYPE = CRAWLER_RESULT_MEDIA_TYPE
23
+ _DIGEST = re.compile(r"^[0-9a-f]{64}$")
24
+ _GENERATION = re.compile(r"^[1-9][0-9]{0,31}$")
25
+ _BUCKET = re.compile(r"^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$")
26
+ _LOCAL_SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$")
27
+ _GCS_CRC32C = re.compile(r"^[A-Za-z0-9+/]{6}==$")
28
+
29
+ # The bounded unit a streaming caller transfers and verifies in. Sixty-four kibibytes is what the
30
+ # content-addressed snapshot verifier in `acquisition/http.py` already reads with, so the two
31
+ # streamed paths on this side of the boundary have one number between them.
32
+ RESULT_CHUNK_BYTES = 65_536
33
+
34
+ # CRC-32C (Castagnoli), reflected, initialised and finalised with 0xFFFFFFFF -- the exact function
35
+ # GCS reports as `crc32c`. The table is built once at import instead of once per verification: it
36
+ # is 256 constants derived from the polynomial and nothing about it varies per result.
37
+ _CRC32C_POLYNOMIAL = 0x82F63B78
38
+
39
+
40
+ def _build_crc32c_table() -> tuple[int, ...]:
41
+ table = []
42
+ for index in range(256):
43
+ value = index
44
+ for _ in range(8):
45
+ value = (value >> 1) ^ (_CRC32C_POLYNOMIAL if value & 1 else 0)
46
+ table.append(value)
47
+ return tuple(table)
48
+
49
+
50
+ _CRC32C_TABLE = _build_crc32c_table()
51
+
52
+
53
+ class ResultDownloadError(ValueError):
54
+ """One download authority or integrity invariant failed."""
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class ValidatedResultDownload:
59
+ url: str
60
+ size: int
61
+ media_type: str
62
+ integrity: Mapping[str, str]
63
+
64
+
65
+ def _refuse(detail: str) -> NoReturn:
66
+ """Refuse, and say so in the signature.
67
+
68
+ ``NoReturn`` is what makes "an integrity pin this module does not implement can never reach
69
+ the comparison" a property a checker can see rather than a sentence in a docstring: the
70
+ fall-through branch in :meth:`ResultBytesVerifier.verify` needs no dead ``return`` to prove
71
+ it, because the refusal is known not to come back.
72
+ """
73
+
74
+ raise ResultDownloadError(detail)
75
+
76
+
77
+ def _timestamp(value: Any, label: str) -> datetime:
78
+ if not isinstance(value, str) or not value.endswith("Z"):
79
+ _refuse(f"{label} is invalid")
80
+ try:
81
+ parsed = datetime.fromisoformat(value.removesuffix("Z") + "+00:00")
82
+ except ValueError:
83
+ _refuse(f"{label} is invalid")
84
+ if parsed.tzinfo is None:
85
+ _refuse(f"{label} is invalid")
86
+ return parsed.astimezone(UTC)
87
+
88
+
89
+ def _validate_transfer_url(
90
+ signed_url: Any,
91
+ *,
92
+ base_url: str,
93
+ crawler_session_id: str,
94
+ generation: str,
95
+ storage_authority: Any,
96
+ expected_gcs_bucket: str | None,
97
+ ) -> str:
98
+ if not isinstance(signed_url, str) or not isinstance(storage_authority, Mapping):
99
+ _refuse("result transfer authority is invalid")
100
+ parsed = urlsplit(signed_url)
101
+ base = urlsplit(base_url)
102
+ try:
103
+ port = parsed.port
104
+ except ValueError:
105
+ _refuse("result transfer authority is invalid")
106
+ if parsed.username is not None or parsed.password is not None or parsed.fragment:
107
+ _refuse("result transfer authority is invalid")
108
+ if storage_authority.get("kind") == "gcs_bucket":
109
+ if set(storage_authority) != {"kind", "bucket"}:
110
+ _refuse("GCS storage authority is not exact")
111
+ bucket = storage_authority.get("bucket")
112
+ if not isinstance(bucket, str) or _BUCKET.fullmatch(bucket) is None:
113
+ _refuse("GCS storage authority is invalid")
114
+ if bucket != expected_gcs_bucket:
115
+ _refuse("GCS storage authority differs from the trusted deployment pin")
116
+ if (
117
+ parsed.scheme != "https"
118
+ or parsed.hostname != "storage.googleapis.com"
119
+ or port is not None
120
+ or "%" in parsed.path
121
+ or "\\" in parsed.path
122
+ ):
123
+ _refuse("GCS result URL authority is invalid")
124
+ parts = parsed.path.split("/")
125
+ if (
126
+ len(parts) != 5
127
+ or parts[1] != bucket
128
+ or parts[2:4] != ["crawler-quarantine", "v2"]
129
+ or _DIGEST.fullmatch(parts[4]) is None
130
+ ):
131
+ _refuse("GCS result URL path is outside its exact quarantine authority")
132
+ try:
133
+ query = parse_qsl(parsed.query, keep_blank_values=True, strict_parsing=True)
134
+ except ValueError:
135
+ _refuse("GCS result URL query is invalid")
136
+ names = {
137
+ "X-Goog-Algorithm",
138
+ "X-Goog-Credential",
139
+ "X-Goog-Date",
140
+ "X-Goog-Expires",
141
+ "X-Goog-SignedHeaders",
142
+ "X-Goog-Signature",
143
+ "generation",
144
+ }
145
+ if len(query) != len(names) or {key for key, _ in query} != names:
146
+ _refuse("GCS result URL query is not exact")
147
+ values = dict(query)
148
+ if (
149
+ any(not item for item in values.values())
150
+ or values["X-Goog-Algorithm"] != "GOOG4-RSA-SHA256"
151
+ or values["X-Goog-SignedHeaders"] != "host"
152
+ or values["generation"] != generation
153
+ or not values["X-Goog-Expires"].isdigit()
154
+ or not 1 <= int(values["X-Goog-Expires"]) <= 900
155
+ or re.fullmatch(r"[0-9]{8}T[0-9]{6}Z", values["X-Goog-Date"]) is None
156
+ or re.fullmatch(r"(?:[0-9A-Fa-f]{2}){64,512}", values["X-Goog-Signature"]) is None
157
+ or re.fullmatch(
158
+ r"[^/\x00-\x20]{1,320}/[0-9]{8}/auto/storage/goog4_request",
159
+ values["X-Goog-Credential"],
160
+ )
161
+ is None
162
+ or values["X-Goog-Credential"].split("/", 2)[1] != values["X-Goog-Date"][:8]
163
+ ):
164
+ _refuse("GCS result URL signature policy mismatches")
165
+ return signed_url
166
+ if storage_authority != {"kind": "studio_origin"}:
167
+ _refuse("result storage authority is unsupported")
168
+ if expected_gcs_bucket is not None:
169
+ _refuse("Studio-local result authority cannot replace the trusted GCS deployment pin")
170
+ if generation != "1":
171
+ _refuse("Studio-local result generation is invalid")
172
+ prefix = f"/_crawler-results/{crawler_session_id}/downloads/"
173
+ suffix = parsed.path.removeprefix(prefix)
174
+ try:
175
+ download_id = UUID(suffix)
176
+ except ValueError:
177
+ _refuse("Studio result URL has no canonical download ID")
178
+ if (
179
+ parsed.scheme != base.scheme
180
+ or parsed.hostname != base.hostname
181
+ or port != base.port
182
+ or parsed.query
183
+ or not parsed.path.startswith(prefix)
184
+ or str(download_id) != suffix
185
+ ):
186
+ _refuse("Studio result URL differs from the configured origin")
187
+ return signed_url
188
+
189
+
190
+ def validate_result_download(
191
+ download: Any,
192
+ *,
193
+ base_url: str,
194
+ crawler_session_id: str,
195
+ request_digest: str,
196
+ source_id: str,
197
+ source_authority_digest: str,
198
+ now: datetime,
199
+ expected_gcs_bucket: str | None,
200
+ ) -> ValidatedResultDownload:
201
+ fields = {
202
+ "schema_version",
203
+ "crawler_session_id",
204
+ "request_digest",
205
+ "source_id",
206
+ "source_authority_digest",
207
+ "media_type",
208
+ "method",
209
+ "signed_url",
210
+ "required_headers",
211
+ "storage_authority",
212
+ "object_generation",
213
+ "bundle_size_bytes",
214
+ "storage_integrity",
215
+ "max_result_bytes",
216
+ "issued_at",
217
+ "expires_at",
218
+ "redirect_policy",
219
+ "single_use",
220
+ }
221
+ if not isinstance(download, Mapping) or set(download) != fields:
222
+ _refuse("result download fields are not exact")
223
+ issued = _timestamp(download["issued_at"], "result download issued_at")
224
+ expires = _timestamp(download["expires_at"], "result download expires_at")
225
+ size = download["bundle_size_bytes"]
226
+ generation = download["object_generation"]
227
+ integrity = download["storage_integrity"]
228
+ if (
229
+ download["schema_version"] != RESULT_DOWNLOAD_SCHEMA
230
+ or download["crawler_session_id"] != crawler_session_id
231
+ or download["request_digest"] != request_digest
232
+ or download["source_id"] != source_id
233
+ or download["source_authority_digest"] != source_authority_digest
234
+ or download["media_type"] != RESULT_MEDIA_TYPE
235
+ or download["method"] != "GET"
236
+ or download["required_headers"] != []
237
+ or type(size) is not int
238
+ or not 1 <= size <= MAX_CRAWLER_RESULT_BYTES
239
+ or download["max_result_bytes"] != MAX_CRAWLER_RESULT_BYTES
240
+ or not isinstance(generation, str)
241
+ or _GENERATION.fullmatch(generation) is None
242
+ or download["redirect_policy"] != "deny"
243
+ or download["single_use"] is not False
244
+ or issued >= expires
245
+ or issued > now.astimezone(UTC) + timedelta(minutes=1)
246
+ or now.astimezone(UTC) >= expires
247
+ or expires - issued > timedelta(minutes=15)
248
+ or not isinstance(integrity, Mapping)
249
+ or set(integrity) != {"kind", "value"}
250
+ ):
251
+ _refuse("result download constants, identity, or expiry are invalid")
252
+ integrity_kind = integrity["kind"]
253
+ integrity_value = integrity["value"]
254
+ if not isinstance(integrity_value, str) or not (
255
+ (integrity_kind == "local_sha256" and _LOCAL_SHA256.fullmatch(integrity_value) is not None)
256
+ or (integrity_kind == "gcs_crc32c" and _GCS_CRC32C.fullmatch(integrity_value) is not None)
257
+ ):
258
+ _refuse("result storage integrity is unsupported")
259
+ url = _validate_transfer_url(
260
+ download["signed_url"],
261
+ base_url=base_url,
262
+ crawler_session_id=crawler_session_id,
263
+ generation=generation,
264
+ storage_authority=download["storage_authority"],
265
+ expected_gcs_bucket=expected_gcs_bucket,
266
+ )
267
+ return ValidatedResultDownload(url, size, RESULT_MEDIA_TYPE, dict(integrity))
268
+
269
+
270
+ class ResultBytesVerifier:
271
+ """Accumulate a result's pinned length and integrity value without holding the result.
272
+
273
+ A hosted result is up to ``MAX_CRAWLER_RESULT_BYTES``, and the caller used to have to hand
274
+ the whole thing over as one ``bytes`` before either check could run. Both checks are
275
+ accumulative, so neither ever needed that: the length is a counter and both pinned integrity
276
+ functions are streaming ones. A transferring caller feeds this verifier bounded chunks and
277
+ keeps none of them; ``verify_result_bytes`` feeds it exactly one chunk and is what it always
278
+ was. One implementation therefore decides both, so a streamed transfer cannot reach a
279
+ verdict a whole-buffer transfer would not.
280
+
281
+ The verdict order is the order the whole-buffer form used and is load-bearing: length first,
282
+ then the pinned value. An unsupported integrity pin is recognised here at construction but
283
+ refused only in :meth:`verify`, so a download that is both the wrong length and pinned by an
284
+ unsupported kind still reports its length, exactly as it did before. Nothing is accepted by
285
+ default -- a pin this module does not implement leaves both accumulators unset and
286
+ :meth:`verify` refuses rather than falling through, and ``_refuse`` is typed ``NoReturn`` so
287
+ that is checkable rather than merely stated here.
288
+ """
289
+
290
+ __slots__ = ("_crc32c", "_length", "_pinned", "_sha256", "_size")
291
+
292
+ def __init__(self, validated: ValidatedResultDownload) -> None:
293
+ kind = validated.integrity.get("kind")
294
+ value = validated.integrity.get("value")
295
+ self._size = validated.size
296
+ self._length = 0
297
+ self._pinned: str | None = None
298
+ self._sha256: Any = None
299
+ self._crc32c: int | None = None
300
+ if not isinstance(value, str):
301
+ return
302
+ if kind == "local_sha256" and value.startswith("sha256:"):
303
+ self._pinned = value
304
+ self._sha256 = hashlib.sha256()
305
+ elif kind == "gcs_crc32c":
306
+ self._pinned = value
307
+ self._crc32c = 0xFFFFFFFF
308
+
309
+ @property
310
+ def length(self) -> int:
311
+ """Bytes accumulated so far, which is what the length check will be decided on."""
312
+
313
+ return self._length
314
+
315
+ def update(self, chunk: bytes) -> None:
316
+ """Fold one transferred chunk into the length and the pinned integrity accumulator."""
317
+
318
+ self._length += len(chunk)
319
+ if self._sha256 is not None:
320
+ self._sha256.update(chunk)
321
+ elif self._crc32c is not None:
322
+ checksum = self._crc32c
323
+ table = _CRC32C_TABLE
324
+ for value in chunk:
325
+ checksum = table[(checksum ^ value) & 0xFF] ^ (checksum >> 8)
326
+ self._crc32c = checksum
327
+
328
+ def verify(self) -> None:
329
+ """Refuse unless the accumulated length and integrity value match the pinned ones."""
330
+
331
+ if self._length != self._size:
332
+ _refuse("result byte length mismatches the generation-pinned size")
333
+ if self._sha256 is not None:
334
+ observed = "sha256:" + self._sha256.hexdigest()
335
+ elif self._crc32c is not None:
336
+ finalized = ((~self._crc32c) & 0xFFFFFFFF).to_bytes(4, "big")
337
+ observed = base64.b64encode(finalized).decode("ascii")
338
+ else:
339
+ _refuse("result storage integrity is unsupported")
340
+ pinned = self._pinned or ""
341
+ if not hmac.compare_digest(pinned.encode("ascii"), observed.encode("ascii")):
342
+ _refuse("result storage integrity mismatches")
343
+
344
+
345
+ def verify_result_bytes(validated: ValidatedResultDownload, content: bytes) -> None:
346
+ """Verify a result already held whole; the streamed and whole-buffer verdicts are one."""
347
+
348
+ verifier = ResultBytesVerifier(validated)
349
+ verifier.update(content)
350
+ verifier.verify()
351
+
352
+
353
+ __all__ = [
354
+ "RESULT_CHUNK_BYTES",
355
+ "ResultBytesVerifier",
356
+ "ResultDownloadError",
357
+ "ValidatedResultDownload",
358
+ "validate_result_download",
359
+ "verify_result_bytes",
360
+ ]
@@ -0,0 +1,248 @@
1
+ """Fail-closed retention admission at the content-addressed sealing boundary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from mostlyright.data_harness.canonical import parse_json
11
+
12
+ _DELETION_STATUS_VERSION = "harness-source-deletion-status.v1"
13
+ _MAX_EVIDENCE_BYTES = 1 << 20
14
+
15
+
16
+ def _refusal(code: str, path: str, message: str) -> Exception:
17
+ # Import lazily: ``sources`` re-exports adapters that import the HTTP sealer, so importing
18
+ # its contract module while this module is imported would make that public-package cycle.
19
+ from mostlyright.data_harness.sources.contracts import SourceContractError
20
+
21
+ return SourceContractError(code, path, message)
22
+
23
+
24
+ def retention_evidence_root(snapshot_root: Path) -> Path:
25
+ """The one coordinator evidence directory a snapshot root admits."""
26
+
27
+ return snapshot_root.parent / f".{snapshot_root.name}.deletion-evidence"
28
+
29
+
30
+ def configured_retention_evidence_root(snapshot_root: Path) -> Path | None:
31
+ """Return a configured authority root; absence means deletion is not governed here."""
32
+
33
+ candidate = retention_evidence_root(snapshot_root)
34
+ try:
35
+ os.lstat(candidate)
36
+ except FileNotFoundError:
37
+ return None
38
+ except OSError:
39
+ raise _refusal(
40
+ "DELETION_EVIDENCE_ROOT",
41
+ "evidence_root",
42
+ "configured deletion evidence root could not be inspected",
43
+ ) from None
44
+ return candidate
45
+
46
+
47
+ def require_snapshot_admission(
48
+ *,
49
+ evidence_root: Path,
50
+ content_sha256: str,
51
+ ) -> None:
52
+ """Refuse to publish or reuse bytes that immutable deletion evidence withdrew.
53
+
54
+ The caller holds the snapshot name's exclusive seal lock. Deletion writes its status
55
+ envelope while holding that same lock before unlinking the snapshot, so a queued
56
+ re-acquisition cannot pass this check between removal and the tombstone write.
57
+ """
58
+
59
+ if len(content_sha256) != 64 or any(
60
+ character not in "0123456789abcdef" for character in content_sha256
61
+ ):
62
+ raise _refusal(
63
+ "DELETION_ADMISSION",
64
+ "content_sha256",
65
+ "must be a lowercase SHA-256",
66
+ )
67
+ root_fd = _open_evidence_root(evidence_root)
68
+ try:
69
+ status = _read_evidence(root_fd, f"status-{content_sha256}.json")
70
+ tombstone = _read_evidence(root_fd, f"tombstone-{content_sha256}.json")
71
+ finally:
72
+ os.close(root_fd)
73
+
74
+ if status is not None:
75
+ _require_status_binding(status, content_sha256)
76
+ raise _refusal(
77
+ "SOURCE_UNAVAILABLE_BY_DELETION",
78
+ "source",
79
+ "source bytes were deleted; replay, resume, and refetch must not substitute them",
80
+ )
81
+ if tombstone is not None:
82
+ _require_tombstone_binding(tombstone, content_sha256)
83
+ raise _refusal(
84
+ "SOURCE_UNAVAILABLE_BY_DELETION",
85
+ "source",
86
+ "source bytes were deleted; replay, resume, and refetch must not substitute them",
87
+ )
88
+
89
+
90
+ def _open_evidence_root(root: Path) -> int:
91
+ if not root.is_absolute():
92
+ raise _refusal(
93
+ "DELETION_EVIDENCE_ROOT",
94
+ "evidence_root",
95
+ "must be an existing absolute non-symlink directory",
96
+ )
97
+ try:
98
+ return os.open(
99
+ root,
100
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
101
+ )
102
+ except OSError:
103
+ raise _refusal(
104
+ "DELETION_EVIDENCE_ROOT",
105
+ "evidence_root",
106
+ "must be an existing absolute non-symlink directory",
107
+ ) from None
108
+
109
+
110
+ def _read_evidence(root_fd: int, name: str) -> dict[str, Any] | None:
111
+ try:
112
+ descriptor = os.open(
113
+ name,
114
+ os.O_RDONLY
115
+ | getattr(os, "O_NOFOLLOW", 0)
116
+ | getattr(os, "O_NONBLOCK", 0)
117
+ | getattr(os, "O_CLOEXEC", 0),
118
+ dir_fd=root_fd,
119
+ )
120
+ except FileNotFoundError:
121
+ return None
122
+ except OSError:
123
+ raise _refusal(
124
+ "DELETION_EVIDENCE_CONFLICT",
125
+ name,
126
+ "deletion evidence could not be opened safely",
127
+ ) from None
128
+ try:
129
+ info = os.fstat(descriptor)
130
+ if not stat.S_ISREG(info.st_mode) or info.st_size > _MAX_EVIDENCE_BYTES:
131
+ raise _refusal(
132
+ "DELETION_EVIDENCE_CONFLICT",
133
+ name,
134
+ "deletion evidence must be a bounded regular file",
135
+ )
136
+ content = _read_exact(descriptor, info.st_size)
137
+ finally:
138
+ os.close(descriptor)
139
+ try:
140
+ value = parse_json(content)
141
+ except Exception:
142
+ raise _refusal(
143
+ "DELETION_EVIDENCE_CONFLICT",
144
+ name,
145
+ "deletion evidence is not valid canonical JSON",
146
+ ) from None
147
+ if not isinstance(value, dict):
148
+ raise _refusal(
149
+ "DELETION_EVIDENCE_CONFLICT",
150
+ name,
151
+ "deletion evidence must be an object",
152
+ )
153
+ return value
154
+
155
+
156
+ def _read_exact(descriptor: int, size: int) -> bytes:
157
+ chunks: list[bytes] = []
158
+ remaining = size
159
+ while remaining:
160
+ chunk = os.read(descriptor, min(64 * 1024, remaining))
161
+ if not chunk:
162
+ break
163
+ chunks.append(chunk)
164
+ remaining -= len(chunk)
165
+ if remaining or os.read(descriptor, 1):
166
+ raise _refusal(
167
+ "DELETION_EVIDENCE_CONFLICT",
168
+ "evidence",
169
+ "deletion evidence changed while it was being read",
170
+ )
171
+ return b"".join(chunks)
172
+
173
+
174
+ def _require_status_binding(value: dict[str, Any], content_sha256: str) -> None:
175
+ expected = {
176
+ "schema_version",
177
+ "content_sha256",
178
+ "status",
179
+ "source_access_allowed",
180
+ "refetch_allowed",
181
+ "replay",
182
+ "removed_at",
183
+ "reason_code",
184
+ "policy_digest",
185
+ "complete_reference_set_digest",
186
+ "references",
187
+ "planned_tombstone_digest",
188
+ }
189
+ if (
190
+ set(value) != expected
191
+ or value["schema_version"] != _DELETION_STATUS_VERSION
192
+ or value["content_sha256"] != content_sha256
193
+ or value["status"] != "deletion-pending"
194
+ or value["source_access_allowed"] is not False
195
+ or value["refetch_allowed"] is not False
196
+ or value["replay"] != "unavailable-by-deletion"
197
+ or not isinstance(value["references"], list)
198
+ ):
199
+ raise _refusal(
200
+ "DELETION_EVIDENCE_CONFLICT",
201
+ f"status-{content_sha256}.json",
202
+ "deletion status does not bind an unavailable source",
203
+ )
204
+
205
+
206
+ def _require_tombstone_binding(value: dict[str, Any], content_sha256: str) -> None:
207
+ expected = {
208
+ "schema_version",
209
+ "tombstone_id",
210
+ "removed_content_sha256",
211
+ "removed_at",
212
+ "reason_code",
213
+ "policy_digest",
214
+ "complete_reference_set_digest",
215
+ "references",
216
+ "byte_removal_confirmed",
217
+ }
218
+ if set(value) != expected or value["removed_content_sha256"] != content_sha256:
219
+ raise _refusal(
220
+ "DELETION_EVIDENCE_CONFLICT",
221
+ f"tombstone-{content_sha256}.json",
222
+ "tombstone does not bind confirmed removal of this source",
223
+ )
224
+ try:
225
+ from mostlyright.data_harness.sources.contracts import (
226
+ DeletedReference,
227
+ DeletionTombstone,
228
+ SourceContractError,
229
+ )
230
+
231
+ references = tuple(DeletedReference(**item) for item in value["references"])
232
+ DeletionTombstone(
233
+ schema_version=value["schema_version"],
234
+ tombstone_id=value["tombstone_id"],
235
+ removed_content_sha256=value["removed_content_sha256"],
236
+ removed_at=value["removed_at"],
237
+ reason_code=value["reason_code"],
238
+ policy_digest=value["policy_digest"],
239
+ complete_reference_set_digest=value["complete_reference_set_digest"],
240
+ references=references,
241
+ byte_removal_confirmed=value["byte_removal_confirmed"],
242
+ )
243
+ except (TypeError, SourceContractError):
244
+ raise _refusal(
245
+ "DELETION_EVIDENCE_CONFLICT",
246
+ f"tombstone-{content_sha256}.json",
247
+ "tombstone has an invalid closed deletion contract",
248
+ ) from None