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,1105 @@
1
+ """Descriptor-rooted private immutable staging for resumable catalog fills."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import fcntl
7
+ import os
8
+ import secrets
9
+ import stat
10
+ import tempfile
11
+ from collections.abc import Iterator
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from mostlyright.data_harness.canonical import (
16
+ CanonicalJSONError,
17
+ canonical_json_bytes,
18
+ canonical_sha256,
19
+ parse_canonical_json,
20
+ sha256_bytes,
21
+ )
22
+ from mostlyright.data_harness.sources.contracts import SourceContractError
23
+
24
+ STATE_SCHEMA = "harness-catalog-fill-state.v2"
25
+ SHARD_SCHEMA = "harness-catalog-fill-shard.v1"
26
+ MAX_STATE_BYTES = 64 * 1024
27
+ MAX_SHARD_BYTES = 64 * 1024 * 1024
28
+ MAX_INDEX_SEGMENT_BYTES = 1024 * 1024
29
+ MAX_INDEX_SEGMENT_ITEMS = 4_096
30
+ MAX_BLOB_BYTES = 16 * 1024 * 1024
31
+ MAX_HISTORY_DIGESTS = 1_000_000
32
+ MAX_INDEX_ITEMS = 20_000_000
33
+ _OPEN_DIRECTORY = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
34
+ _OPEN_MEMBER = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0)
35
+ _SHARD_KINDS = frozenset(
36
+ {
37
+ "classification",
38
+ "history",
39
+ "index",
40
+ "loop_set",
41
+ "normalized",
42
+ "page",
43
+ "partition_run",
44
+ "pass_map",
45
+ "response",
46
+ }
47
+ )
48
+ _BLOB_KINDS = frozenset({"raw_response"})
49
+
50
+
51
+ class CatalogFillRefused(SourceContractError):
52
+ """A stable local fill/configuration/staging refusal."""
53
+
54
+
55
+ def shard_byte_limit(kind: str) -> int:
56
+ """Return the live immutable-shard byte ceiling for preflight composition."""
57
+
58
+ return MAX_INDEX_SEGMENT_BYTES if kind == "index" else MAX_SHARD_BYTES
59
+
60
+
61
+ class FillStaging:
62
+ """One private staging root retained by descriptor for the whole locked operation."""
63
+
64
+ def __init__(self, root: Path, *, root_descriptor: int | None = None) -> None:
65
+ self.root = Path(root)
66
+ self._retained_root_descriptor = root_descriptor
67
+ self.state_path = self.root / "state.json"
68
+ self.lock_path = self.root / ".lock"
69
+ self.shard_root = self.root / "shards"
70
+ self._root_fd = -1
71
+ self._shards_fd = -1
72
+ self._kind_fds: dict[str, int] = {}
73
+ self._chain: list[tuple[int, str, int, tuple[int, int, int]]] = []
74
+ self._owned_fds: list[int] = []
75
+ self._active_histories: dict[str, list[str]] = {}
76
+
77
+ @contextlib.contextmanager
78
+ def locked(self) -> Iterator[None]:
79
+ if self._root_fd >= 0:
80
+ raise CatalogFillRefused(
81
+ "FILL_STAGING_LOCK", "fill.staging", "staging lock is not reentrant"
82
+ )
83
+ lock_fd = -1
84
+ try:
85
+ self._open_retained_tree()
86
+ flags = os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0)
87
+ lock_fd = os.open(".lock", flags, 0o600, dir_fd=self._root_fd)
88
+ info = os.fstat(lock_fd)
89
+ if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
90
+ raise CatalogFillRefused(
91
+ "FILL_STAGING_PATH",
92
+ "fill.staging.lock",
93
+ "lock must be a single-link regular file",
94
+ )
95
+ fcntl.flock(lock_fd, fcntl.LOCK_EX)
96
+ self._verify_tree()
97
+ yield
98
+ self._verify_tree()
99
+ finally:
100
+ if lock_fd >= 0:
101
+ os.close(lock_fd)
102
+ for descriptor in reversed(self._owned_fds):
103
+ os.close(descriptor)
104
+ self._owned_fds = []
105
+ self._chain = []
106
+ self._kind_fds = {}
107
+ self._shards_fd = -1
108
+ self._root_fd = -1
109
+ self._active_histories = {}
110
+
111
+ def load(self) -> dict[str, Any] | None:
112
+ self._require_locked()
113
+ return self._load_state(validate_shards=True)
114
+
115
+ def write_shard(self, kind: str, payload: dict[str, Any]) -> str:
116
+ self._require_locked()
117
+ if kind not in _SHARD_KINDS:
118
+ raise CatalogFillRefused("FILL_STAGING_KIND", "fill.staging.kind", "unknown shard")
119
+ document = {"schema_version": SHARD_SCHEMA, "kind": kind, "payload": payload}
120
+ raw = canonical_json_bytes(document)
121
+ maximum = shard_byte_limit(kind)
122
+ if len(raw) > maximum:
123
+ raise CatalogFillRefused(
124
+ "FILL_SHARD_LIMIT", "fill.staging.shard", "shard exceeds the byte limit"
125
+ )
126
+ digest = sha256_bytes(raw)
127
+ name = f"{digest}.json"
128
+ directory_fd = self._kind_fds[kind]
129
+ self._publish_immutable(
130
+ directory_fd,
131
+ name,
132
+ raw,
133
+ maximum=len(raw),
134
+ field="fill.staging.shard",
135
+ )
136
+ self._verify_tree()
137
+ return digest
138
+
139
+ def read_shard(self, kind: str, digest: str) -> dict[str, Any]:
140
+ self._require_locked()
141
+ if kind not in _SHARD_KINDS or not _is_digest(digest):
142
+ raise CatalogFillRefused(
143
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.shard", "shard coordinate is invalid"
144
+ )
145
+ try:
146
+ raw = self._read_regular_at(
147
+ self._kind_fds[kind],
148
+ f"{digest}.json",
149
+ maximum=MAX_INDEX_SEGMENT_BYTES if kind == "index" else MAX_SHARD_BYTES,
150
+ )
151
+ except FileNotFoundError as error:
152
+ raise CatalogFillRefused(
153
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.shard", "referenced shard is absent"
154
+ ) from error
155
+ if sha256_bytes(raw) != digest:
156
+ raise CatalogFillRefused(
157
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.shard", "shard digest mismatch"
158
+ )
159
+ try:
160
+ document = parse_canonical_json(raw)
161
+ except CanonicalJSONError:
162
+ raise CatalogFillRefused(
163
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.shard", "shard is not canonical"
164
+ ) from None
165
+ if (
166
+ not isinstance(document, dict)
167
+ or set(document) != {"schema_version", "kind", "payload"}
168
+ or document["schema_version"] != SHARD_SCHEMA
169
+ or document["kind"] != kind
170
+ or not isinstance(document["payload"], dict)
171
+ ):
172
+ raise CatalogFillRefused(
173
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.shard", "shard envelope is invalid"
174
+ )
175
+ return document["payload"]
176
+
177
+ def write_blob(self, kind: str, content: bytes) -> str:
178
+ """Install exact bounded bytes under their digest without a JSON/base64 rewrite."""
179
+
180
+ self._require_locked()
181
+ if kind not in _BLOB_KINDS or not isinstance(content, bytes):
182
+ raise CatalogFillRefused("FILL_STAGING_KIND", "fill.staging.kind", "unknown blob")
183
+ if len(content) > MAX_BLOB_BYTES:
184
+ raise CatalogFillRefused(
185
+ "FILL_SHARD_LIMIT", "fill.staging.blob", "blob exceeds the byte limit"
186
+ )
187
+ digest = sha256_bytes(content)
188
+ name = f"{digest}.bin"
189
+ directory_fd = self._kind_fds[kind]
190
+ self._publish_immutable(
191
+ directory_fd,
192
+ name,
193
+ content,
194
+ maximum=len(content),
195
+ field="fill.staging.blob",
196
+ )
197
+ return digest
198
+
199
+ def _publish_immutable(
200
+ self,
201
+ directory_fd: int,
202
+ name: str,
203
+ content: bytes,
204
+ *,
205
+ maximum: int,
206
+ field: str,
207
+ ) -> None:
208
+ """Atomically install immutable bytes without ever exposing a partial final name."""
209
+
210
+ try:
211
+ existing = self._read_regular_at(directory_fd, name, maximum=maximum)
212
+ except FileNotFoundError:
213
+ existing = None
214
+ except OSError as error:
215
+ raise CatalogFillRefused(
216
+ "FILL_STAGING_IO", field, "immutable object lookup failed"
217
+ ) from error
218
+ if existing is not None:
219
+ if existing != content:
220
+ raise CatalogFillRefused("FILL_SHARD_COLLISION", field, "digest path bytes differ")
221
+ return
222
+
223
+ temporary = f".publish.{os.getpid()}.{secrets.token_hex(16)}.tmp"
224
+ descriptor = -1
225
+ installed = False
226
+ try:
227
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
228
+ descriptor = os.open(temporary, flags, 0o600, dir_fd=directory_fd)
229
+ _write_all(descriptor, content)
230
+ os.fsync(descriptor)
231
+ os.close(descriptor)
232
+ descriptor = -1
233
+ try:
234
+ os.link(
235
+ temporary,
236
+ name,
237
+ src_dir_fd=directory_fd,
238
+ dst_dir_fd=directory_fd,
239
+ follow_symlinks=False,
240
+ )
241
+ installed = True
242
+ except FileExistsError:
243
+ pass
244
+ except OSError as error:
245
+ raise CatalogFillRefused(
246
+ "FILL_STAGING_IO", field, "immutable object publication failed"
247
+ ) from error
248
+ finally:
249
+ if descriptor >= 0:
250
+ os.close(descriptor)
251
+ try:
252
+ os.unlink(temporary, dir_fd=directory_fd)
253
+ except FileNotFoundError:
254
+ pass
255
+ except OSError as error:
256
+ raise CatalogFillRefused(
257
+ "FILL_STAGING_IO", field, "temporary object cleanup failed"
258
+ ) from error
259
+ try:
260
+ os.fsync(directory_fd)
261
+ except OSError as error:
262
+ raise CatalogFillRefused(
263
+ "FILL_STAGING_IO", field, "immutable object directory sync failed"
264
+ ) from error
265
+
266
+ try:
267
+ observed = self._read_regular_at(directory_fd, name, maximum=maximum)
268
+ except OSError as error:
269
+ raise CatalogFillRefused(
270
+ "FILL_STAGING_IO", field, "immutable object readback failed"
271
+ ) from error
272
+ if observed != content:
273
+ code = "FILL_SHARD_READBACK" if installed else "FILL_SHARD_COLLISION"
274
+ raise CatalogFillRefused(code, field, "immutable object readback differs")
275
+
276
+ def read_blob(self, kind: str, digest: str) -> bytes:
277
+ self._require_locked()
278
+ if kind not in _BLOB_KINDS or not _is_digest(digest):
279
+ raise CatalogFillRefused(
280
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.blob", "blob coordinate is invalid"
281
+ )
282
+ try:
283
+ content = self._read_regular_at(
284
+ self._kind_fds[kind], f"{digest}.bin", maximum=MAX_BLOB_BYTES
285
+ )
286
+ except FileNotFoundError as error:
287
+ raise CatalogFillRefused(
288
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.blob", "referenced blob is absent"
289
+ ) from error
290
+ if sha256_bytes(content) != digest:
291
+ raise CatalogFillRefused(
292
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.blob", "blob digest mismatch"
293
+ )
294
+ return content
295
+
296
+ @staticmethod
297
+ def empty_index_head() -> dict[str, Any]:
298
+ return {
299
+ "root_sha256": None,
300
+ "count": 0,
301
+ "tail_sha256": None,
302
+ "segment_count": 0,
303
+ "descriptor_count": 0,
304
+ }
305
+
306
+ def append_index(
307
+ self,
308
+ head: dict[str, Any],
309
+ *,
310
+ stream: str,
311
+ items: list[dict[str, Any]],
312
+ ) -> dict[str, Any]:
313
+ """Append one immutable <=4096-item/1-MiB segment to a compact Merkle-style head."""
314
+
315
+ self._require_locked()
316
+ _validate_index_head(head)
317
+ if (
318
+ not isinstance(stream, str)
319
+ or not stream
320
+ or not isinstance(items, list)
321
+ or not 1 <= len(items) <= MAX_INDEX_SEGMENT_ITEMS
322
+ or any(not isinstance(item, dict) for item in items)
323
+ ):
324
+ raise CatalogFillRefused(
325
+ "FILL_INDEX_SEGMENT", "fill.staging.index", "index segment is invalid"
326
+ )
327
+ if head["count"] + len(items) > MAX_INDEX_ITEMS:
328
+ raise CatalogFillRefused(
329
+ "FILL_CHECKPOINT_LIMIT",
330
+ "fill.staging.index",
331
+ "prospective index count exceeds the segmented cap",
332
+ )
333
+ payload = {
334
+ "stream": stream,
335
+ "previous_sha256": head["tail_sha256"],
336
+ "start": head["count"],
337
+ "items": items,
338
+ }
339
+ digest = self.write_shard("index", payload)
340
+ root = canonical_index_root(
341
+ previous_root=head["root_sha256"],
342
+ segment_sha256=digest,
343
+ start=head["count"],
344
+ item_count=len(items),
345
+ )
346
+ return {
347
+ "root_sha256": root,
348
+ "count": head["count"] + len(items),
349
+ "tail_sha256": digest,
350
+ "segment_count": head["segment_count"] + 1,
351
+ # Segment descriptors live only in the immutable chain. The bounded head embeds none.
352
+ "descriptor_count": 0,
353
+ }
354
+
355
+ def iter_index(
356
+ self,
357
+ head: dict[str, Any],
358
+ *,
359
+ stream: str,
360
+ start: int = 0,
361
+ ) -> Iterator[dict[str, Any]]:
362
+ self._require_locked()
363
+ _validate_index_head(head)
364
+ if type(start) is not int or not 0 <= start <= head["count"]:
365
+ raise CatalogFillRefused(
366
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.index", "index start is invalid"
367
+ )
368
+ traversed = 0
369
+ current = head["tail_sha256"]
370
+ with tempfile.TemporaryFile(mode="w+b") as coordinates:
371
+ while current is not None:
372
+ if traversed >= head["segment_count"] + 1:
373
+ raise CatalogFillRefused(
374
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.index", "index chain loops"
375
+ )
376
+ traversed += 1
377
+ node = self.read_shard("index", current)
378
+ if (
379
+ set(node) != {"stream", "previous_sha256", "start", "items"}
380
+ or node["stream"] != stream
381
+ or type(node["start"]) is not int
382
+ or not isinstance(node["items"], list)
383
+ or not 1 <= len(node["items"]) <= MAX_INDEX_SEGMENT_ITEMS
384
+ or any(not isinstance(item, dict) for item in node["items"])
385
+ or (
386
+ node["previous_sha256"] is not None
387
+ and not _is_digest(node["previous_sha256"])
388
+ )
389
+ ):
390
+ raise CatalogFillRefused(
391
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.index", "index node is invalid"
392
+ )
393
+ coordinates.write(current.encode("ascii") + b"\n")
394
+ current = node["previous_sha256"]
395
+ if traversed != head["segment_count"]:
396
+ raise CatalogFillRefused(
397
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.index", "segment count differs"
398
+ )
399
+ count = 0
400
+ root: str | None = None
401
+ for position in range(head["segment_count"] - 1, -1, -1):
402
+ coordinates.seek(position * 65)
403
+ digest = coordinates.read(64).decode("ascii")
404
+ node = self.read_shard("index", digest)
405
+ if node["start"] != count:
406
+ raise CatalogFillRefused(
407
+ "FILL_CHECKPOINT_CORRUPT",
408
+ "fill.staging.index",
409
+ "index is not contiguous",
410
+ )
411
+ root = canonical_index_root(
412
+ previous_root=root,
413
+ segment_sha256=digest,
414
+ start=count,
415
+ item_count=len(node["items"]),
416
+ )
417
+ for offset, item in enumerate(node["items"]):
418
+ if count + offset >= start:
419
+ yield item
420
+ count += len(node["items"])
421
+ if count != head["count"] or root != head["root_sha256"]:
422
+ raise CatalogFillRefused(
423
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.index", "index head differs"
424
+ )
425
+
426
+ def validate_index(self, head: dict[str, Any], *, stream: str) -> None:
427
+ """Read and authenticate every bounded segment behind one compact head."""
428
+
429
+ for _item in self.iter_index(head, stream=stream):
430
+ pass
431
+
432
+ def integrity_epoch(self) -> tuple[tuple[str, int, int, int, int], ...]:
433
+ """Return a cheap process-local mutation coordinate for the retained tree."""
434
+
435
+ self._require_locked()
436
+ descriptors = {
437
+ "$root": self._root_fd,
438
+ "$shards": self._shards_fd,
439
+ **self._kind_fds,
440
+ }
441
+ coordinates: list[tuple[str, int, int, int, int]] = []
442
+ for name, descriptor in sorted(descriptors.items()):
443
+ info = os.fstat(descriptor)
444
+ coordinates.append((name, info.st_dev, info.st_ino, info.st_mtime_ns, info.st_ctime_ns))
445
+ return tuple(coordinates)
446
+
447
+ def index_tail_items(self, head: dict[str, Any], *, stream: str) -> tuple[dict[str, Any], ...]:
448
+ """Authenticate only the newly appended immutable tail segment."""
449
+
450
+ self._require_locked()
451
+ _validate_index_head(head)
452
+ if head["count"] == 0:
453
+ if (
454
+ head["tail_sha256"] is not None
455
+ or head["root_sha256"] is not None
456
+ or head["segment_count"] != 0
457
+ ):
458
+ raise CatalogFillRefused(
459
+ "FILL_CHECKPOINT_CORRUPT",
460
+ "fill.staging.index",
461
+ "empty index head has a tail",
462
+ )
463
+ return ()
464
+ tail = head["tail_sha256"]
465
+ if not _is_digest(tail) or head["segment_count"] < 1:
466
+ raise CatalogFillRefused(
467
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.index", "index tail is absent"
468
+ )
469
+ node = self.read_shard("index", tail)
470
+ if (
471
+ set(node) != {"stream", "previous_sha256", "start", "items"}
472
+ or node["stream"] != stream
473
+ or type(node["start"]) is not int
474
+ or not isinstance(node["items"], list)
475
+ or not 1 <= len(node["items"]) <= MAX_INDEX_SEGMENT_ITEMS
476
+ or any(not isinstance(item, dict) for item in node["items"])
477
+ or node["start"] + len(node["items"]) != head["count"]
478
+ or (head["segment_count"] == 1) != (node["previous_sha256"] is None)
479
+ or (node["previous_sha256"] is not None and not _is_digest(node["previous_sha256"]))
480
+ ):
481
+ raise CatalogFillRefused(
482
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.index", "index tail is invalid"
483
+ )
484
+ return tuple(node["items"])
485
+
486
+ def commit(
487
+ self,
488
+ state: dict[str, Any],
489
+ *,
490
+ expected_generation: int,
491
+ expected_checkpoint_digest: str | None,
492
+ ) -> dict[str, Any]:
493
+ """CAS one state generation against both generation and exact live bytes."""
494
+
495
+ self._require_locked()
496
+ live_document, live_raw = self._read_state_document()
497
+ live_generation = -1 if live_document is None else live_document["generation"]
498
+ live_digest = None if live_raw is None else sha256_bytes(live_raw)
499
+ if live_generation != expected_generation or live_digest != expected_checkpoint_digest:
500
+ raise CatalogFillRefused(
501
+ "FILL_CHECKPOINT_STALE",
502
+ "fill.staging.state",
503
+ "live checkpoint generation or digest changed",
504
+ )
505
+ next_state = dict(state)
506
+ next_state["schema_version"] = STATE_SCHEMA
507
+ next_state["generation"] = expected_generation + 1
508
+ compact = dict(next_state)
509
+ if "evidence_indexes" in next_state:
510
+ if "page_shards" in next_state or "response_shards" in next_state:
511
+ raise CatalogFillRefused(
512
+ "FILL_CHECKPOINT_CORRUPT",
513
+ "fill.staging.state",
514
+ "segmented state may not embed growing shard histories",
515
+ )
516
+ else:
517
+ for stream in ("page", "response"):
518
+ values = next_state[f"{stream}_shards"]
519
+ compact.pop(f"{stream}_shards")
520
+ compact[f"{stream}_history"] = self._advance_history(
521
+ stream,
522
+ values,
523
+ live_document=live_document,
524
+ )
525
+ raw = canonical_json_bytes(compact)
526
+ if len(raw) > MAX_STATE_BYTES:
527
+ raise CatalogFillRefused(
528
+ "FILL_CHECKPOINT_LIMIT", "fill.staging.state", "state exceeds the byte limit"
529
+ )
530
+ temporary = f".state.{os.getpid()}.{expected_generation + 1}.{secrets.token_hex(8)}.tmp"
531
+ flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
532
+ replaced = False
533
+ try:
534
+ descriptor = os.open(temporary, flags, 0o600, dir_fd=self._root_fd)
535
+ try:
536
+ _write_all(descriptor, raw)
537
+ os.fsync(descriptor)
538
+ os.lseek(descriptor, 0, os.SEEK_SET)
539
+ if _read_open(descriptor, maximum=len(raw)) != raw:
540
+ raise CatalogFillRefused(
541
+ "FILL_CHECKPOINT_READBACK",
542
+ "fill.staging.state",
543
+ "temporary readback differs",
544
+ )
545
+ finally:
546
+ os.close(descriptor)
547
+ self._verify_tree()
548
+ os.replace(
549
+ temporary,
550
+ "state.json",
551
+ src_dir_fd=self._root_fd,
552
+ dst_dir_fd=self._root_fd,
553
+ )
554
+ replaced = True
555
+ os.fsync(self._root_fd)
556
+ if self._read_regular_at(self._root_fd, "state.json", maximum=len(raw)) != raw:
557
+ raise CatalogFillRefused(
558
+ "FILL_CHECKPOINT_READBACK", "fill.staging.state", "state readback differs"
559
+ )
560
+ self._verify_tree()
561
+ except BaseException:
562
+ if replaced:
563
+ self._restore_checkpoint(live_raw)
564
+ raise
565
+ finally:
566
+ try:
567
+ os.unlink(temporary, dir_fd=self._root_fd)
568
+ except FileNotFoundError:
569
+ pass
570
+ if "page_shards" in next_state:
571
+ self._active_histories = {
572
+ "page": next_state["page_shards"],
573
+ "response": next_state["response_shards"],
574
+ }
575
+ return next_state
576
+
577
+ def _restore_checkpoint(self, predecessor: bytes | None) -> None:
578
+ """Restore the authorized head after a post-replace durability failure."""
579
+
580
+ if predecessor is None:
581
+ try:
582
+ os.unlink("state.json", dir_fd=self._root_fd)
583
+ except FileNotFoundError:
584
+ pass
585
+ os.fsync(self._root_fd)
586
+ return
587
+ temporary = f".state.recover.{os.getpid()}.{secrets.token_hex(8)}.tmp"
588
+ flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
589
+ try:
590
+ descriptor = os.open(temporary, flags, 0o600, dir_fd=self._root_fd)
591
+ try:
592
+ _write_all(descriptor, predecessor)
593
+ os.fsync(descriptor)
594
+ finally:
595
+ os.close(descriptor)
596
+ os.replace(
597
+ temporary,
598
+ "state.json",
599
+ src_dir_fd=self._root_fd,
600
+ dst_dir_fd=self._root_fd,
601
+ )
602
+ os.fsync(self._root_fd)
603
+ if (
604
+ self._read_regular_at(self._root_fd, "state.json", maximum=len(predecessor))
605
+ != predecessor
606
+ ):
607
+ raise CatalogFillRefused(
608
+ "FILL_CHECKPOINT_RECOVERY",
609
+ "fill.staging.state",
610
+ "predecessor checkpoint recovery readback differs",
611
+ )
612
+ finally:
613
+ try:
614
+ os.unlink(temporary, dir_fd=self._root_fd)
615
+ except FileNotFoundError:
616
+ pass
617
+
618
+ def state_digest(self) -> str:
619
+ self._require_locked()
620
+ try:
621
+ raw = self._read_regular_at(self._root_fd, "state.json", maximum=MAX_STATE_BYTES)
622
+ except FileNotFoundError as error:
623
+ raise CatalogFillRefused(
624
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.state", "state is absent"
625
+ ) from error
626
+ return sha256_bytes(raw)
627
+
628
+ def _load_state(self, *, validate_shards: bool) -> dict[str, Any] | None:
629
+ state, _raw = self._read_state_document()
630
+ if state is None:
631
+ return None
632
+ if type(state.get("generation")) is not int or state["generation"] < 0:
633
+ raise CatalogFillRefused(
634
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.generation", "generation is invalid"
635
+ )
636
+ budget = state.get("budget")
637
+ budget_keys = {
638
+ "requests",
639
+ "responses",
640
+ "pages",
641
+ "records",
642
+ "network_bytes",
643
+ "elapsed_ns",
644
+ "skipped_records",
645
+ }
646
+ if (
647
+ not isinstance(budget, dict)
648
+ or set(budget) != budget_keys
649
+ or any(type(value) is not int or value < 0 for value in budget.values())
650
+ ):
651
+ raise CatalogFillRefused(
652
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.budget", "budget is invalid"
653
+ )
654
+ if "evidence_indexes" in state:
655
+ if "page_history" in state or "response_history" in state:
656
+ raise CatalogFillRefused(
657
+ "FILL_CHECKPOINT_CORRUPT",
658
+ "fill.staging.state",
659
+ "segmented state may not carry legacy histories",
660
+ )
661
+ indexes = state["evidence_indexes"]
662
+ if not isinstance(indexes, dict) or not indexes:
663
+ raise CatalogFillRefused(
664
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.indexes", "indexes are invalid"
665
+ )
666
+ for stream, head in indexes.items():
667
+ if not isinstance(stream, str) or not stream:
668
+ raise CatalogFillRefused(
669
+ "FILL_CHECKPOINT_CORRUPT",
670
+ "fill.staging.indexes",
671
+ "index stream is invalid",
672
+ )
673
+ _validate_index_head(head)
674
+ if validate_shards:
675
+ # Segmented checkpoints carry a producer-authenticated tail seal. Ordinary
676
+ # resumes authenticate the immutable appended tail here; eligibility runs
677
+ # the full streaming chain audit explicitly.
678
+ self.index_tail_items(head, stream=stream)
679
+ return state
680
+ pages = self._expand_history("page", state.get("page_history"))
681
+ responses = self._expand_history("response", state.get("response_history"))
682
+ if budget["pages"] != len(pages) or budget["responses"] < len(responses):
683
+ raise CatalogFillRefused(
684
+ "FILL_CHECKPOINT_CORRUPT",
685
+ "fill.staging.budget",
686
+ "budget does not bind the staged shards",
687
+ )
688
+ if validate_shards:
689
+ for digest in pages:
690
+ self.read_shard("page", digest)
691
+ for digest in responses:
692
+ self.read_shard("response", digest)
693
+ expanded = dict(state)
694
+ del expanded["page_history"]
695
+ del expanded["response_history"]
696
+ expanded["page_shards"] = pages
697
+ expanded["response_shards"] = responses
698
+ self._active_histories = {"page": pages, "response": responses}
699
+ return expanded
700
+
701
+ def _read_state_document(self) -> tuple[dict[str, Any] | None, bytes | None]:
702
+ try:
703
+ raw = self._read_regular_at(self._root_fd, "state.json", maximum=MAX_STATE_BYTES)
704
+ except FileNotFoundError:
705
+ return None, None
706
+ try:
707
+ state = parse_canonical_json(raw)
708
+ except CanonicalJSONError as error:
709
+ raise CatalogFillRefused(
710
+ "FILL_CHECKPOINT_CORRUPT",
711
+ "fill.staging.state",
712
+ f"state is not canonical: {error.code}",
713
+ ) from None
714
+ if not isinstance(state, dict) or state.get("schema_version") != STATE_SCHEMA:
715
+ raise CatalogFillRefused(
716
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.state", "state schema is invalid"
717
+ )
718
+ if "page_shards" in state or "response_shards" in state:
719
+ raise CatalogFillRefused(
720
+ "FILL_CHECKPOINT_CORRUPT",
721
+ "fill.staging.state",
722
+ "growing shard history may not be embedded in the checkpoint head",
723
+ )
724
+ return state, raw
725
+
726
+ def _advance_history(
727
+ self,
728
+ stream: str,
729
+ values: Any,
730
+ *,
731
+ live_document: dict[str, Any] | None,
732
+ ) -> dict[str, Any]:
733
+ if not isinstance(values, list) or any(not _is_digest(value) for value in values):
734
+ raise CatalogFillRefused(
735
+ "FILL_CHECKPOINT_CORRUPT",
736
+ f"fill.staging.{stream}_shards",
737
+ "history values must be SHA-256 digests",
738
+ )
739
+ prior = (
740
+ {"head_sha256": None, "count": 0}
741
+ if live_document is None
742
+ else live_document.get(f"{stream}_history")
743
+ )
744
+ if not _valid_history_head(prior):
745
+ raise CatalogFillRefused(
746
+ "FILL_CHECKPOINT_CORRUPT",
747
+ f"fill.staging.{stream}_history",
748
+ "history head is invalid",
749
+ )
750
+ prior_count = prior["count"]
751
+ active = self._active_histories.get(stream)
752
+ if live_document is not None and values is not active:
753
+ raise CatalogFillRefused(
754
+ "FILL_CHECKPOINT_STALE",
755
+ f"fill.staging.{stream}_history",
756
+ "history must extend the exact list returned by the retained staging load",
757
+ )
758
+ if not prior_count <= len(values) <= MAX_HISTORY_DIGESTS:
759
+ raise CatalogFillRefused(
760
+ "FILL_CHECKPOINT_LIMIT",
761
+ f"fill.staging.{stream}_history",
762
+ "history count is outside its bound",
763
+ )
764
+ suffix = values[prior_count:]
765
+ if not suffix:
766
+ return dict(prior)
767
+ head = self.write_shard(
768
+ "history",
769
+ {
770
+ "stream": stream,
771
+ "previous_sha256": prior["head_sha256"],
772
+ "start": prior_count,
773
+ "digests": suffix,
774
+ },
775
+ )
776
+ return {"head_sha256": head, "count": len(values)}
777
+
778
+ def _expand_history(self, stream: str, head: Any) -> list[str]:
779
+ if not _valid_history_head(head):
780
+ raise CatalogFillRefused(
781
+ "FILL_CHECKPOINT_CORRUPT",
782
+ f"fill.staging.{stream}_history",
783
+ "history head is invalid",
784
+ )
785
+ count = head["count"]
786
+ if count > MAX_HISTORY_DIGESTS:
787
+ raise CatalogFillRefused(
788
+ "FILL_CHECKPOINT_LIMIT",
789
+ f"fill.staging.{stream}_history",
790
+ "history exceeds the digest-count limit",
791
+ )
792
+ chunks: list[tuple[int, list[str]]] = []
793
+ seen: set[str] = set()
794
+ current = head["head_sha256"]
795
+ while current is not None:
796
+ if current in seen or len(seen) > count:
797
+ raise CatalogFillRefused(
798
+ "FILL_CHECKPOINT_CORRUPT",
799
+ f"fill.staging.{stream}_history",
800
+ "history chain loops or exceeds its count",
801
+ )
802
+ seen.add(current)
803
+ node = self.read_shard("history", current)
804
+ if (
805
+ set(node) != {"stream", "previous_sha256", "start", "digests"}
806
+ or node["stream"] != stream
807
+ or type(node["start"]) is not int
808
+ or not isinstance(node["digests"], list)
809
+ or any(not _is_digest(value) for value in node["digests"])
810
+ or (node["previous_sha256"] is not None and not _is_digest(node["previous_sha256"]))
811
+ ):
812
+ raise CatalogFillRefused(
813
+ "FILL_CHECKPOINT_CORRUPT",
814
+ f"fill.staging.{stream}_history",
815
+ "history node is invalid",
816
+ )
817
+ chunks.append((node["start"], node["digests"]))
818
+ current = node["previous_sha256"]
819
+ result: list[str] = []
820
+ for start, digests in reversed(chunks):
821
+ if start != len(result):
822
+ raise CatalogFillRefused(
823
+ "FILL_CHECKPOINT_CORRUPT",
824
+ f"fill.staging.{stream}_history",
825
+ "history nodes are not contiguous",
826
+ )
827
+ result.extend(digests)
828
+ if len(result) != count:
829
+ raise CatalogFillRefused(
830
+ "FILL_CHECKPOINT_CORRUPT",
831
+ f"fill.staging.{stream}_history",
832
+ "history count differs from its chain",
833
+ )
834
+ return result
835
+
836
+ def _open_retained_tree(self) -> None:
837
+ if self._retained_root_descriptor is not None:
838
+ try:
839
+ descriptor = os.dup(self._retained_root_descriptor)
840
+ opened = os.fstat(descriptor)
841
+ except OSError as error:
842
+ raise CatalogFillRefused(
843
+ "FILL_STAGING_PATH",
844
+ "fill.staging",
845
+ "the retained staging root is unusable",
846
+ ) from error
847
+ self._owned_fds.append(descriptor)
848
+ if not stat.S_ISDIR(opened.st_mode) or stat.S_IMODE(opened.st_mode) & 0o077:
849
+ raise CatalogFillRefused(
850
+ "FILL_STAGING_PERMISSIONS",
851
+ "fill.staging",
852
+ "staging root must be a retained private directory",
853
+ )
854
+ self._root_fd = descriptor
855
+ self._shards_fd = self._open_or_create_directory(self._root_fd, "shards")
856
+ for kind in sorted(_SHARD_KINDS | _BLOB_KINDS):
857
+ self._kind_fds[kind] = self._open_or_create_directory(self._shards_fd, kind)
858
+ return
859
+ absolute = Path(os.path.abspath(self.root))
860
+ descriptor = os.open("/", _OPEN_DIRECTORY)
861
+ self._owned_fds.append(descriptor)
862
+ for part in absolute.parts[1:]:
863
+ try:
864
+ named = os.stat(part, dir_fd=descriptor, follow_symlinks=False)
865
+ except FileNotFoundError:
866
+ os.mkdir(part, mode=0o700, dir_fd=descriptor)
867
+ os.fsync(descriptor)
868
+ named = os.stat(part, dir_fd=descriptor, follow_symlinks=False)
869
+ child = os.open(part, _OPEN_DIRECTORY, dir_fd=descriptor)
870
+ self._owned_fds.append(child)
871
+ opened = os.fstat(child)
872
+ if not stat.S_ISDIR(named.st_mode) or _directory_identity(named) != _directory_identity(
873
+ opened
874
+ ):
875
+ raise CatalogFillRefused(
876
+ "FILL_STAGING_RACE", "fill.staging", "named and opened directory differ"
877
+ )
878
+ self._chain.append((descriptor, part, child, _directory_identity(opened)))
879
+ descriptor = child
880
+ self._root_fd = descriptor
881
+ root_info = os.fstat(self._root_fd)
882
+ if stat.S_IMODE(root_info.st_mode) & 0o077:
883
+ raise CatalogFillRefused(
884
+ "FILL_STAGING_PERMISSIONS", "fill.staging", "staging root must be mode 0700"
885
+ )
886
+ self._shards_fd = self._open_or_create_directory(self._root_fd, "shards")
887
+ for kind in sorted(_SHARD_KINDS | _BLOB_KINDS):
888
+ self._kind_fds[kind] = self._open_or_create_directory(self._shards_fd, kind)
889
+
890
+ def _open_or_create_directory(self, parent_fd: int, name: str) -> int:
891
+ try:
892
+ os.mkdir(name, mode=0o700, dir_fd=parent_fd)
893
+ os.fsync(parent_fd)
894
+ except FileExistsError:
895
+ pass
896
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
897
+ descriptor = os.open(name, _OPEN_DIRECTORY, dir_fd=parent_fd)
898
+ self._owned_fds.append(descriptor)
899
+ opened = os.fstat(descriptor)
900
+ if (
901
+ not stat.S_ISDIR(named.st_mode)
902
+ or _directory_identity(named) != _directory_identity(opened)
903
+ or stat.S_IMODE(opened.st_mode) & 0o077
904
+ ):
905
+ raise CatalogFillRefused(
906
+ "FILL_STAGING_PATH",
907
+ "fill.staging",
908
+ "staging components must be retained private directories",
909
+ )
910
+ return descriptor
911
+
912
+ def _verify_tree(self) -> None:
913
+ for parent_fd, name, child_fd, identity in self._chain:
914
+ try:
915
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
916
+ except OSError as error:
917
+ raise CatalogFillRefused(
918
+ "FILL_STAGING_RACE", "fill.staging", "staging ancestor was replaced"
919
+ ) from error
920
+ if (
921
+ _directory_identity(named) != identity
922
+ or _directory_identity(os.fstat(child_fd)) != identity
923
+ ):
924
+ raise CatalogFillRefused(
925
+ "FILL_STAGING_RACE", "fill.staging", "staging ancestor was replaced"
926
+ )
927
+ for parent_fd, name, child_fd in (
928
+ (self._root_fd, "shards", self._shards_fd),
929
+ *((self._shards_fd, kind, fd) for kind, fd in self._kind_fds.items()),
930
+ ):
931
+ named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
932
+ if _directory_identity(named) != _directory_identity(os.fstat(child_fd)):
933
+ raise CatalogFillRefused(
934
+ "FILL_STAGING_RACE", "fill.staging", "staging directory was replaced"
935
+ )
936
+
937
+ def _read_regular_at(self, parent_fd: int, name: str, *, maximum: int) -> bytes:
938
+ descriptor = -1
939
+ try:
940
+ named_before = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
941
+ descriptor = os.open(name, _OPEN_MEMBER, dir_fd=parent_fd)
942
+ except FileNotFoundError:
943
+ if descriptor >= 0:
944
+ os.close(descriptor)
945
+ raise
946
+ except OSError as error:
947
+ if descriptor >= 0:
948
+ os.close(descriptor)
949
+ raise CatalogFillRefused(
950
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.member", "member cannot be opened"
951
+ ) from error
952
+ try:
953
+ opened_before = os.fstat(descriptor)
954
+ if (
955
+ not stat.S_ISREG(named_before.st_mode)
956
+ or not stat.S_ISREG(opened_before.st_mode)
957
+ or named_before.st_nlink != 1
958
+ or opened_before.st_nlink != 1
959
+ or opened_before.st_size > maximum
960
+ or _identity(named_before) != _identity(opened_before)
961
+ ):
962
+ raise CatalogFillRefused(
963
+ "FILL_CHECKPOINT_CORRUPT",
964
+ "fill.staging.member",
965
+ "member is not a bounded single-link regular file",
966
+ )
967
+ raw = _read_open(descriptor, maximum=maximum)
968
+ opened_after = os.fstat(descriptor)
969
+ named_after = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
970
+ if (
971
+ _identity(opened_before) != _identity(opened_after)
972
+ or _identity(opened_after) != _identity(named_after)
973
+ or len(raw) != opened_after.st_size
974
+ ):
975
+ raise CatalogFillRefused(
976
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.member", "member changed during read"
977
+ )
978
+ return raw
979
+ finally:
980
+ os.close(descriptor)
981
+
982
+ def _require_locked(self) -> None:
983
+ if self._root_fd < 0:
984
+ raise CatalogFillRefused(
985
+ "FILL_STAGING_LOCK", "fill.staging", "operation requires the retained lock"
986
+ )
987
+
988
+
989
+ def _read_open(descriptor: int, *, maximum: int) -> bytes:
990
+ chunks: list[bytes] = []
991
+ observed = 0
992
+ while True:
993
+ chunk = os.read(descriptor, min(1024 * 1024, maximum - observed + 1))
994
+ if not chunk:
995
+ break
996
+ observed += len(chunk)
997
+ if observed > maximum:
998
+ raise CatalogFillRefused(
999
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.member", "member exceeds byte limit"
1000
+ )
1001
+ chunks.append(chunk)
1002
+ return b"".join(chunks)
1003
+
1004
+
1005
+ def _write_all(descriptor: int, raw: bytes) -> None:
1006
+ written = 0
1007
+ while written < len(raw):
1008
+ count = os.write(descriptor, raw[written:])
1009
+ if count < 1:
1010
+ raise CatalogFillRefused(
1011
+ "FILL_STAGING_WRITE", "fill.staging.member", "write made no progress"
1012
+ )
1013
+ written += count
1014
+
1015
+
1016
+ def _identity(info: os.stat_result) -> tuple[int, ...]:
1017
+ return (
1018
+ info.st_dev,
1019
+ info.st_ino,
1020
+ info.st_mode,
1021
+ info.st_nlink,
1022
+ info.st_size,
1023
+ info.st_mtime_ns,
1024
+ info.st_ctime_ns,
1025
+ )
1026
+
1027
+
1028
+ def _directory_identity(info: os.stat_result) -> tuple[int, int, int]:
1029
+ return (info.st_dev, info.st_ino, stat.S_IFMT(info.st_mode))
1030
+
1031
+
1032
+ def _is_digest(value: Any) -> bool:
1033
+ return (
1034
+ isinstance(value, str) and len(value) == 64 and all(c in "0123456789abcdef" for c in value)
1035
+ )
1036
+
1037
+
1038
+ def _valid_history_head(value: Any) -> bool:
1039
+ return (
1040
+ isinstance(value, dict)
1041
+ and set(value) == {"head_sha256", "count"}
1042
+ and type(value["count"]) is int
1043
+ and value["count"] >= 0
1044
+ and (
1045
+ (value["count"] == 0 and value["head_sha256"] is None)
1046
+ or (value["count"] > 0 and _is_digest(value["head_sha256"]))
1047
+ )
1048
+ )
1049
+
1050
+
1051
+ def canonical_index_root(
1052
+ *,
1053
+ previous_root: str | None,
1054
+ segment_sha256: str,
1055
+ start: int,
1056
+ item_count: int,
1057
+ ) -> str:
1058
+ return canonical_sha256(
1059
+ {
1060
+ "previous_root_sha256": previous_root,
1061
+ "segment_sha256": segment_sha256,
1062
+ "start": start,
1063
+ "item_count": item_count,
1064
+ }
1065
+ )
1066
+
1067
+
1068
+ def _validate_index_head(value: Any) -> None:
1069
+ if (
1070
+ not isinstance(value, dict)
1071
+ or set(value)
1072
+ != {
1073
+ "root_sha256",
1074
+ "count",
1075
+ "tail_sha256",
1076
+ "segment_count",
1077
+ "descriptor_count",
1078
+ }
1079
+ or type(value["count"]) is not int
1080
+ or type(value["segment_count"]) is not int
1081
+ or value["count"] < 0
1082
+ or value["segment_count"] < 0
1083
+ or value["count"] > MAX_INDEX_ITEMS
1084
+ or value["segment_count"] > value["count"]
1085
+ or value["descriptor_count"] != 0
1086
+ or (
1087
+ value["count"] == 0
1088
+ and (
1089
+ value["root_sha256"] is not None
1090
+ or value["tail_sha256"] is not None
1091
+ or value["segment_count"] != 0
1092
+ )
1093
+ )
1094
+ or (
1095
+ value["count"] > 0
1096
+ and (
1097
+ not _is_digest(value["root_sha256"])
1098
+ or not _is_digest(value["tail_sha256"])
1099
+ or value["segment_count"] < 1
1100
+ )
1101
+ )
1102
+ ):
1103
+ raise CatalogFillRefused(
1104
+ "FILL_CHECKPOINT_CORRUPT", "fill.staging.index", "index head is invalid"
1105
+ )