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,891 @@
1
+ """Installing a published catalog generation onto this computer, with no service in between.
2
+
3
+ A generation is distributed as one zip archive whose exact SHA-256 the operator publishes next
4
+ to it. The archive holds the generation's folder under one ``catalog/`` prefix plus one
5
+ ``release.json`` naming the store kind, the store file's digest, and the digest of the store file
6
+ this release descends from. Trust flows from exactly two places and nowhere else: the archive
7
+ digest the person typed, and the state this computer recorded when it installed the current
8
+ generation. There is no ambient discovery, no version service, and no clock: everything verified
9
+ here is a pure function of the archive bytes, the arguments, and the recorded state.
10
+
11
+ The descent statement is cross-checked against the store itself for a packed generation, whose
12
+ head names its own predecessor. A v2 retrieval store carries no predecessor field, so for that
13
+ kind the statement stands on the packaging invocation alone, bound by the archive digest: it is
14
+ the packager's word, published under the digest the person typed.
15
+
16
+ Installation is append-only. Each generation lands in its own folder named by its store digest,
17
+ written under a private staging name, verified in place by the real store loading -- the same
18
+ checks a search performs -- and only then made visible by one atomic rename; the recorded state
19
+ is replaced through the same append-only receipt discipline the publish lane uses, so the
20
+ previous state is retained rather than overwritten. A destination folder that already holds this
21
+ generation -- retained from an earlier install -- is re-verified and adopted rather than refused,
22
+ so rolling back to a retained generation and retrying after an interrupted state write both work.
23
+
24
+ The archive is screened the way the execution-coordinate wheel is screened: member names are
25
+ refused rather than sanitised (no absolute names, no ``..`` parts, no backslashes, no folder
26
+ members, no duplicate names -- including names that collide once case or accent normalisation is
27
+ applied, because the destination filesystem may apply either -- no links or special files, no
28
+ encrypted members), and byte budgets bound every member, the whole expansion, and the expansion
29
+ ratio, so a hostile archive is refused before a byte of it reaches the destination folder.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import io
35
+ import os
36
+ import re
37
+ import stat
38
+ import unicodedata
39
+ import zipfile
40
+ import zlib
41
+ from collections.abc import Mapping
42
+ from dataclasses import dataclass
43
+ from pathlib import Path
44
+ from typing import Any
45
+
46
+ from mostlyright.data_harness.canonical import (
47
+ CanonicalJSONError,
48
+ canonical_json_bytes,
49
+ parse_canonical_json,
50
+ sha256_bytes,
51
+ )
52
+ from mostlyright.data_harness.readers.containers import read_bounded, screen_zip_entry
53
+ from mostlyright.data_harness.readers.contracts import ReaderError
54
+ from mostlyright.data_harness.sources.catalog.generation_receipt import (
55
+ ARCHIVE_EXPANSION_FLOOR_BYTES,
56
+ ARCHIVE_EXPANSION_RATIO,
57
+ install_receipt,
58
+ is_digest,
59
+ read_bounded_document,
60
+ )
61
+ from mostlyright.data_harness.sources.catalog.packed_catalog import (
62
+ MAX_PACKED_HEAD_BYTES,
63
+ PACKED_HEAD_FILENAME,
64
+ verify_packed_generation,
65
+ )
66
+ from mostlyright.data_harness.sources.catalog.packed_retrieval import detect_public_store_kind
67
+ from mostlyright.data_harness.sources.catalog.retrieval_manifest import (
68
+ MAX_RETRIEVAL_MANIFEST_BYTES,
69
+ RETRIEVAL_MANIFEST_FILENAME,
70
+ load_retrieval_catalog,
71
+ )
72
+ from mostlyright.data_harness.sources.contracts import (
73
+ SourceContractError,
74
+ require_canonical_timestamp,
75
+ )
76
+
77
+ __all__ = [
78
+ "ARCHIVE_MEMBER_PREFIX",
79
+ "GENERATIONS_DIRNAME",
80
+ "INSTALLED_STATE_FILENAME",
81
+ "INSTALLED_STATE_SCHEMA",
82
+ "MAX_RELEASE_ARCHIVE_BYTES",
83
+ "RELEASE_DESCRIPTOR_FILENAME",
84
+ "RELEASE_DESCRIPTOR_SCHEMA",
85
+ "SUPPORTED_STORE_KINDS",
86
+ "CatalogUpdateReport",
87
+ "InstalledState",
88
+ "ReleaseDescriptor",
89
+ "build_release_archive",
90
+ "confirm_already_current",
91
+ "install_release",
92
+ "load_installed_state",
93
+ "verify_release_archive",
94
+ ]
95
+
96
+ RELEASE_DESCRIPTOR_FILENAME = "release.json"
97
+ RELEASE_DESCRIPTOR_SCHEMA = "mr-data-catalog-release.v1"
98
+ INSTALLED_STATE_FILENAME = "installed.json"
99
+ INSTALLED_STATE_SCHEMA = "mr-data-catalog-installed.v1"
100
+ GENERATIONS_DIRNAME = "generations"
101
+ ARCHIVE_MEMBER_PREFIX = "catalog/"
102
+ SUPPORTED_STORE_KINDS = ("packed", "retrieval_v2")
103
+
104
+ MAX_RELEASE_ARCHIVE_BYTES = 1_073_741_824
105
+ MAX_RELEASE_MEMBER_BYTES = 268_435_456
106
+ MAX_RELEASE_MEMBER_COUNT = 50_000
107
+ MAX_RELEASE_DESCRIPTOR_BYTES = 4_096
108
+ MAX_INSTALLED_STATE_BYTES = 65_536
109
+ MAX_RELEASE_ENTRY_COUNT = 10_000_000
110
+ MAX_MEMBER_NAME_PARTS = 16
111
+ MAX_MEMBER_NAME_PART_LENGTH = 255
112
+
113
+ _STORE_FILENAMES = {
114
+ "packed": PACKED_HEAD_FILENAME,
115
+ "retrieval_v2": RETRIEVAL_MANIFEST_FILENAME,
116
+ }
117
+ _STORE_FILE_MAXIMUMS = {
118
+ "packed": MAX_PACKED_HEAD_BYTES,
119
+ "retrieval_v2": MAX_RETRIEVAL_MANIFEST_BYTES,
120
+ }
121
+ _RELEASE_DESCRIPTOR_KEYS = frozenset(
122
+ {
123
+ "entry_count",
124
+ "predecessor_store_sha256",
125
+ "published_at",
126
+ "schema_version",
127
+ "store_kind",
128
+ "store_sha256",
129
+ }
130
+ )
131
+ _INSTALLED_STATE_KEYS = frozenset(
132
+ {
133
+ "archive_sha256",
134
+ "entry_count",
135
+ "generation_path",
136
+ "predecessor_store_sha256",
137
+ "published_at",
138
+ "schema_version",
139
+ "store_kind",
140
+ "store_sha256",
141
+ }
142
+ )
143
+ # A generation folder is named by the first twelve characters of its store digest and by
144
+ # nothing else, so the recorded path is a closed shape rather than a screened one.
145
+ _GENERATION_FOLDER_PATTERN = re.compile(r"[0-9a-f]{12}")
146
+
147
+
148
+ def _refuse(code: str, path: str, detail: str) -> SourceContractError:
149
+ return SourceContractError(code, path, detail)
150
+
151
+
152
+ def _require_release_facts(
153
+ *,
154
+ store_kind: str,
155
+ store_sha256: str,
156
+ predecessor_store_sha256: str | None,
157
+ entry_count: int,
158
+ published_at: str,
159
+ code: str,
160
+ path: str,
161
+ ) -> None:
162
+ """The field rules a release statement and a recorded install state share."""
163
+
164
+ if store_kind not in SUPPORTED_STORE_KINDS:
165
+ raise _refuse(
166
+ "CATALOG_UPDATE_STORE", f"{path}.store_kind", "must name a supported store kind"
167
+ )
168
+ if not is_digest(store_sha256):
169
+ raise _refuse(code, f"{path}.store_sha256", "must be one exact lowercase SHA-256")
170
+ if predecessor_store_sha256 is not None:
171
+ if not is_digest(predecessor_store_sha256):
172
+ raise _refuse(
173
+ code,
174
+ f"{path}.predecessor_store_sha256",
175
+ "must be one exact lowercase SHA-256 or null",
176
+ )
177
+ if predecessor_store_sha256 == store_sha256:
178
+ raise _refuse(
179
+ code, f"{path}.predecessor_store_sha256", "must not name the release itself"
180
+ )
181
+ if (
182
+ not isinstance(entry_count, int)
183
+ or isinstance(entry_count, bool)
184
+ or not 0 <= entry_count <= MAX_RELEASE_ENTRY_COUNT
185
+ ):
186
+ raise _refuse(code, f"{path}.entry_count", "must be a bounded whole number")
187
+ require_canonical_timestamp(published_at, f"{path}.published_at")
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class ReleaseDescriptor:
192
+ """The one statement a release archive makes about itself."""
193
+
194
+ store_kind: str
195
+ store_sha256: str
196
+ predecessor_store_sha256: str | None
197
+ entry_count: int
198
+ published_at: str
199
+
200
+ def __post_init__(self) -> None:
201
+ _require_release_facts(
202
+ store_kind=self.store_kind,
203
+ store_sha256=self.store_sha256,
204
+ predecessor_store_sha256=self.predecessor_store_sha256,
205
+ entry_count=self.entry_count,
206
+ published_at=self.published_at,
207
+ code="CATALOG_UPDATE_RELEASE",
208
+ path="release",
209
+ )
210
+
211
+ @classmethod
212
+ def from_dict(cls, data: Any, *, path: str = "release") -> ReleaseDescriptor:
213
+ if not isinstance(data, dict):
214
+ raise _refuse("CATALOG_UPDATE_RELEASE", path, "must be one JSON object")
215
+ if set(data) != _RELEASE_DESCRIPTOR_KEYS:
216
+ raise _refuse(
217
+ "CATALOG_UPDATE_RELEASE",
218
+ path,
219
+ "must carry exactly the declared release fields",
220
+ )
221
+ if data["schema_version"] != RELEASE_DESCRIPTOR_SCHEMA:
222
+ raise _refuse(
223
+ "CATALOG_UPDATE_RELEASE",
224
+ f"{path}.schema_version",
225
+ f"must be {RELEASE_DESCRIPTOR_SCHEMA}",
226
+ )
227
+ return cls(
228
+ store_kind=data["store_kind"],
229
+ store_sha256=data["store_sha256"],
230
+ predecessor_store_sha256=data["predecessor_store_sha256"],
231
+ entry_count=data["entry_count"],
232
+ published_at=data["published_at"],
233
+ )
234
+
235
+ def to_dict(self) -> dict[str, Any]:
236
+ return {
237
+ "schema_version": RELEASE_DESCRIPTOR_SCHEMA,
238
+ "store_kind": self.store_kind,
239
+ "store_sha256": self.store_sha256,
240
+ "predecessor_store_sha256": self.predecessor_store_sha256,
241
+ "entry_count": self.entry_count,
242
+ "published_at": self.published_at,
243
+ }
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class InstalledState:
248
+ """What this computer remembers about the generation it installed."""
249
+
250
+ store_kind: str
251
+ store_sha256: str
252
+ archive_sha256: str
253
+ predecessor_store_sha256: str | None
254
+ entry_count: int
255
+ published_at: str
256
+ generation_path: str
257
+
258
+ def __post_init__(self) -> None:
259
+ _require_release_facts(
260
+ store_kind=self.store_kind,
261
+ store_sha256=self.store_sha256,
262
+ predecessor_store_sha256=self.predecessor_store_sha256,
263
+ entry_count=self.entry_count,
264
+ published_at=self.published_at,
265
+ code="CATALOG_UPDATE_STATE",
266
+ path="installed",
267
+ )
268
+ if not is_digest(self.archive_sha256):
269
+ raise _refuse(
270
+ "CATALOG_UPDATE_STATE",
271
+ "installed.archive_sha256",
272
+ "must be one exact lowercase SHA-256",
273
+ )
274
+ prefix = f"{GENERATIONS_DIRNAME}/"
275
+ folder = (
276
+ self.generation_path[len(prefix) :]
277
+ if isinstance(self.generation_path, str) and self.generation_path.startswith(prefix)
278
+ else ""
279
+ )
280
+ if not _GENERATION_FOLDER_PATTERN.fullmatch(folder):
281
+ raise _refuse(
282
+ "CATALOG_UPDATE_STATE",
283
+ "installed.generation_path",
284
+ "must name one digest-named folder directly under the generations folder",
285
+ )
286
+ if not self.store_sha256.startswith(folder):
287
+ raise _refuse(
288
+ "CATALOG_UPDATE_STATE",
289
+ "installed.generation_path",
290
+ "must be named by the recorded store digest",
291
+ )
292
+
293
+ @classmethod
294
+ def from_dict(cls, data: Any, *, path: str = "installed") -> InstalledState:
295
+ if not isinstance(data, dict) or set(data) != _INSTALLED_STATE_KEYS:
296
+ raise _refuse(
297
+ "CATALOG_UPDATE_STATE", path, "must carry exactly the recorded install fields"
298
+ )
299
+ if data["schema_version"] != INSTALLED_STATE_SCHEMA:
300
+ raise _refuse(
301
+ "CATALOG_UPDATE_STATE",
302
+ f"{path}.schema_version",
303
+ f"must be {INSTALLED_STATE_SCHEMA}",
304
+ )
305
+ return cls(
306
+ store_kind=data["store_kind"],
307
+ store_sha256=data["store_sha256"],
308
+ archive_sha256=data["archive_sha256"],
309
+ predecessor_store_sha256=data["predecessor_store_sha256"],
310
+ entry_count=data["entry_count"],
311
+ published_at=data["published_at"],
312
+ generation_path=data["generation_path"],
313
+ )
314
+
315
+ def to_dict(self) -> dict[str, Any]:
316
+ return {
317
+ "schema_version": INSTALLED_STATE_SCHEMA,
318
+ "store_kind": self.store_kind,
319
+ "store_sha256": self.store_sha256,
320
+ "archive_sha256": self.archive_sha256,
321
+ "predecessor_store_sha256": self.predecessor_store_sha256,
322
+ "entry_count": self.entry_count,
323
+ "published_at": self.published_at,
324
+ "generation_path": self.generation_path,
325
+ }
326
+
327
+
328
+ @dataclass(frozen=True)
329
+ class CatalogUpdateReport:
330
+ """The complete account of one install decision."""
331
+
332
+ status: str
333
+ state: InstalledState
334
+ catalog_root: str
335
+
336
+ def to_dict(self) -> dict[str, Any]:
337
+ generation_root = os.path.join(self.catalog_root, self.state.generation_path)
338
+ return {
339
+ "schema_version": "mr-data-catalog-update.v1",
340
+ "status": self.status,
341
+ "store_kind": self.state.store_kind,
342
+ "store_sha256": self.state.store_sha256,
343
+ "archive_sha256": self.state.archive_sha256,
344
+ "predecessor_store_sha256": self.state.predecessor_store_sha256,
345
+ "entry_count": self.state.entry_count,
346
+ "published_at": self.state.published_at,
347
+ "catalog_root": self.catalog_root,
348
+ "generation_root": generation_root,
349
+ "search": {
350
+ "public_index_root": generation_root,
351
+ "public_manifest_sha256": self.state.store_sha256,
352
+ },
353
+ }
354
+
355
+
356
+ def _screen_member_name(name: str) -> None:
357
+ """Admit one archive member name, or refuse it outright. Folder members are refused."""
358
+
359
+ if "\\" in name:
360
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", name, "member names never contain a backslash")
361
+ if name.startswith("/") or "//" in name:
362
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", name, "member names are relative and clean")
363
+ if name.endswith("/"):
364
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", name, "folder members are not admitted")
365
+ parts = name.split("/")
366
+ if len(parts) > MAX_MEMBER_NAME_PARTS:
367
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", name, "member name has too many folders")
368
+ for part in parts:
369
+ if part in {"", ".", ".."}:
370
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", name, "member name has an empty or dot part")
371
+ if len(part) > MAX_MEMBER_NAME_PART_LENGTH:
372
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", name, "member name part is too long")
373
+
374
+
375
+ def _screen_archive(
376
+ archive: zipfile.ZipFile,
377
+ ) -> tuple[zipfile.ZipInfo, tuple[zipfile.ZipInfo, ...]]:
378
+ """Screen every member, returning the release descriptor member and the payload members."""
379
+
380
+ descriptor_member: zipfile.ZipInfo | None = None
381
+ payload: list[zipfile.ZipInfo] = []
382
+ # Duplicates are judged after case folding and accent normalisation because the destination
383
+ # filesystem may apply either: two names this set cannot tell apart cannot both land.
384
+ seen: set[str] = set()
385
+ total_bytes = 0
386
+ total_compressed = 0
387
+ for info in archive.infolist():
388
+ name = info.filename
389
+ _screen_member_name(name)
390
+ folded = unicodedata.normalize("NFC", name).casefold()
391
+ if folded in seen:
392
+ raise _refuse(
393
+ "CATALOG_UPDATE_ARCHIVE",
394
+ name,
395
+ "member name appears more than once, or collides with another on a filesystem "
396
+ "that folds case or accents",
397
+ )
398
+ seen.add(folded)
399
+ try:
400
+ screen_zip_entry(info, subject=name, described="release archive member")
401
+ except ReaderError as error:
402
+ raise _refuse(
403
+ "CATALOG_UPDATE_ARCHIVE", name, "member is not one plain admissible file"
404
+ ) from error
405
+ if info.file_size > MAX_RELEASE_MEMBER_BYTES:
406
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", name, "member is larger than the bound")
407
+ total_bytes += info.file_size
408
+ total_compressed += info.compress_size
409
+ if name == RELEASE_DESCRIPTOR_FILENAME:
410
+ if info.file_size > MAX_RELEASE_DESCRIPTOR_BYTES:
411
+ raise _refuse(
412
+ "CATALOG_UPDATE_ARCHIVE", name, "release statement is larger than the bound"
413
+ )
414
+ descriptor_member = info
415
+ continue
416
+ if not name.startswith(ARCHIVE_MEMBER_PREFIX) or len(name) <= len(ARCHIVE_MEMBER_PREFIX):
417
+ raise _refuse(
418
+ "CATALOG_UPDATE_ARCHIVE",
419
+ name,
420
+ "every payload member lives under the catalog prefix",
421
+ )
422
+ payload.append(info)
423
+ if len(payload) > MAX_RELEASE_MEMBER_COUNT:
424
+ raise _refuse(
425
+ "CATALOG_UPDATE_ARCHIVE", "archive", "archive holds more members than the bound"
426
+ )
427
+ if total_bytes > MAX_RELEASE_ARCHIVE_BYTES:
428
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", "archive", "archive expands past the byte bound")
429
+ if total_bytes > max(ARCHIVE_EXPANSION_FLOOR_BYTES, total_compressed * ARCHIVE_EXPANSION_RATIO):
430
+ raise _refuse(
431
+ "CATALOG_UPDATE_ARCHIVE", "archive", "archive expands out of proportion to its size"
432
+ )
433
+ if descriptor_member is None:
434
+ raise _refuse(
435
+ "CATALOG_UPDATE_ARCHIVE", RELEASE_DESCRIPTOR_FILENAME, "release statement is missing"
436
+ )
437
+ if not payload:
438
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", "archive", "archive holds no catalog members")
439
+ return descriptor_member, tuple(payload)
440
+
441
+
442
+ def _read_member(archive: zipfile.ZipFile, info: zipfile.ZipInfo) -> bytes:
443
+ """Read one screened member, holding it to its declared size exactly.
444
+
445
+ The archive digest covers the packed bytes, not the unpacking: a member whose stream is
446
+ corrupt, or whose checksum disagrees, is a typed refusal rather than a crash.
447
+ """
448
+
449
+ try:
450
+ with archive.open(info) as handle:
451
+ return read_bounded(handle, declared=info.file_size, subject=info.filename)
452
+ except ReaderError as error:
453
+ raise _refuse(
454
+ "CATALOG_UPDATE_ARCHIVE", info.filename, "member bytes differ from the declared size"
455
+ ) from error
456
+ except (zipfile.BadZipFile, zlib.error) as error:
457
+ raise _refuse(
458
+ "CATALOG_UPDATE_ARCHIVE", info.filename, "member bytes are corrupt inside the archive"
459
+ ) from error
460
+
461
+
462
+ def verify_release_archive(
463
+ raw: bytes, *, expected_archive_sha256: str
464
+ ) -> tuple[ReleaseDescriptor, zipfile.ZipFile, tuple[zipfile.ZipInfo, ...]]:
465
+ """Verify one release archive down to its store file digest, without touching a disk."""
466
+
467
+ if not isinstance(raw, bytes):
468
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", "archive", "archive must be bytes")
469
+ if len(raw) > MAX_RELEASE_ARCHIVE_BYTES:
470
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", "archive", "archive is larger than the bound")
471
+ if not is_digest(expected_archive_sha256):
472
+ raise _refuse(
473
+ "CATALOG_UPDATE_DIGEST_SPELLING",
474
+ "expected_archive_sha256",
475
+ "must be exactly 64 lowercase hex characters",
476
+ )
477
+ if sha256_bytes(raw) != expected_archive_sha256:
478
+ raise _refuse(
479
+ "CATALOG_UPDATE_DIGEST",
480
+ "archive",
481
+ "archive bytes do not match the published SHA-256",
482
+ )
483
+ try:
484
+ archive = zipfile.ZipFile(io.BytesIO(raw))
485
+ except (zipfile.BadZipFile, ValueError) as error:
486
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", "archive", "archive is not a readable zip") from (
487
+ error
488
+ )
489
+ descriptor_member, payload = _screen_archive(archive)
490
+ try:
491
+ descriptor_data = parse_canonical_json(_read_member(archive, descriptor_member))
492
+ except CanonicalJSONError as error:
493
+ raise _refuse(
494
+ "CATALOG_UPDATE_RELEASE",
495
+ RELEASE_DESCRIPTOR_FILENAME,
496
+ "release statement is not canonical JSON",
497
+ ) from error
498
+ descriptor = ReleaseDescriptor.from_dict(descriptor_data)
499
+ store_name = ARCHIVE_MEMBER_PREFIX + _STORE_FILENAMES[descriptor.store_kind]
500
+ store_member = next((info for info in payload if info.filename == store_name), None)
501
+ if store_member is None:
502
+ raise _refuse(
503
+ "CATALOG_UPDATE_RELEASE", store_name, "the named store file is not in the archive"
504
+ )
505
+ if store_member.file_size > _STORE_FILE_MAXIMUMS[descriptor.store_kind]:
506
+ raise _refuse("CATALOG_UPDATE_RELEASE", store_name, "store file is larger than its bound")
507
+ store_raw = _read_member(archive, store_member)
508
+ if sha256_bytes(store_raw) != descriptor.store_sha256:
509
+ raise _refuse(
510
+ "CATALOG_UPDATE_RELEASE",
511
+ store_name,
512
+ "store file bytes do not match the release statement",
513
+ )
514
+ if descriptor.store_kind == "packed":
515
+ try:
516
+ head = parse_canonical_json(store_raw)
517
+ except CanonicalJSONError as error:
518
+ raise _refuse(
519
+ "CATALOG_UPDATE_RELEASE", store_name, "store file is not canonical JSON"
520
+ ) from error
521
+ head_predecessor = head.get("predecessor_head_sha256") if isinstance(head, dict) else None
522
+ if head_predecessor != descriptor.predecessor_store_sha256:
523
+ raise _refuse(
524
+ "CATALOG_UPDATE_RELEASE",
525
+ store_name,
526
+ "release statement disagrees with the store file about the predecessor",
527
+ )
528
+ return descriptor, archive, payload
529
+
530
+
531
+ def load_installed_state(root: Path) -> tuple[InstalledState, bytes] | None:
532
+ """Read the recorded install state under ``root``, or None when nothing is recorded."""
533
+
534
+ if not isinstance(root, Path):
535
+ raise _refuse("CATALOG_UPDATE_STATE", "root", "must be a pathlib.Path")
536
+ state_path = root / INSTALLED_STATE_FILENAME
537
+ if not os.path.lexists(state_path):
538
+ return None
539
+ try:
540
+ raw = read_bounded_document(
541
+ state_path, maximum=MAX_INSTALLED_STATE_BYTES, label="catalog update state"
542
+ )
543
+ except SourceContractError as error:
544
+ raise _refuse(
545
+ "CATALOG_UPDATE_STATE",
546
+ str(state_path),
547
+ "recorded install state could not be read safely",
548
+ ) from error
549
+ try:
550
+ data = parse_canonical_json(raw)
551
+ except CanonicalJSONError as error:
552
+ raise _refuse(
553
+ "CATALOG_UPDATE_STATE", str(state_path), "recorded install state is not canonical"
554
+ ) from error
555
+ return InstalledState.from_dict(data), raw
556
+
557
+
558
+ def _materialize(
559
+ archive: zipfile.ZipFile,
560
+ payload: tuple[zipfile.ZipInfo, ...],
561
+ *,
562
+ staging: Path,
563
+ ) -> None:
564
+ """Write the payload under the private staging folder, durably, without publishing it."""
565
+
566
+ os.makedirs(staging, mode=0o755)
567
+ try:
568
+ for info in sorted(payload, key=lambda member: member.filename):
569
+ relative = info.filename[len(ARCHIVE_MEMBER_PREFIX) :]
570
+ target = staging.joinpath(*relative.split("/"))
571
+ os.makedirs(target.parent, mode=0o755, exist_ok=True)
572
+ data = _read_member(archive, info)
573
+ descriptor = os.open(
574
+ target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), 0o644
575
+ )
576
+ try:
577
+ view = memoryview(data)
578
+ while view:
579
+ written = os.write(descriptor, view)
580
+ if written <= 0:
581
+ raise _refuse(
582
+ "CATALOG_UPDATE_INSTALL",
583
+ str(target),
584
+ "the destination stopped accepting bytes",
585
+ )
586
+ view = view[written:]
587
+ os.fsync(descriptor)
588
+ finally:
589
+ os.close(descriptor)
590
+ for current, _directories, _files in os.walk(staging):
591
+ dir_fd = os.open(current, os.O_RDONLY)
592
+ try:
593
+ os.fsync(dir_fd)
594
+ finally:
595
+ os.close(dir_fd)
596
+ except OSError as error:
597
+ raise _refuse(
598
+ "CATALOG_UPDATE_INSTALL", str(staging), "the generation could not be written out"
599
+ ) from error
600
+
601
+
602
+ def _publish(staging: Path, destination: Path, generations_root: Path) -> None:
603
+ """Make one fully verified staging folder visible under its final name, atomically."""
604
+
605
+ try:
606
+ os.rename(staging, destination)
607
+ root_fd = os.open(generations_root, os.O_RDONLY)
608
+ try:
609
+ os.fsync(root_fd)
610
+ finally:
611
+ os.close(root_fd)
612
+ except OSError as error:
613
+ raise _refuse(
614
+ "CATALOG_UPDATE_INSTALL", str(destination), "the generation could not be published"
615
+ ) from error
616
+
617
+
618
+ def _verify_installed_store(
619
+ root: Path, descriptor: ReleaseDescriptor, *, neural_backend: Any = None
620
+ ) -> None:
621
+ """Run the real store verification over the installed folder, exactly as a search would."""
622
+
623
+ kind = detect_public_store_kind(root)
624
+ if kind != descriptor.store_kind:
625
+ raise _refuse(
626
+ "CATALOG_UPDATE_RELEASE",
627
+ str(root),
628
+ "installed folder is not the store kind the release names",
629
+ )
630
+ if descriptor.store_kind == "packed":
631
+ verified = verify_packed_generation(root, expected_head_sha256=descriptor.store_sha256)
632
+ installed_count = verified.entry_count
633
+ else:
634
+ loaded = load_retrieval_catalog(
635
+ root,
636
+ expected_manifest_sha256=descriptor.store_sha256,
637
+ neural_backend=neural_backend,
638
+ )
639
+ installed_count = len(loaded.lexical.entries)
640
+ if installed_count != descriptor.entry_count:
641
+ raise _refuse(
642
+ "CATALOG_UPDATE_RELEASE",
643
+ str(root),
644
+ "installed entry count differs from the release statement",
645
+ )
646
+
647
+
648
+ def confirm_already_current(
649
+ root: Path, *, expected_archive_sha256: str, neural_backend: Any = None
650
+ ) -> CatalogUpdateReport | None:
651
+ """Answer whether the release named by this digest is already installed, without the archive.
652
+
653
+ The recorded state binds the archive digest it was installed from, so equality of the typed
654
+ digest is enough to know no bytes need to move; the installed folder is still re-verified
655
+ end to end before "already-current" is claimed.
656
+ """
657
+
658
+ if not is_digest(expected_archive_sha256):
659
+ raise _refuse(
660
+ "CATALOG_UPDATE_DIGEST_SPELLING",
661
+ "expected_archive_sha256",
662
+ "must be exactly 64 lowercase hex characters",
663
+ )
664
+ recorded = load_installed_state(root)
665
+ if recorded is None:
666
+ return None
667
+ state, _state_raw = recorded
668
+ if state.archive_sha256 != expected_archive_sha256:
669
+ return None
670
+ _verify_installed_store(
671
+ root / state.generation_path,
672
+ ReleaseDescriptor(
673
+ store_kind=state.store_kind,
674
+ store_sha256=state.store_sha256,
675
+ predecessor_store_sha256=state.predecessor_store_sha256,
676
+ entry_count=state.entry_count,
677
+ published_at=state.published_at,
678
+ ),
679
+ neural_backend=neural_backend,
680
+ )
681
+ return CatalogUpdateReport(status="already-current", state=state, catalog_root=str(root))
682
+
683
+
684
+ def install_release(
685
+ root: Path,
686
+ raw: bytes,
687
+ *,
688
+ expected_archive_sha256: str,
689
+ replace: bool = False,
690
+ neural_backend: Any = None,
691
+ ) -> CatalogUpdateReport:
692
+ """Verify one release archive and install it under ``root``, honouring the recorded chain."""
693
+
694
+ if not isinstance(root, Path):
695
+ raise _refuse("CATALOG_UPDATE_INSTALL", "root", "must be a pathlib.Path")
696
+ recorded = load_installed_state(root)
697
+ descriptor, archive, payload = verify_release_archive(
698
+ raw, expected_archive_sha256=expected_archive_sha256
699
+ )
700
+ if recorded is not None:
701
+ state, _state_raw = recorded
702
+ if state.store_sha256 == descriptor.store_sha256:
703
+ _verify_installed_store(
704
+ root / state.generation_path, descriptor, neural_backend=neural_backend
705
+ )
706
+ return CatalogUpdateReport(
707
+ status="already-current", state=state, catalog_root=str(root)
708
+ )
709
+ if not replace and descriptor.predecessor_store_sha256 != state.store_sha256:
710
+ raise _refuse(
711
+ "CATALOG_UPDATE_CHAIN",
712
+ str(root / INSTALLED_STATE_FILENAME),
713
+ "release does not descend from the installed generation",
714
+ )
715
+ generation_name = descriptor.store_sha256[:12]
716
+ generations_root = root / GENERATIONS_DIRNAME
717
+ os.makedirs(generations_root, exist_ok=True)
718
+ destination = generations_root / generation_name
719
+ if os.path.lexists(destination):
720
+ # The append-only design retains every generation ever installed, so the folder may be
721
+ # a complete earlier install of this same release -- re-verify and adopt it. Anything
722
+ # else wearing this name is refused, never overwritten.
723
+ try:
724
+ _verify_installed_store(destination, descriptor, neural_backend=neural_backend)
725
+ except SourceContractError as error:
726
+ raise _refuse(
727
+ "CATALOG_UPDATE_INSTALL",
728
+ str(destination),
729
+ "a folder with this generation's name is present but is not this release; "
730
+ "remove it and run the update again",
731
+ ) from error
732
+ else:
733
+ staging = generations_root / f".staging-{generation_name}"
734
+ if os.path.lexists(staging):
735
+ raise _refuse(
736
+ "CATALOG_UPDATE_INSTALL",
737
+ str(staging),
738
+ "an unfinished earlier install is in the way; remove it and run the update again",
739
+ )
740
+ _materialize(archive, payload, staging=staging)
741
+ archive.close()
742
+ _verify_installed_store(staging, descriptor, neural_backend=neural_backend)
743
+ _publish(staging, destination, generations_root)
744
+ new_state = InstalledState(
745
+ store_kind=descriptor.store_kind,
746
+ store_sha256=descriptor.store_sha256,
747
+ archive_sha256=expected_archive_sha256,
748
+ predecessor_store_sha256=descriptor.predecessor_store_sha256,
749
+ entry_count=descriptor.entry_count,
750
+ published_at=descriptor.published_at,
751
+ generation_path=f"{GENERATIONS_DIRNAME}/{generation_name}",
752
+ )
753
+ replaces: Mapping[str, Any] | None = None
754
+ if recorded is not None:
755
+ replaces = {"receipt_sha256": sha256_bytes(recorded[1])}
756
+ install_receipt(
757
+ root / INSTALLED_STATE_FILENAME,
758
+ new_state.to_dict(),
759
+ maximum=MAX_INSTALLED_STATE_BYTES,
760
+ label="catalog update state",
761
+ code_prefix="CATALOG_UPDATE",
762
+ refusal=SourceContractError,
763
+ replaces=replaces,
764
+ )
765
+ return CatalogUpdateReport(status="installed", state=new_state, catalog_root=str(root))
766
+
767
+
768
+ def _collect_generation_files(generation_root: Path) -> tuple[tuple[str, Path], ...]:
769
+ """Walk one generation folder, admitting only plain files reached without links.
770
+
771
+ Names are screened exactly as the installer will screen them -- with the archive prefix on --
772
+ so nothing packages here that an installer would refuse there, and the byte bounds are
773
+ enforced from sizes before any file is read.
774
+ """
775
+
776
+ collected: list[tuple[str, Path]] = []
777
+ total_bytes = 0
778
+ for current, directories, files in os.walk(generation_root, followlinks=False):
779
+ for directory in directories:
780
+ if stat.S_ISLNK(os.lstat(Path(current) / directory).st_mode):
781
+ raise _refuse(
782
+ "CATALOG_UPDATE_ARCHIVE",
783
+ str(Path(current) / directory),
784
+ "a folder inside the generation may not be a symbolic link",
785
+ )
786
+ directories.sort()
787
+ for name in sorted(files):
788
+ full = Path(current) / name
789
+ probe = os.lstat(full)
790
+ if not stat.S_ISREG(probe.st_mode):
791
+ raise _refuse(
792
+ "CATALOG_UPDATE_ARCHIVE", str(full), "only plain files can be released"
793
+ )
794
+ if probe.st_size > MAX_RELEASE_MEMBER_BYTES:
795
+ raise _refuse(
796
+ "CATALOG_UPDATE_ARCHIVE", str(full), "member is larger than the bound"
797
+ )
798
+ total_bytes += probe.st_size
799
+ relative = full.relative_to(generation_root).as_posix()
800
+ _screen_member_name(ARCHIVE_MEMBER_PREFIX + relative)
801
+ collected.append((relative, full))
802
+ if not collected:
803
+ raise _refuse(
804
+ "CATALOG_UPDATE_ARCHIVE", str(generation_root), "generation folder holds no files"
805
+ )
806
+ if len(collected) > MAX_RELEASE_MEMBER_COUNT:
807
+ raise _refuse(
808
+ "CATALOG_UPDATE_ARCHIVE",
809
+ str(generation_root),
810
+ "generation folder holds more files than the bound",
811
+ )
812
+ if total_bytes > MAX_RELEASE_ARCHIVE_BYTES:
813
+ raise _refuse(
814
+ "CATALOG_UPDATE_ARCHIVE",
815
+ str(generation_root),
816
+ "generation folder is larger than the release bound",
817
+ )
818
+ return tuple(sorted(collected))
819
+
820
+
821
+ def _write_archive_member(archive: zipfile.ZipFile, name: str, data: bytes) -> None:
822
+ info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
823
+ info.create_system = 3
824
+ info.external_attr = (stat.S_IFREG | 0o644) << 16
825
+ info.compress_type = zipfile.ZIP_STORED
826
+ archive.writestr(info, data)
827
+
828
+
829
+ def build_release_archive(
830
+ generation_root: Path,
831
+ *,
832
+ predecessor_store_sha256: str | None,
833
+ published_at: str,
834
+ neural_backend: Any = None,
835
+ ) -> tuple[bytes, ReleaseDescriptor]:
836
+ """Package one verified generation folder into a byte-reproducible release archive.
837
+
838
+ Every member is stored uncompressed with fixed metadata, so the archive's SHA-256 is a pure
839
+ function of the generation's bytes: the same generation packaged on any machine yields the
840
+ same published digest.
841
+ """
842
+
843
+ kind = detect_public_store_kind(generation_root)
844
+ if kind not in SUPPORTED_STORE_KINDS:
845
+ raise _refuse(
846
+ "CATALOG_UPDATE_STORE", str(generation_root), "must name a supported store kind"
847
+ )
848
+ store_raw = read_bounded_document(
849
+ generation_root / _STORE_FILENAMES[kind],
850
+ maximum=_STORE_FILE_MAXIMUMS[kind],
851
+ label="release store file",
852
+ )
853
+ store_sha256 = sha256_bytes(store_raw)
854
+ if kind == "packed":
855
+ verified = verify_packed_generation(generation_root, expected_head_sha256=store_sha256)
856
+ entry_count = verified.entry_count
857
+ head_predecessor = verified.head.get("predecessor_head_sha256")
858
+ if head_predecessor != predecessor_store_sha256:
859
+ raise _refuse(
860
+ "CATALOG_UPDATE_RELEASE",
861
+ str(generation_root),
862
+ "stated predecessor disagrees with the store file",
863
+ )
864
+ else:
865
+ loaded = load_retrieval_catalog(
866
+ generation_root,
867
+ expected_manifest_sha256=store_sha256,
868
+ neural_backend=neural_backend,
869
+ )
870
+ entry_count = len(loaded.lexical.entries)
871
+ descriptor = ReleaseDescriptor(
872
+ store_kind=kind,
873
+ store_sha256=store_sha256,
874
+ predecessor_store_sha256=predecessor_store_sha256,
875
+ entry_count=entry_count,
876
+ published_at=published_at,
877
+ )
878
+ files = _collect_generation_files(generation_root)
879
+ buffer = io.BytesIO()
880
+ with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_STORED) as archive:
881
+ # "catalog/..." sorts before "release.json", and the constant prefix preserves the
882
+ # files' own order, so writing files first then the statement is the sorted order.
883
+ for relative, full in files:
884
+ _write_archive_member(archive, ARCHIVE_MEMBER_PREFIX + relative, full.read_bytes())
885
+ _write_archive_member(
886
+ archive, RELEASE_DESCRIPTOR_FILENAME, canonical_json_bytes(descriptor.to_dict())
887
+ )
888
+ raw = buffer.getvalue()
889
+ if len(raw) > MAX_RELEASE_ARCHIVE_BYTES:
890
+ raise _refuse("CATALOG_UPDATE_ARCHIVE", "archive", "archive is larger than the bound")
891
+ return raw, descriptor