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,761 @@
1
+ """Crash-recoverable retention and immutable deletion-status projection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+ import stat
9
+ import threading
10
+ import time
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass
13
+ from datetime import UTC, datetime, timedelta
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from mostlyright.data_harness.acquisition.http import (
18
+ exclusive_content_addressed_name,
19
+ seal_content_addressed_snapshot,
20
+ )
21
+ from mostlyright.data_harness.acquisition.retention_admission import retention_evidence_root
22
+ from mostlyright.data_harness.canonical import (
23
+ canonical_json_bytes,
24
+ canonical_sha256,
25
+ parse_json,
26
+ )
27
+ from mostlyright.data_harness.formats import SNAPSHOT_SUFFIX_PATTERN
28
+ from mostlyright.data_harness.sources.contracts import (
29
+ RETENTION_KINDS,
30
+ DeletedReference,
31
+ DeletionTombstone,
32
+ RetentionPolicy,
33
+ SourceContractError,
34
+ )
35
+
36
+ # Compiled once at import time, as before, but built from the single closed table so a
37
+ # newly admitted format cannot become parseable while staying invisible to deletion.
38
+ _SNAPSHOT_NAME = re.compile(r"^(?P<digest>[0-9a-f]{64})\.(?:" + SNAPSHOT_SUFFIX_PATTERN + r")$")
39
+ _DELETION_STATUS_VERSION = "harness-source-deletion-status.v1"
40
+ _AUDIT_VERSION = "harness-source-deletion-audit.v1"
41
+ _STATUS_VERSION = "harness-source-availability.v1"
42
+ _KIND_DAYS = {
43
+ "raw": "raw_days",
44
+ "derived": "derived_days",
45
+ "logs": "log_days",
46
+ "review_evidence": "review_evidence_days",
47
+ }
48
+ assert set(_KIND_DAYS) == set(RETENTION_KINDS), (
49
+ "retention kinds and their policy day fields must stay in step"
50
+ )
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class RetentionDecision:
55
+ """Expiry of one evidence kind under a retention policy; derived by retention_decision."""
56
+
57
+ evidence_kind: str
58
+ retained_until: str
59
+ expired: bool
60
+ policy_digest: str
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class DeletionOutcome:
65
+ """Evidence sealed by DeletionCoordinator.delete for one removed snapshot.
66
+
67
+ ``status_envelope_path`` names the sealed ``status-<digest>.json`` deletion-status envelope.
68
+ """
69
+
70
+ status_envelope_path: Path
71
+ tombstone_path: Path
72
+ audit_path: Path
73
+ tombstone: DeletionTombstone
74
+ status: dict[str, Any]
75
+
76
+
77
+ class DeletionCoordinator:
78
+ """Remove only exact content-addressed snapshots after sealing complete references."""
79
+
80
+ def __init__(
81
+ self,
82
+ *,
83
+ snapshot_root: Path,
84
+ evidence_root: Path,
85
+ failure_injector: Callable[[str], None] | None = None,
86
+ ) -> None:
87
+ for name, root in (("snapshot_root", snapshot_root), ("evidence_root", evidence_root)):
88
+ if not root.is_absolute() or root.is_symlink() or not root.is_dir():
89
+ raise SourceContractError(
90
+ "DELETION_ROOT",
91
+ name,
92
+ "must be an existing absolute non-symlink directory",
93
+ )
94
+ expected_evidence_root = retention_evidence_root(snapshot_root)
95
+ if evidence_root != expected_evidence_root:
96
+ raise SourceContractError(
97
+ "DELETION_ROOT",
98
+ "evidence_root",
99
+ "must be the deterministic sibling authority root for snapshot_root",
100
+ )
101
+ self._snapshot_root = snapshot_root
102
+ self._evidence_root = evidence_root
103
+ self._failure_injector = failure_injector
104
+
105
+ def delete(
106
+ self,
107
+ *,
108
+ storage_name: str,
109
+ content_sha256: str,
110
+ removed_at: str,
111
+ reason_code: str,
112
+ policy: RetentionPolicy,
113
+ references: tuple[DeletedReference, ...],
114
+ ) -> DeletionOutcome:
115
+ match = _SNAPSHOT_NAME.fullmatch(storage_name)
116
+ if match is None or match.group("digest") != content_sha256:
117
+ raise SourceContractError(
118
+ "DELETION_TARGET",
119
+ "storage_name",
120
+ "target must be the exact content-addressed snapshot name",
121
+ )
122
+ ordered = _complete_reference_set(references)
123
+ reference_digest = canonical_sha256([item.to_dict() for item in ordered])
124
+ tombstone = DeletionTombstone(
125
+ tombstone_id=f"delete.{content_sha256[:24]}",
126
+ removed_content_sha256=content_sha256,
127
+ removed_at=removed_at,
128
+ reason_code=reason_code,
129
+ policy_digest=policy.digest,
130
+ complete_reference_set_digest=reference_digest,
131
+ references=ordered,
132
+ byte_removal_confirmed=True,
133
+ )
134
+ deletion_status = {
135
+ "schema_version": _DELETION_STATUS_VERSION,
136
+ "content_sha256": content_sha256,
137
+ "status": "deletion-pending",
138
+ "source_access_allowed": False,
139
+ "refetch_allowed": False,
140
+ "replay": "unavailable-by-deletion",
141
+ "removed_at": removed_at,
142
+ "reason_code": reason_code,
143
+ "policy_digest": policy.digest,
144
+ "complete_reference_set_digest": reference_digest,
145
+ "references": [item.to_dict() for item in ordered],
146
+ "planned_tombstone_digest": tombstone.digest,
147
+ }
148
+ deletion_status_bytes = canonical_json_bytes(deletion_status)
149
+ deletion_status_name = f"status-{content_sha256}.json"
150
+ # Held from before the target is opened until after its name is gone. A sealer of this
151
+ # same name is validating bytes it has open, and an unlink landing between two of its
152
+ # observations moves st_nlink from 1 to 0 under its descriptor -- the exact refusal issue
153
+ # #209 reported. Taking the sealer's own per-name lock is what stops the harness racing
154
+ # itself, in this process and in any other. The evidence envelopes sealed inside this
155
+ # block take a lock of their own, under the separate evidence root the constructor
156
+ # requires; that direction is the only nesting in the tree, so nothing waits on a cycle.
157
+ with exclusive_content_addressed_name(self._snapshot_root, storage_name):
158
+ target = self._open_validated_snapshot(
159
+ storage_name=storage_name,
160
+ content_sha256=content_sha256,
161
+ )
162
+ if target is None:
163
+ if not _keyed_envelope_matches(
164
+ self._evidence_root,
165
+ content=deletion_status_bytes,
166
+ index_name=deletion_status_name,
167
+ ):
168
+ raise SourceContractError(
169
+ "DELETION_TARGET",
170
+ "snapshot",
171
+ "snapshot is absent without exact recoverable deletion-pending evidence",
172
+ )
173
+ status_envelope_path = self._evidence_root / deletion_status_name
174
+ else:
175
+ root_fd, descriptor, identity = target
176
+ try:
177
+ self._checkpoint("before_deletion_status")
178
+ status_envelope_path = _seal_keyed_envelope(
179
+ self._evidence_root,
180
+ content=deletion_status_bytes,
181
+ index_name=deletion_status_name,
182
+ )
183
+ self._checkpoint("after_deletion_status")
184
+ self._checkpoint("before_unlink")
185
+ self._unlink_validated_snapshot(
186
+ root_fd=root_fd,
187
+ descriptor=descriptor,
188
+ storage_name=storage_name,
189
+ identity=identity,
190
+ )
191
+ self._checkpoint("after_unlink")
192
+ self._checkpoint("before_snapshot_fsync")
193
+ os.fsync(root_fd)
194
+ self._checkpoint("after_snapshot_fsync")
195
+ finally:
196
+ os.close(descriptor)
197
+ os.close(root_fd)
198
+
199
+ self._checkpoint("before_tombstone")
200
+ tombstone_path = _seal_keyed_envelope(
201
+ self._evidence_root,
202
+ content=tombstone.canonical_bytes,
203
+ index_name=f"tombstone-{content_sha256}.json",
204
+ )
205
+ self._checkpoint("after_tombstone")
206
+ audit = {
207
+ "schema_version": _AUDIT_VERSION,
208
+ "event": "source_bytes_deleted",
209
+ "removed_content_sha256": content_sha256,
210
+ "removed_at": removed_at,
211
+ "reason_code": reason_code,
212
+ "policy_digest": policy.digest,
213
+ "complete_reference_set_digest": reference_digest,
214
+ "deletion_status_digest": _path_digest(status_envelope_path),
215
+ "tombstone_digest": tombstone.digest,
216
+ }
217
+ self._checkpoint("before_audit")
218
+ audit_path = _seal_keyed_envelope(
219
+ self._evidence_root,
220
+ content=canonical_json_bytes(audit),
221
+ index_name=f"audit-{content_sha256}.json",
222
+ )
223
+ self._checkpoint("after_audit")
224
+ status = project_availability(
225
+ content_sha256=content_sha256,
226
+ tombstone=tombstone,
227
+ deletion_status=deletion_status,
228
+ )
229
+ return DeletionOutcome(
230
+ status_envelope_path=status_envelope_path,
231
+ tombstone_path=tombstone_path,
232
+ audit_path=audit_path,
233
+ tombstone=tombstone,
234
+ status=status,
235
+ )
236
+
237
+ def _checkpoint(self, name: str) -> None:
238
+ if self._failure_injector is not None:
239
+ self._failure_injector(name)
240
+
241
+ def _open_validated_snapshot(
242
+ self,
243
+ *,
244
+ storage_name: str,
245
+ content_sha256: str,
246
+ ) -> tuple[int, int, os.stat_result] | None:
247
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
248
+ root_fd = os.open(self._snapshot_root, directory_flags)
249
+ try:
250
+ try:
251
+ descriptor = os.open(
252
+ storage_name,
253
+ os.O_RDONLY
254
+ | getattr(os, "O_NOFOLLOW", 0)
255
+ | getattr(os, "O_NONBLOCK", 0)
256
+ | getattr(os, "O_CLOEXEC", 0),
257
+ dir_fd=root_fd,
258
+ )
259
+ except FileNotFoundError:
260
+ os.close(root_fd)
261
+ return None
262
+ try:
263
+ before = os.fstat(descriptor)
264
+ if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1:
265
+ raise SourceContractError(
266
+ "DELETION_TARGET",
267
+ "snapshot",
268
+ "deletion target must be one non-hardlinked regular file",
269
+ )
270
+ observed = _hash_descriptor(descriptor)
271
+ after = os.fstat(descriptor)
272
+ if (
273
+ observed != content_sha256
274
+ or before.st_dev != after.st_dev
275
+ or before.st_ino != after.st_ino
276
+ or before.st_size != after.st_size
277
+ or before.st_mtime_ns != after.st_mtime_ns
278
+ ):
279
+ raise SourceContractError(
280
+ "DELETION_TARGET_CHANGED",
281
+ "snapshot",
282
+ "snapshot identity, bytes, or metadata changed during deletion validation",
283
+ )
284
+ return root_fd, descriptor, before
285
+ except Exception:
286
+ os.close(descriptor)
287
+ raise
288
+ except Exception:
289
+ os.close(root_fd)
290
+ raise
291
+
292
+ @staticmethod
293
+ def _unlink_validated_snapshot(
294
+ *,
295
+ root_fd: int,
296
+ descriptor: int,
297
+ storage_name: str,
298
+ identity: os.stat_result,
299
+ ) -> None:
300
+ after = os.fstat(descriptor)
301
+ if (
302
+ identity.st_dev != after.st_dev
303
+ or identity.st_ino != after.st_ino
304
+ or identity.st_size != after.st_size
305
+ or identity.st_mtime_ns != after.st_mtime_ns
306
+ ):
307
+ raise SourceContractError(
308
+ "DELETION_TARGET_CHANGED",
309
+ "snapshot",
310
+ "snapshot identity or metadata changed after deletion evidence was sealed",
311
+ )
312
+ try:
313
+ current = os.stat(storage_name, dir_fd=root_fd, follow_symlinks=False)
314
+ except FileNotFoundError:
315
+ raise SourceContractError(
316
+ "DELETION_TARGET_CHANGED",
317
+ "snapshot",
318
+ "snapshot path disappeared after deletion evidence was sealed",
319
+ ) from None
320
+ if current.st_dev != identity.st_dev or current.st_ino != identity.st_ino:
321
+ raise SourceContractError(
322
+ "DELETION_TARGET_CHANGED",
323
+ "snapshot",
324
+ "snapshot path no longer names the validated file",
325
+ )
326
+ os.unlink(storage_name, dir_fd=root_fd)
327
+
328
+
329
+ def retention_decision(
330
+ *,
331
+ evidence_kind: str,
332
+ ingested_at: str,
333
+ as_of: str,
334
+ policy: RetentionPolicy,
335
+ ) -> RetentionDecision:
336
+ """Derive expiry from exact UTC instants and the immutable policy."""
337
+
338
+ if evidence_kind not in _KIND_DAYS:
339
+ raise SourceContractError(
340
+ "RETENTION_KIND",
341
+ "evidence_kind",
342
+ f"must be one of {sorted(_KIND_DAYS)}",
343
+ )
344
+ ingested = _utc(ingested_at, "ingested_at")
345
+ observed = _utc(as_of, "as_of")
346
+ days = getattr(policy, _KIND_DAYS[evidence_kind])
347
+ retained_until = ingested + timedelta(days=days)
348
+ return RetentionDecision(
349
+ evidence_kind=evidence_kind,
350
+ retained_until=_format_utc(retained_until),
351
+ expired=observed >= retained_until,
352
+ policy_digest=policy.digest,
353
+ )
354
+
355
+
356
+ def project_availability(
357
+ *,
358
+ content_sha256: str,
359
+ tombstone: DeletionTombstone | None,
360
+ deletion_status: dict[str, Any] | None = None,
361
+ ) -> dict[str, Any]:
362
+ """Join immutable source identity with separate immutable deletion evidence."""
363
+
364
+ if deletion_status is not None:
365
+ _validate_deletion_status(deletion_status, content_sha256=content_sha256)
366
+ if tombstone is None and deletion_status is None:
367
+ status = "available"
368
+ tombstone_digest = None
369
+ replay = "exact-bytes-required"
370
+ elif tombstone is None:
371
+ status = "deletion-pending"
372
+ tombstone_digest = deletion_status["planned_tombstone_digest"]
373
+ replay = "unavailable-by-deletion"
374
+ else:
375
+ if tombstone.removed_content_sha256 != content_sha256:
376
+ raise SourceContractError(
377
+ "TOMBSTONE_BINDING",
378
+ "tombstone.removed_content_sha256",
379
+ "tombstone does not bind the requested source digest",
380
+ )
381
+ if (
382
+ deletion_status is not None
383
+ and deletion_status["planned_tombstone_digest"] != tombstone.digest
384
+ ):
385
+ raise SourceContractError(
386
+ "TOMBSTONE_BINDING",
387
+ "deletion_status.planned_tombstone_digest",
388
+ "deletion status does not bind the confirmed tombstone",
389
+ )
390
+ status = "unavailable-by-deletion"
391
+ tombstone_digest = tombstone.digest
392
+ replay = "unavailable-by-deletion"
393
+ return {
394
+ "schema_version": _STATUS_VERSION,
395
+ "content_sha256": content_sha256,
396
+ "status": status,
397
+ "source_access_allowed": tombstone is None and deletion_status is None,
398
+ "refetch_allowed": False if tombstone is not None or deletion_status is not None else None,
399
+ "replay": replay,
400
+ "tombstone_digest": tombstone_digest,
401
+ }
402
+
403
+
404
+ def require_source_available(
405
+ *,
406
+ content_sha256: str,
407
+ tombstone: DeletionTombstone | None,
408
+ deletion_status: dict[str, Any] | None = None,
409
+ ) -> None:
410
+ status = project_availability(
411
+ content_sha256=content_sha256,
412
+ tombstone=tombstone,
413
+ deletion_status=deletion_status,
414
+ )
415
+ if status["status"] != "available":
416
+ raise SourceContractError(
417
+ "SOURCE_UNAVAILABLE_BY_DELETION",
418
+ "source",
419
+ "source bytes were deleted; replay, resume, and refetch must not substitute them",
420
+ )
421
+
422
+
423
+ def load_sealed_deletion_status(path: Path) -> dict[str, Any]:
424
+ """Load strict immutable deletion-pending evidence for recovery and access denial."""
425
+
426
+ value = parse_json(path.read_bytes())
427
+ if not isinstance(value, dict):
428
+ raise SourceContractError("DELETION_STATUS", "deletion_status", "must be an object")
429
+ _validate_deletion_status(value, content_sha256=value.get("content_sha256"))
430
+ return value
431
+
432
+
433
+ def load_sealed_tombstone(path: Path) -> DeletionTombstone:
434
+ """Load and validate a sealed tombstone without accepting unknown fields."""
435
+
436
+ value = parse_json(path.read_bytes())
437
+ if not isinstance(value, dict):
438
+ raise SourceContractError("TOMBSTONE", "tombstone", "must be an object")
439
+ expected = {
440
+ "schema_version",
441
+ "tombstone_id",
442
+ "removed_content_sha256",
443
+ "removed_at",
444
+ "reason_code",
445
+ "policy_digest",
446
+ "complete_reference_set_digest",
447
+ "references",
448
+ "byte_removal_confirmed",
449
+ }
450
+ if set(value) != expected or not isinstance(value["references"], list):
451
+ raise SourceContractError(
452
+ "TOMBSTONE",
453
+ "tombstone",
454
+ "contains missing or unknown fields",
455
+ )
456
+ references: list[DeletedReference] = []
457
+ for item in value["references"]:
458
+ if not isinstance(item, dict) or set(item) != {"reference_kind", "reference_digest"}:
459
+ raise SourceContractError("TOMBSTONE", "references", "reference shape is invalid")
460
+ references.append(DeletedReference(**item))
461
+ copied = dict(value)
462
+ copied["references"] = tuple(references)
463
+ return DeletionTombstone(**copied)
464
+
465
+
466
+ def _validate_deletion_status(
467
+ value: dict[str, Any],
468
+ *,
469
+ content_sha256: Any,
470
+ ) -> None:
471
+ expected = {
472
+ "schema_version",
473
+ "content_sha256",
474
+ "status",
475
+ "source_access_allowed",
476
+ "refetch_allowed",
477
+ "replay",
478
+ "removed_at",
479
+ "reason_code",
480
+ "policy_digest",
481
+ "complete_reference_set_digest",
482
+ "references",
483
+ "planned_tombstone_digest",
484
+ }
485
+ if (
486
+ set(value) != expected
487
+ or value["schema_version"] != _DELETION_STATUS_VERSION
488
+ or value["content_sha256"] != content_sha256
489
+ or value["status"] != "deletion-pending"
490
+ or value["source_access_allowed"] is not False
491
+ or value["refetch_allowed"] is not False
492
+ or value["replay"] != "unavailable-by-deletion"
493
+ or not isinstance(value["references"], list)
494
+ ):
495
+ raise SourceContractError(
496
+ "DELETION_STATUS",
497
+ "deletion_status",
498
+ "shape or immutable availability state is invalid",
499
+ )
500
+ references: list[DeletedReference] = []
501
+ for item in value["references"]:
502
+ if not isinstance(item, dict) or set(item) != {"reference_kind", "reference_digest"}:
503
+ raise SourceContractError(
504
+ "DELETION_STATUS",
505
+ "deletion_status.references",
506
+ "reference shape is invalid",
507
+ )
508
+ references.append(DeletedReference(**item))
509
+ ordered = _complete_reference_set(tuple(references))
510
+ if (
511
+ canonical_sha256([item.to_dict() for item in ordered])
512
+ != value["complete_reference_set_digest"]
513
+ ):
514
+ raise SourceContractError(
515
+ "DELETION_STATUS",
516
+ "deletion_status.complete_reference_set_digest",
517
+ "does not match the exact reference set",
518
+ )
519
+ planned = DeletionTombstone(
520
+ tombstone_id=f"delete.{value['content_sha256'][:24]}",
521
+ removed_content_sha256=value["content_sha256"],
522
+ removed_at=value["removed_at"],
523
+ reason_code=value["reason_code"],
524
+ policy_digest=value["policy_digest"],
525
+ complete_reference_set_digest=value["complete_reference_set_digest"],
526
+ references=ordered,
527
+ byte_removal_confirmed=True,
528
+ )
529
+ if planned.digest != value["planned_tombstone_digest"]:
530
+ raise SourceContractError(
531
+ "DELETION_STATUS",
532
+ "deletion_status.planned_tombstone_digest",
533
+ "does not bind the exact confirmed tombstone",
534
+ )
535
+
536
+
537
+ def _complete_reference_set(
538
+ references: tuple[DeletedReference, ...],
539
+ ) -> tuple[DeletedReference, ...]:
540
+ if not references:
541
+ raise SourceContractError(
542
+ "REFERENCE_SET_EMPTY",
543
+ "references",
544
+ "deletion requires the complete non-empty reference set",
545
+ )
546
+ ordered = tuple(
547
+ sorted(references, key=lambda item: (item.reference_kind, item.reference_digest))
548
+ )
549
+ if ordered != references:
550
+ raise SourceContractError(
551
+ "REFERENCE_SET_ORDER",
552
+ "references",
553
+ "complete reference set must be canonically ordered",
554
+ )
555
+ coordinates = {(item.reference_kind, item.reference_digest) for item in ordered}
556
+ if len(coordinates) != len(ordered):
557
+ raise SourceContractError(
558
+ "REFERENCE_SET_DUPLICATE",
559
+ "references",
560
+ "complete reference set must not contain duplicates",
561
+ )
562
+ return ordered
563
+
564
+
565
+ def _hash_descriptor(descriptor: int) -> str:
566
+ digest = hashlib.sha256()
567
+ os.lseek(descriptor, 0, os.SEEK_SET)
568
+ while True:
569
+ chunk = os.read(descriptor, 64 * 1024)
570
+ if not chunk:
571
+ break
572
+ digest.update(chunk)
573
+ return digest.hexdigest()
574
+
575
+
576
+ def _path_digest(path: Path) -> str:
577
+ """Digest one evidence file through the same bound open its validating reads use.
578
+
579
+ A bare ``Path.read_bytes`` follows links and waits forever on a FIFO, and this digest lands
580
+ in the sealed audit envelope -- so the name is opened exactly as the evidence reads open it,
581
+ and the kind is bound before a byte is hashed.
582
+ """
583
+
584
+ descriptor = os.open(
585
+ path,
586
+ os.O_RDONLY
587
+ | getattr(os, "O_NOFOLLOW", 0)
588
+ | getattr(os, "O_NONBLOCK", 0)
589
+ | getattr(os, "O_CLOEXEC", 0),
590
+ )
591
+ try:
592
+ _require_readable_evidence_kind(descriptor, path.name)
593
+ return _hash_descriptor(descriptor)
594
+ finally:
595
+ os.close(descriptor)
596
+
597
+
598
+ def _seal_keyed_envelope(root: Path, *, content: bytes, index_name: str) -> Path:
599
+ """Seal immutable bytes and atomically bind one removed-source index to them."""
600
+
601
+ seal_content_addressed_snapshot(root, content=content, suffix="json")
602
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
603
+ root_fd = os.open(root, directory_flags)
604
+ temporary_name = f".stage-index-{os.getpid()}-{threading.get_ident()}-{time.monotonic_ns()}"
605
+ temporary_fd: int | None = None
606
+ try:
607
+ temporary_fd = os.open(
608
+ temporary_name,
609
+ os.O_WRONLY
610
+ | os.O_CREAT
611
+ | os.O_EXCL
612
+ | getattr(os, "O_NOFOLLOW", 0)
613
+ | getattr(os, "O_CLOEXEC", 0),
614
+ 0o600,
615
+ dir_fd=root_fd,
616
+ )
617
+ view = memoryview(content)
618
+ written = 0
619
+ while written < len(view):
620
+ count = os.write(temporary_fd, view[written:])
621
+ if count <= 0:
622
+ raise SourceContractError(
623
+ "DELETION_EVIDENCE_WRITE",
624
+ index_name,
625
+ "removed-source evidence write made no progress",
626
+ )
627
+ written += count
628
+ os.fchmod(temporary_fd, 0o444)
629
+ os.fsync(temporary_fd)
630
+ os.close(temporary_fd)
631
+ temporary_fd = None
632
+ try:
633
+ os.link(
634
+ temporary_name,
635
+ index_name,
636
+ src_dir_fd=root_fd,
637
+ dst_dir_fd=root_fd,
638
+ follow_symlinks=False,
639
+ )
640
+ except FileExistsError:
641
+ existing_fd = os.open(
642
+ index_name,
643
+ os.O_RDONLY
644
+ | getattr(os, "O_NOFOLLOW", 0)
645
+ | getattr(os, "O_NONBLOCK", 0)
646
+ | getattr(os, "O_CLOEXEC", 0),
647
+ dir_fd=root_fd,
648
+ )
649
+ try:
650
+ _require_readable_evidence_kind(existing_fd, index_name)
651
+ existing = _read_exact_descriptor(existing_fd, len(content))
652
+ finally:
653
+ os.close(existing_fd)
654
+ if existing != content:
655
+ raise SourceContractError(
656
+ "DELETION_EVIDENCE_CONFLICT",
657
+ index_name,
658
+ "removed-source evidence index already binds different immutable bytes",
659
+ ) from None
660
+ os.unlink(temporary_name, dir_fd=root_fd)
661
+ os.fsync(root_fd)
662
+ finally:
663
+ if temporary_fd is not None:
664
+ os.close(temporary_fd)
665
+ try:
666
+ os.unlink(temporary_name, dir_fd=root_fd)
667
+ except FileNotFoundError:
668
+ pass
669
+ os.close(root_fd)
670
+ return root / index_name
671
+
672
+
673
+ def _keyed_envelope_matches(root: Path, *, content: bytes, index_name: str) -> bool:
674
+ directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
675
+ root_fd = os.open(root, directory_flags)
676
+ try:
677
+ try:
678
+ descriptor = os.open(
679
+ index_name,
680
+ os.O_RDONLY
681
+ | getattr(os, "O_NOFOLLOW", 0)
682
+ | getattr(os, "O_NONBLOCK", 0)
683
+ | getattr(os, "O_CLOEXEC", 0),
684
+ dir_fd=root_fd,
685
+ )
686
+ except FileNotFoundError:
687
+ return False
688
+ try:
689
+ _require_readable_evidence_kind(descriptor, index_name)
690
+ existing = _read_exact_descriptor(descriptor, len(content))
691
+ finally:
692
+ os.close(descriptor)
693
+ if existing != content:
694
+ raise SourceContractError(
695
+ "DELETION_EVIDENCE_CONFLICT",
696
+ index_name,
697
+ "removed-source evidence index binds different immutable bytes",
698
+ )
699
+ return True
700
+ finally:
701
+ os.close(root_fd)
702
+
703
+
704
+ def _require_readable_evidence_kind(descriptor: int, index_name: str) -> None:
705
+ """Refuse an evidence index that is not a plain file, before a byte of it is read.
706
+
707
+ These descriptors are opened ``O_NONBLOCK``, because opening a FIFO without it waits in the
708
+ kernel for a writer that never comes. The status-envelope open runs while this coordinator
709
+ holds a snapshot name's cross-process lock, where the wait would take every sealer of that
710
+ digest with it; the tombstone and audit opens run after release, where the wait would still
711
+ park this deletion forever. The flag makes the open return; it also means ``read`` can answer
712
+ ``EAGAIN`` on anything that is not a regular file, which would leave this module through an
713
+ untyped ``BlockingIOError`` instead of a refusal that names the evidence it declined.
714
+
715
+ So the kind is bound first, exactly as :meth:`DeletionCoordinator._open_validated_snapshot`
716
+ binds it before hashing. ``O_NONBLOCK`` has no effect on ``read`` for a regular file, which is
717
+ the only kind that gets past this line.
718
+ """
719
+
720
+ info = os.fstat(descriptor)
721
+ if not stat.S_ISREG(info.st_mode):
722
+ raise SourceContractError(
723
+ "DELETION_EVIDENCE_CONFLICT",
724
+ index_name,
725
+ f"removed-source evidence index is kind {stat.S_IFMT(info.st_mode):#o},"
726
+ " not a regular file",
727
+ )
728
+
729
+
730
+ def _read_exact_descriptor(descriptor: int, expected_size: int) -> bytes:
731
+ chunks: list[bytes] = []
732
+ total = 0
733
+ while total <= expected_size:
734
+ chunk = os.read(descriptor, min(64 * 1024, expected_size + 1 - total))
735
+ if not chunk:
736
+ break
737
+ chunks.append(chunk)
738
+ total += len(chunk)
739
+ if total != expected_size:
740
+ raise SourceContractError(
741
+ "DELETION_EVIDENCE_CONFLICT",
742
+ "evidence",
743
+ "existing evidence length differs from the immutable envelope",
744
+ )
745
+ return b"".join(chunks)
746
+
747
+
748
+ def _utc(value: str, path: str) -> datetime:
749
+ if not isinstance(value, str) or not value.endswith("Z"):
750
+ raise SourceContractError("TIMESTAMP", path, "must be an RFC 3339 UTC timestamp")
751
+ try:
752
+ parsed = datetime.fromisoformat(value[:-1] + "+00:00")
753
+ except ValueError:
754
+ raise SourceContractError("TIMESTAMP", path, "must be a valid UTC timestamp") from None
755
+ if parsed.tzinfo != UTC:
756
+ raise SourceContractError("TIMESTAMP", path, "must use UTC")
757
+ return parsed
758
+
759
+
760
+ def _format_utc(value: datetime) -> str:
761
+ return value.astimezone(UTC).isoformat(timespec="seconds").replace("+00:00", "Z")