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,495 @@
1
+ """The content half of slice fetching: sidecar parsing, range planning, structural checks.
2
+
3
+ Everything here is pure and I/O-free. It parses an untrusted index sidecar into an ordered
4
+ message table, turns a recipe-named selection into exact byte ranges emitted in index order,
5
+ and proves a fetched slice is a well-formed GRIB message before anything else touches it.
6
+
7
+ The division of labour with ``acquisition.http`` is deliberate and no single part is sufficient
8
+ alone. HTTP proves the *span*: exactly 206, a ``Content-Range`` parsed strictly and equal to
9
+ what was ordered, three lengths that agree. ``check_grib_message`` proves the delivered bytes
10
+ are a whole, well-formed message rather than a shifted, truncated or spliced window of one --
11
+ the shape a wrong offset produces.
12
+
13
+ Neither proves *which object* the message came from. Two well-formed messages of equal length
14
+ are indistinguishable to both, and for GRIB2 at a fixed grid and a fixed bits-per-value, equal
15
+ length across a re-issued or superseded run is the ordinary case rather than a contrived one.
16
+ ``check_grib_reference_time`` is the control for that case: it reads Section 1's reference time
17
+ out of the delivered message and refuses anything that did not come from the run the order
18
+ named. It is in force only when the order names a run -- ``expected_reference_time`` on the
19
+ range plan -- because a slice order that names no run has nothing to be held to.
20
+
21
+ What remains uncovered, stated rather than implied, in two parts.
22
+
23
+ First, a *different object belonging to the same run* -- a neighbouring field's file at the same
24
+ cycle, of equal message length -- passes every check here. Closing that would need per-object
25
+ evidence the index sidecar does not carry.
26
+
27
+ Second, and cheaper: *the index chooses which message answers a descriptor, and nothing verifies
28
+ that pairing*. ``plan_ranges`` turns a descriptor into a byte span using nothing but the
29
+ sidecar's own descriptor->offset mapping, and no control anywhere binds a delivered message back
30
+ to the descriptor that named it. All three controls -- the span, ``check_grib_message`` and
31
+ ``check_grib_reference_time`` -- are properties of the bytes alone, so a sidecar that keeps the
32
+ object's real, strictly ascending offsets and attaches each descriptor to its neighbour's span
33
+ delivers a whole, well-formed message from the correct model run at exactly the span the index
34
+ named. The sidecar is untrusted input for its *shape*; it is trusted for the descriptor->offset
35
+ mapping, and this is where that trust is spent. Unlike the case above it needs no second file,
36
+ no mirror lag and no redirect -- only the small text ``.idx`` beside the object, which is a
37
+ separate, far cheaper cache or edge object than the binary it describes. Both are pinned by
38
+ ``known_gap`` tests in ``tests/h3/test_ranges.py`` and ``tests/h3/test_sandbox.py``.
39
+
40
+ This module is a leaf on purpose: it imports the transport's typed range and its range budget
41
+ and nothing else from the package, so the sandbox worker can import it without dragging in a
42
+ recipe, a pipeline or a source registry.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import re
48
+ from dataclasses import dataclass
49
+ from datetime import date
50
+
51
+ from mostlyright.data_harness.acquisition.http import ByteRange, require_range_plan_within_budget
52
+ from mostlyright.data_harness.acquisition.url_policy import AcquisitionSecurityError
53
+
54
+ # A message number or a byte offset, in one canonical spelling: no sign, no whitespace, no
55
+ # leading zeros, and at most sixteen digits. The same shape ``acquisition.http`` accepts in a
56
+ # ``Content-Range``, for the same reason -- one delivered span must have one spelling, and a
57
+ # sidecar must not be able to name an offset no object could have.
58
+ _SIDECAR_DECIMAL = re.compile(r"0|[1-9][0-9]{0,15}")
59
+
60
+ GRIB_MAGIC = b"GRIB"
61
+ GRIB_EDITION = 2
62
+ GRIB_EDITION_OFFSET = 7
63
+ GRIB_TRAILER = b"7777"
64
+ # Section 0 is sixteen bytes: ``GRIB``, two reserved bytes, the discipline, the edition, then
65
+ # the whole message's length as a big-endian eight-byte integer. With the four-byte trailer,
66
+ # the shortest byte string that could be a message at all is twenty bytes.
67
+ GRIB_SECTION_ZERO_BYTES = 16
68
+ GRIB_LENGTH_SLICE = slice(8, 16)
69
+ GRIB_MINIMUM_BYTES = GRIB_SECTION_ZERO_BYTES + len(GRIB_TRAILER)
70
+
71
+ # Section 1, the identification section, begins where Section 0 ends and is at least twenty-one
72
+ # octets. Its octets are numbered from one, so octet ``n`` sits at index ``15 + n`` of the
73
+ # message. Only the four fields below are read and nothing here decodes: the reference time,
74
+ # and the significance byte that says what the reference time means.
75
+ GRIB_SECTION_ONE_OFFSET = GRIB_SECTION_ZERO_BYTES
76
+ GRIB_SECTION_ONE_MINIMUM_BYTES = 21
77
+ GRIB_SECTION_ONE_NUMBER = 1
78
+ GRIB_SECTION_ONE_LENGTH_SLICE = slice(16, 20)
79
+ GRIB_SECTION_ONE_NUMBER_OFFSET = 20
80
+ # Code table 1.2. Value 1 is "start of forecast", which is the model run itself. Any other
81
+ # significance names a different instant -- a verifying time, an observation time -- and is
82
+ # refused rather than compared against a cycle it does not denote.
83
+ GRIB_REFERENCE_SIGNIFICANCE_OFFSET = 27
84
+ GRIB_REFERENCE_START_OF_FORECAST = 1
85
+ GRIB_REFERENCE_YEAR_SLICE = slice(28, 30)
86
+ GRIB_REFERENCE_MONTH_OFFSET = 30
87
+ GRIB_REFERENCE_DAY_OFFSET = 31
88
+ GRIB_REFERENCE_HOUR_OFFSET = 32
89
+ GRIB_REFERENCE_MINUTE_OFFSET = 33
90
+ GRIB_REFERENCE_SECOND_OFFSET = 34
91
+
92
+ # One spelling for a model run, everywhere it crosses a boundary: a UTC instant to the second,
93
+ # zero-padded, with the literal ``Z``. The pattern is the shape; ``date`` below is what rejects
94
+ # a shape-valid instant that is not a day, so ``2026-02-30T00:00:00Z`` cannot be ordered.
95
+ _REFERENCE_TIME_SPELLING = re.compile(
96
+ r"([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z"
97
+ )
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class SidecarLimits:
102
+ """Coordinator-owned ceilings for parsing one index sidecar.
103
+
104
+ A real NOAA ``.idx`` for a ~518 MiB GFS file is a few hundred lines of a few hundred bytes.
105
+ The defaults leave that an order of magnitude of headroom and refuse anything past it, so a
106
+ pathological or hostile sidecar is bounded before it is looked at rather than after.
107
+ """
108
+
109
+ max_bytes: int = 1024 * 1024
110
+ max_lines: int = 4096
111
+ max_line_bytes: int = 512
112
+
113
+ def __post_init__(self) -> None:
114
+ bounded = (
115
+ ("max_bytes", self.max_bytes, 1, 16 * 1024 * 1024),
116
+ ("max_lines", self.max_lines, 1, 65_536),
117
+ ("max_line_bytes", self.max_line_bytes, 1, 8192),
118
+ )
119
+ for name, value, minimum, maximum in bounded:
120
+ if type(value) is not int or not minimum <= value <= maximum:
121
+ raise AcquisitionSecurityError(
122
+ "SIDECAR_LIMIT",
123
+ f"{name} must be an integer in [{minimum}, {maximum}]",
124
+ )
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class SidecarMessage:
129
+ """One line of an index sidecar, after parsing: where a message starts and what it is.
130
+
131
+ A sidecar value is never allowed to become a length, an allocation size or a URL. The
132
+ offset is a proposal about an object, checked against what actually arrives.
133
+ """
134
+
135
+ index: int
136
+ offset: int
137
+ descriptor: str
138
+
139
+ def __post_init__(self) -> None:
140
+ if type(self.index) is not int or self.index < 1:
141
+ raise AcquisitionSecurityError(
142
+ "SIDECAR_MESSAGE",
143
+ "message index must be an integer of at least 1",
144
+ )
145
+ if type(self.offset) is not int or self.offset < 0:
146
+ raise AcquisitionSecurityError(
147
+ "SIDECAR_MESSAGE",
148
+ "message offset must be a non-negative integer",
149
+ )
150
+ if not isinstance(self.descriptor, str):
151
+ raise AcquisitionSecurityError(
152
+ "SIDECAR_MESSAGE",
153
+ "message descriptor must be text",
154
+ )
155
+
156
+
157
+ def parse_index_sidecar(content: bytes, *, limits: SidecarLimits) -> tuple[SidecarMessage, ...]:
158
+ """Parse an untrusted ``.idx`` sidecar into an ordered message table, or refuse it.
159
+
160
+ The sidecar is untrusted input and is treated as such throughout: it is bounded before it
161
+ is decoded, decoded strictly, and accepted only in one closed shape. Message numbers must
162
+ run exactly ``1, 2, 3, ...`` and offsets must ascend strictly. Strict ascent is what makes
163
+ each message's end computable -- a message ends where the next one begins -- and what stops
164
+ a crafted sidecar from proposing an inverted or overlapping span.
165
+
166
+ No refusal here quotes the sidecar's own text. A refusal crosses the sandbox boundary and
167
+ must not become a channel for whatever the fetched bytes happened to contain.
168
+ """
169
+
170
+ if not isinstance(content, bytes):
171
+ raise AcquisitionSecurityError("SIDECAR_INPUT", "sidecar input must be exact bytes")
172
+ if len(content) > limits.max_bytes:
173
+ raise AcquisitionSecurityError(
174
+ "SIDECAR_BYTES",
175
+ f"sidecar exceeds the {limits.max_bytes} byte budget",
176
+ )
177
+ try:
178
+ text = content.decode("ascii", errors="strict")
179
+ except UnicodeDecodeError:
180
+ raise AcquisitionSecurityError("SIDECAR_ENCODING", "sidecar must be strict ASCII") from None
181
+
182
+ lines = text.split("\n")
183
+ # Exactly one trailing empty line is a line terminator on the final record. Every other
184
+ # blank line is a malformed record and falls through to the field check below.
185
+ if lines and lines[-1] == "":
186
+ lines = lines[:-1]
187
+ if not lines:
188
+ raise AcquisitionSecurityError("SIDECAR_EMPTY", "sidecar names no messages")
189
+ if len(lines) > limits.max_lines:
190
+ raise AcquisitionSecurityError(
191
+ "SIDECAR_LINES",
192
+ f"sidecar exceeds the {limits.max_lines} line budget",
193
+ )
194
+
195
+ messages: list[SidecarMessage] = []
196
+ for line_number, line in enumerate(lines, start=1):
197
+ if len(line) > limits.max_line_bytes:
198
+ raise AcquisitionSecurityError(
199
+ "SIDECAR_LINE",
200
+ f"sidecar line {line_number} exceeds the {limits.max_line_bytes} byte budget",
201
+ )
202
+ fields = line.split(":")
203
+ if len(fields) < 3:
204
+ raise AcquisitionSecurityError(
205
+ "SIDECAR_FIELD",
206
+ f"sidecar line {line_number} has fewer than three fields",
207
+ )
208
+ if not _SIDECAR_DECIMAL.fullmatch(fields[0]) or not _SIDECAR_DECIMAL.fullmatch(fields[1]):
209
+ raise AcquisitionSecurityError(
210
+ "SIDECAR_FIELD",
211
+ f"sidecar line {line_number} does not begin with a plain number and offset",
212
+ )
213
+ index = int(fields[0])
214
+ offset = int(fields[1])
215
+ if index != line_number:
216
+ raise AcquisitionSecurityError(
217
+ "SIDECAR_ORDER",
218
+ f"sidecar line {line_number} is numbered {index}, breaking the message sequence",
219
+ )
220
+ if messages and offset <= messages[-1].offset:
221
+ raise AcquisitionSecurityError(
222
+ "SIDECAR_ORDER",
223
+ f"sidecar line {line_number} does not advance past the previous offset",
224
+ )
225
+ descriptor = ":".join(fields[2:])
226
+ if descriptor.endswith(":"):
227
+ descriptor = descriptor[:-1]
228
+ messages.append(SidecarMessage(index=index, offset=offset, descriptor=descriptor))
229
+
230
+ return tuple(messages)
231
+
232
+
233
+ @dataclass(frozen=True)
234
+ class PlannedRange:
235
+ """One message the acquisition ordered: which message it is and exactly which bytes."""
236
+
237
+ index: int
238
+ descriptor: str
239
+ byte_range: ByteRange
240
+
241
+ def __post_init__(self) -> None:
242
+ if type(self.index) is not int or self.index < 1:
243
+ raise AcquisitionSecurityError(
244
+ "PLANNED_RANGE",
245
+ "planned message index must be an integer of at least 1",
246
+ )
247
+ if not isinstance(self.descriptor, str):
248
+ raise AcquisitionSecurityError("PLANNED_RANGE", "planned descriptor must be text")
249
+ if not isinstance(self.byte_range, ByteRange):
250
+ raise AcquisitionSecurityError(
251
+ "PLANNED_RANGE",
252
+ "a planned range must carry the transport's own byte range type",
253
+ )
254
+
255
+
256
+ def plan_ranges(
257
+ messages: tuple[SidecarMessage, ...],
258
+ *,
259
+ selectors: tuple[str, ...],
260
+ max_ranges: int,
261
+ ) -> tuple[PlannedRange, ...]:
262
+ """Turn a recipe-named selection into exact byte ranges, emitted in message-index order.
263
+
264
+ Matching is exact string equality against ``SidecarMessage.descriptor``. There is no
265
+ substring match, no glob and no regular expression: a selector that matched by prefix would
266
+ silently pick up a different variable the day a producer adds a level to its descriptors,
267
+ and the acquisition would keep succeeding while fetching the wrong field.
268
+
269
+ The result is sorted by message index before it is returned, and that sort is what makes the
270
+ sealed digest reproducible. Slices may be fetched with any concurrency, so responses arrive
271
+ in whatever order the network produced; emitting in arrival order would make the receipt a
272
+ function of network timing rather than of the recipe.
273
+
274
+ Selecting the last message in the sidecar is refused. Its end is the end of the object and
275
+ this module does not learn object lengths -- a guessed end is exactly the "correct span,
276
+ wrong bytes" failure the structural check below exists to catch, and guessing it here would
277
+ manufacture that failure rather than detect it.
278
+
279
+ No refusal quotes a selector or a descriptor: both are text that came from outside.
280
+ """
281
+
282
+ if not isinstance(messages, tuple) or not messages:
283
+ raise AcquisitionSecurityError(
284
+ "SIDECAR_EMPTY",
285
+ "a range plan needs a non-empty parsed message table",
286
+ )
287
+ if not all(isinstance(message, SidecarMessage) for message in messages):
288
+ raise AcquisitionSecurityError(
289
+ "SIDECAR_MESSAGE",
290
+ "a range plan may only be built from parsed sidecar messages",
291
+ )
292
+ if not isinstance(selectors, tuple) or not all(
293
+ isinstance(selector, str) for selector in selectors
294
+ ):
295
+ raise AcquisitionSecurityError("SELECTOR_TYPE", "selectors must be a tuple of text")
296
+
297
+ # Bound the plan before doing any of its work, and bound it in exactly one place: the
298
+ # transport owns the range budget, so this module calls it rather than restating it.
299
+ require_range_plan_within_budget(len(selectors), max_ranges=max_ranges)
300
+
301
+ if len(set(selectors)) != len(selectors):
302
+ raise AcquisitionSecurityError(
303
+ "SELECTOR_DUPLICATE",
304
+ "a selector is named more than once; a repeated order is not a plan",
305
+ )
306
+
307
+ by_descriptor: dict[str, list[int]] = {}
308
+ for position, message in enumerate(messages):
309
+ by_descriptor.setdefault(message.descriptor, []).append(position)
310
+
311
+ planned: list[PlannedRange] = []
312
+ for selector in selectors:
313
+ positions = by_descriptor.get(selector, [])
314
+ if not positions:
315
+ raise AcquisitionSecurityError(
316
+ "SELECTOR_MISS",
317
+ "a selector names no message in this sidecar",
318
+ )
319
+ if len(positions) > 1:
320
+ raise AcquisitionSecurityError(
321
+ "SELECTOR_AMBIGUOUS",
322
+ f"a selector names {len(positions)} messages in this sidecar",
323
+ )
324
+ position = positions[0]
325
+ if position == len(messages) - 1:
326
+ raise AcquisitionSecurityError(
327
+ "SELECTOR_TERMINAL",
328
+ "the last message in a sidecar has no computable end and is not selectable",
329
+ )
330
+ message = messages[position]
331
+ planned.append(
332
+ PlannedRange(
333
+ index=message.index,
334
+ descriptor=message.descriptor,
335
+ byte_range=ByteRange(
336
+ first_byte=message.offset,
337
+ last_byte=messages[position + 1].offset - 1,
338
+ ),
339
+ )
340
+ )
341
+
342
+ return tuple(sorted(planned, key=lambda entry: entry.index))
343
+
344
+
345
+ def check_grib_message(payload: bytes) -> None:
346
+ """Prove a fetched slice is one whole, well-formed GRIB message, or refuse it.
347
+
348
+ The division of labour, stated plainly and no wider than it is. ``acquisition.http``
349
+ establishes that the server answered 206 with a ``Content-Range`` equal to what was ordered
350
+ and that three lengths agree; that proves the *span*. This function proves the *framing*:
351
+ a message carries its own magic, its own edition, its own self-declared length and its own
352
+ trailer, so a window taken at the wrong offset, a window one message short or long, and a
353
+ body that is not a message at all each fail at least one of the four.
354
+
355
+ What this function does **not** prove is which object the bytes came from. Every check
356
+ below is a property of the message alone, so two well-formed messages of equal length are
357
+ indistinguishable here however different their contents -- which is exactly the shape a
358
+ stale cache, a re-issued file or a mirror running behind produces. The control for that is
359
+ ``check_grib_reference_time``, and it is in force only when the order named a model run.
360
+
361
+ Nothing here decodes. Section 1 and beyond, the grid and packing allowlist and the
362
+ known-good samples belong to the decoding family.
363
+
364
+ A refusal names the check that failed and never the payload. These bytes are untrusted and
365
+ the refusal crosses the sandbox boundary, so it must not become a channel out.
366
+ """
367
+
368
+ if not isinstance(payload, bytes):
369
+ raise AcquisitionSecurityError("GRIB_PAYLOAD", "a slice must be checked as exact bytes")
370
+ # Length first, so no comparison below can index past the end of a short payload.
371
+ if len(payload) < GRIB_MINIMUM_BYTES:
372
+ raise AcquisitionSecurityError(
373
+ "GRIB_TRUNCATED",
374
+ f"a GRIB message is at least {GRIB_MINIMUM_BYTES} bytes",
375
+ )
376
+ if payload[: len(GRIB_MAGIC)] != GRIB_MAGIC:
377
+ raise AcquisitionSecurityError("GRIB_MAGIC", "the slice does not begin with GRIB")
378
+ if payload[GRIB_EDITION_OFFSET] != GRIB_EDITION:
379
+ raise AcquisitionSecurityError(
380
+ "GRIB_EDITION",
381
+ f"only GRIB edition {GRIB_EDITION} is admitted",
382
+ )
383
+ if int.from_bytes(payload[GRIB_LENGTH_SLICE], "big") != len(payload):
384
+ raise AcquisitionSecurityError(
385
+ "GRIB_LENGTH",
386
+ "the message's self-declared length is not the number of bytes fetched",
387
+ )
388
+ if payload[-len(GRIB_TRAILER) :] != GRIB_TRAILER:
389
+ raise AcquisitionSecurityError("GRIB_TRAILER", "the slice does not end with the trailer")
390
+
391
+
392
+ def require_reference_time_spelling(value: str) -> str:
393
+ """Return a model-run instant in the one spelling this package accepts, or refuse it.
394
+
395
+ Shared by the coordinator that builds an order and the worker that parses one, so a run can
396
+ have only one spelling on either side of the boundary. The refusal never quotes the value:
397
+ it is text that came from outside this process and the refusal crosses the sandbox boundary.
398
+ """
399
+
400
+ if not isinstance(value, str):
401
+ raise AcquisitionSecurityError(
402
+ "REFERENCE_TIME",
403
+ "a model run must be named as text",
404
+ )
405
+ matched = _REFERENCE_TIME_SPELLING.fullmatch(value)
406
+ if matched is None:
407
+ raise AcquisitionSecurityError(
408
+ "REFERENCE_TIME",
409
+ "a model run must be spelled YYYY-MM-DDTHH:MM:SSZ, in UTC, zero-padded",
410
+ )
411
+ year, month, day = (int(part) for part in matched.group(1, 2, 3))
412
+ hour, minute, second = (int(part) for part in matched.group(4, 5, 6))
413
+ try:
414
+ date(year, month, day)
415
+ except ValueError:
416
+ raise AcquisitionSecurityError(
417
+ "REFERENCE_TIME",
418
+ "a model run must name a real calendar day",
419
+ ) from None
420
+ if hour > 23 or minute > 59 or second > 59:
421
+ raise AcquisitionSecurityError(
422
+ "REFERENCE_TIME",
423
+ "a model run must name a real time of day",
424
+ )
425
+ return value
426
+
427
+
428
+ def check_grib_reference_time(payload: bytes, *, expected: str) -> None:
429
+ """Prove a delivered message came from the model run that was ordered, or refuse it.
430
+
431
+ This is the control ``check_grib_message`` cannot be: every check there is a property of
432
+ the message alone, so a re-issued run, a stale cache or a mirror running behind can answer
433
+ the ordered span with a well-formed message of the same length from a *different* run and
434
+ pass. Section 1 carries the reference time, and for a forecast product that reference time
435
+ is the run -- so a message from another run says so about itself, in its own bytes.
436
+
437
+ Read, never decoded, and read defensively: ``check_grib_message`` runs first and has already
438
+ established framing, but the section header is re-measured here so a message whose Section 1
439
+ is shorter than its fields cannot index past its own end.
440
+
441
+ The significance byte is required to say "start of forecast". A message whose reference
442
+ time means a verifying time or an observation time is refused rather than compared against a
443
+ cycle it does not denote -- an approximate comparison here is worse than none, because it
444
+ would report a run binding that is not one.
445
+
446
+ Scope, stated so it is not read wider: this binds a slice to a *run*, not to an object. A
447
+ different object belonging to the same run still passes.
448
+
449
+ A refusal names the check that failed and quotes neither the payload nor the expected run.
450
+ """
451
+
452
+ expected = require_reference_time_spelling(expected)
453
+ if not isinstance(payload, bytes):
454
+ raise AcquisitionSecurityError("GRIB_PAYLOAD", "a slice must be checked as exact bytes")
455
+ if len(payload) < GRIB_SECTION_ONE_OFFSET + GRIB_SECTION_ONE_MINIMUM_BYTES + len(GRIB_TRAILER):
456
+ raise AcquisitionSecurityError(
457
+ "GRIB_SECTION_ONE",
458
+ "the slice is too short to carry an identification section",
459
+ )
460
+ section_length = int.from_bytes(payload[GRIB_SECTION_ONE_LENGTH_SLICE], "big")
461
+ if section_length < GRIB_SECTION_ONE_MINIMUM_BYTES:
462
+ raise AcquisitionSecurityError(
463
+ "GRIB_SECTION_ONE",
464
+ f"an identification section is at least {GRIB_SECTION_ONE_MINIMUM_BYTES} bytes",
465
+ )
466
+ if GRIB_SECTION_ONE_OFFSET + section_length > len(payload) - len(GRIB_TRAILER):
467
+ raise AcquisitionSecurityError(
468
+ "GRIB_SECTION_ONE",
469
+ "the identification section declares more bytes than the message holds",
470
+ )
471
+ if payload[GRIB_SECTION_ONE_NUMBER_OFFSET] != GRIB_SECTION_ONE_NUMBER:
472
+ raise AcquisitionSecurityError(
473
+ "GRIB_SECTION_ONE",
474
+ f"the section after the header is not section {GRIB_SECTION_ONE_NUMBER}",
475
+ )
476
+ if payload[GRIB_REFERENCE_SIGNIFICANCE_OFFSET] != GRIB_REFERENCE_START_OF_FORECAST:
477
+ raise AcquisitionSecurityError(
478
+ "GRIB_REFERENCE_SIGNIFICANCE",
479
+ "the message's reference time does not denote the start of a forecast run",
480
+ )
481
+ # Rendered into the one spelling and compared as text rather than built into an instant: a
482
+ # hostile month of 13 or day of 99 must fail this comparison, not raise out of it.
483
+ delivered = "{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}Z".format(
484
+ int.from_bytes(payload[GRIB_REFERENCE_YEAR_SLICE], "big"),
485
+ payload[GRIB_REFERENCE_MONTH_OFFSET],
486
+ payload[GRIB_REFERENCE_DAY_OFFSET],
487
+ payload[GRIB_REFERENCE_HOUR_OFFSET],
488
+ payload[GRIB_REFERENCE_MINUTE_OFFSET],
489
+ payload[GRIB_REFERENCE_SECOND_OFFSET],
490
+ )
491
+ if delivered != expected:
492
+ raise AcquisitionSecurityError(
493
+ "GRIB_REFERENCE_TIME",
494
+ "the delivered message belongs to a model run other than the one ordered",
495
+ )