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.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
"""Admission for weather-model messages: the bounded header walk and the closed pair allowlist.
|
|
2
|
+
|
|
3
|
+
This module decides whether a GRIB2 message may be opened at all, and it decides it without
|
|
4
|
+
reading a single packed value. That is possible because a GRIB2 message states its geometry
|
|
5
|
+
and its packing method in its headers, ahead of the data, so both decisions are available for
|
|
6
|
+
the price of a few hundred bytes. It is also necessary, because the expensive part of a
|
|
7
|
+
weather Reader is not the arithmetic -- it is knowing which files to refuse.
|
|
8
|
+
|
|
9
|
+
What a message is made of
|
|
10
|
+
-------------------------
|
|
11
|
+
A message is a chain of numbered sections between a sixteen-byte indicator and a four-byte
|
|
12
|
+
trailer. Six of them matter here.
|
|
13
|
+
|
|
14
|
+
* **Section 0**, the indicator, carries the magic ``GRIB``, the discipline, the edition, and
|
|
15
|
+
the message's own total length. Edition 2 only; edition 1 is a different format wearing the
|
|
16
|
+
same first four bytes.
|
|
17
|
+
* **Section 1** identifies the producing centre and the reference time. Nothing here reads it;
|
|
18
|
+
its span is recorded so the weather family can read the reference time without a second walk.
|
|
19
|
+
* **Section 2** is optional local use, and is walked over.
|
|
20
|
+
* **Section 3** defines the grid: how the values are laid out on the earth. It carries the
|
|
21
|
+
number of data points the grid holds and the grid definition template number, and those two
|
|
22
|
+
numbers are the whole of this module's interest in it.
|
|
23
|
+
* **Section 4** describes the product -- which variable, at which level, valid at what time.
|
|
24
|
+
Nothing here reads it either: which variable a message carries is not an admission question.
|
|
25
|
+
Its span is recorded for the same reason section 1's is.
|
|
26
|
+
* **Section 5** describes the packing: how the values were compressed into bits. It carries
|
|
27
|
+
the packing template number, the reference value, the binary and decimal scale factors, and
|
|
28
|
+
the bit width.
|
|
29
|
+
* **Section 6** is the bitmap, which says whether some points have no value.
|
|
30
|
+
* **Section 7** holds the packed values. **This module never reads it.** It records where it
|
|
31
|
+
begins and where it ends, and stops.
|
|
32
|
+
* **Section 8** is the four-byte trailer ``7777`` at the declared end.
|
|
33
|
+
|
|
34
|
+
Why the allowlist is a set of pairs
|
|
35
|
+
-----------------------------------
|
|
36
|
+
``ADMITTED_TEMPLATE_PAIRS`` holds ``(grid template number, packing template number)`` tuples
|
|
37
|
+
and is not two independent sets, because the risk is combinatorial. A grid we can locate and
|
|
38
|
+
a packing we can unpack are each necessary and neither is sufficient: unpacking values
|
|
39
|
+
correctly and then placing them on a grid we have never checked produces plausible numbers at
|
|
40
|
+
wrong coordinates, which is the worst failure this Reader has available to it. So the unit of
|
|
41
|
+
approval is the combination, and every pair on the list was read out of a real message that is
|
|
42
|
+
vendored in ``../messages``. A pair nobody has a message for is not on the list, and that
|
|
43
|
+
rule is what stops the list growing by optimism.
|
|
44
|
+
|
|
45
|
+
The allowlist is narrower than the decoder. Complex packing without spatial differencing (5.2)
|
|
46
|
+
and run-length packing (5.200) are not admitted because no vendored sample declares them.
|
|
47
|
+
|
|
48
|
+
Why nothing here decodes
|
|
49
|
+
------------------------
|
|
50
|
+
``admit`` returns the offsets of the data section and never reads inside them, so "refused
|
|
51
|
+
before decode" is a property of the shape of the code rather than of the order of its
|
|
52
|
+
statements. The decoder takes a ``MessageAdmission`` as its argument. A
|
|
53
|
+
message nobody admitted therefore has no way to reach a decoder: it is not that we remember to
|
|
54
|
+
check, it is that there is nothing to pass.
|
|
55
|
+
|
|
56
|
+
The walk is bounded twice over
|
|
57
|
+
------------------------------
|
|
58
|
+
Section lengths are attacker-controlled integers, and a length of zero is the classic
|
|
59
|
+
unbounded walk. Every length is validated against the bytes remaining before it is followed:
|
|
60
|
+
a length too small to hold its own five-byte header is refused, and so is one that runs past
|
|
61
|
+
the declared end. Section numbers must also strictly ascend, which both refuses a multi-field
|
|
62
|
+
message -- out of scope for this Reader -- and caps the walk at seven steps independently of
|
|
63
|
+
the length arithmetic. Two bounds, either of which alone would be sufficient, because this is
|
|
64
|
+
the loop an attacker most wants to own.
|
|
65
|
+
|
|
66
|
+
The declared-geometry budget
|
|
67
|
+
----------------------------
|
|
68
|
+
A message can declare a grid of two million points in under two hundred bytes: set the bit
|
|
69
|
+
width to zero and every value is the reference value. One of the vendored messages does
|
|
70
|
+
exactly that, and it is a real operational product rather than a contrivance. So the ceiling
|
|
71
|
+
that matters is on the **declared** point count, checked against ``max_declared_cells`` from
|
|
72
|
+
the Reader budgets, and it is checked here where the declaration is read. A budget expressed
|
|
73
|
+
as a ratio of output bytes to input bytes cannot see this class at all.
|
|
74
|
+
|
|
75
|
+
Lossy codecs are not an allowlist matter
|
|
76
|
+
----------------------------------------
|
|
77
|
+
JPEG 2000 packing is refused with its own wording and its own code, deliberately not as an
|
|
78
|
+
unknown template. Adding it to the allowlist does not bypass the refusal.
|
|
79
|
+
|
|
80
|
+
That refusal is the outer of two independent layers. The inner one is that the vendored
|
|
81
|
+
decoder is compiled with its JPEG 2000 feature disabled, so the codec is absent from the binary.
|
|
82
|
+
|
|
83
|
+
Import discipline: ``readers.contracts`` and the standard library. Nothing from
|
|
84
|
+
``acquisition``, ``sources``, ``recipe``, or ``pipeline``.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
from __future__ import annotations
|
|
88
|
+
|
|
89
|
+
import struct
|
|
90
|
+
from collections.abc import Mapping
|
|
91
|
+
from dataclasses import dataclass
|
|
92
|
+
from typing import NoReturn
|
|
93
|
+
|
|
94
|
+
from mostlyright.data_harness.readers.contracts import ReaderBudgets, ReaderError
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"ADMISSION_ALLOWLIST_VERSION",
|
|
98
|
+
"ADMITTED_TEMPLATE_PAIRS",
|
|
99
|
+
"ALLOWLIST_CONSTANT",
|
|
100
|
+
"ALLOWLIST_VERSION_CONSTANT",
|
|
101
|
+
"LOSSY_PACKING_TEMPLATES",
|
|
102
|
+
"MessageAdmission",
|
|
103
|
+
"admit",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# The closed allowlist. Every pair here was read out of a message vendored in ``../messages``:
|
|
107
|
+
#
|
|
108
|
+
# (3.0, 5.3) GFS at one degree, regular latitude/longitude, complex packing with spatial
|
|
109
|
+
# differencing
|
|
110
|
+
# (3.30, 5.0) HRRR categorical rain, Lambert conformal, simple packing
|
|
111
|
+
# (3.30, 5.3) HRRR 2 m temperature, Lambert conformal, complex packing with spatial
|
|
112
|
+
# differencing -- the flagship's own field
|
|
113
|
+
#
|
|
114
|
+
# Note what is absent and note that it is absent on purpose: (3.0, 5.0) is every member of the
|
|
115
|
+
# list recombined, and it is refused, because a combination is admitted by evidence and not by
|
|
116
|
+
# arithmetic on the members.
|
|
117
|
+
ADMITTED_TEMPLATE_PAIRS: frozenset[tuple[int, int]] = frozenset(
|
|
118
|
+
{
|
|
119
|
+
(0, 3),
|
|
120
|
+
(30, 0),
|
|
121
|
+
(30, 3),
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# The name a refusal tells an operator to edit. Held as a constant so the message and the
|
|
126
|
+
# thing it names cannot drift apart under a rename.
|
|
127
|
+
ALLOWLIST_CONSTANT = "ADMITTED_TEMPLATE_PAIRS in readers/grib2/admission.py"
|
|
128
|
+
|
|
129
|
+
# Which allowlist a recipe was written against. **Bump this whenever the pair set above
|
|
130
|
+
# changes**, in the same edit, because a recipe seals this number and a Reader that has learned a
|
|
131
|
+
# new template refuses a recipe pinned to an older one.
|
|
132
|
+
#
|
|
133
|
+
# The reason is the direction the refusals above do not cover. Those handle a message this
|
|
134
|
+
# product has never checked. This handles the opposite case: the recipe was frozen when the list
|
|
135
|
+
# held three pairs, the Toolbox has since learned a fourth, and the same recipe would now decode
|
|
136
|
+
# a file it would previously have refused. That is a change in what the recipe *means*, arriving
|
|
137
|
+
# with no change to the recipe, which is exactly what a sealed recipe exists to prevent. So the
|
|
138
|
+
# recipe states the version it was written against and a mismatch halts, naming both.
|
|
139
|
+
#
|
|
140
|
+
# Held to the pair set by test: ``tests/h3/test_readers_grib2.py`` pins the version and the pairs
|
|
141
|
+
# together, so widening one without the other fails.
|
|
142
|
+
ADMISSION_ALLOWLIST_VERSION = 1
|
|
143
|
+
|
|
144
|
+
# The name a version-mismatch refusal tells an operator to read, carrying the current value so a
|
|
145
|
+
# person reading the message knows what this build admits without opening the source.
|
|
146
|
+
ALLOWLIST_VERSION_CONSTANT = (
|
|
147
|
+
f"ADMISSION_ALLOWLIST_VERSION, which is {ADMISSION_ALLOWLIST_VERSION} in this build, "
|
|
148
|
+
f"beside {ALLOWLIST_CONSTANT}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# JPEG 2000 code-stream identifiers are always refused, independently of the allowlist.
|
|
152
|
+
LOSSY_PACKING_TEMPLATES: frozenset[int] = frozenset({40, 40000})
|
|
153
|
+
|
|
154
|
+
_MAGIC = b"GRIB"
|
|
155
|
+
_TRAILER = b"7777"
|
|
156
|
+
_EDITION = 2
|
|
157
|
+
_INDICATOR_LENGTH = 16
|
|
158
|
+
_TRAILER_LENGTH = 4
|
|
159
|
+
_SECTION_HEADER_LENGTH = 5
|
|
160
|
+
|
|
161
|
+
# The sections edition 2 defines between the indicator and the trailer, and the ones a
|
|
162
|
+
# single-field message must carry. Section 2 is local use and optional; the rest are not.
|
|
163
|
+
_SECTION_NUMBERS = frozenset({1, 2, 3, 4, 5, 6, 7})
|
|
164
|
+
_REQUIRED_SECTIONS = (1, 3, 4, 5, 6, 7)
|
|
165
|
+
|
|
166
|
+
# Bitmap indicators this module understands. 255 means every point has a value; 0 means the
|
|
167
|
+
# bitmap is carried in section 6 itself. 254 means "the bitmap defined in an earlier message",
|
|
168
|
+
# which cannot be resolved when exactly one message is admitted at a time, and 1 to 253 are
|
|
169
|
+
# reserved and therefore mean nothing.
|
|
170
|
+
_NO_BITMAP = 255
|
|
171
|
+
_BITMAP_PRESENT = 0
|
|
172
|
+
|
|
173
|
+
# Packing octets 12 to 21 are shared by the simple and complex families and by the codec
|
|
174
|
+
# templates, which is why they can be read before knowing which of them applies.
|
|
175
|
+
_PACKING_PARAMETERS_END = 21
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True)
|
|
179
|
+
class MessageAdmission:
|
|
180
|
+
"""What one admitted message declares about itself, and where its values sit.
|
|
181
|
+
|
|
182
|
+
Every field is a number. There are deliberately no bytes on this record: it locates the
|
|
183
|
+
data section rather than carrying it, so a caller that wants the values has to go back to
|
|
184
|
+
the message with these offsets in hand, and the act of doing so is visible.
|
|
185
|
+
|
|
186
|
+
The grid section is located the same way and for the same reason. This module reads two
|
|
187
|
+
numbers out of it -- the point count and the template number -- because those two are the
|
|
188
|
+
whole of admission's interest in the geometry; the projection parameters are read by
|
|
189
|
+
``geometry`` from an admitted message, and it locates them from ``grid_start`` and
|
|
190
|
+
``grid_length`` rather than by walking the sections a second time. One walk, bounded once,
|
|
191
|
+
in the module that refuses.
|
|
192
|
+
|
|
193
|
+
The identification and product sections are located for the same reason and read by nobody
|
|
194
|
+
here. Which variable a message carries, at which level, and at which time are not admission
|
|
195
|
+
questions -- admission refuses on geometry and packing -- but they are questions the weather
|
|
196
|
+
family must answer, and answering them by walking the sections a second time would put a
|
|
197
|
+
second bounded walk in a module that does not own one.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
edition: int
|
|
201
|
+
discipline: int
|
|
202
|
+
total_length: int
|
|
203
|
+
grid_template: int
|
|
204
|
+
packing_template: int
|
|
205
|
+
declared_points: int
|
|
206
|
+
packed_points: int
|
|
207
|
+
reference_value: float
|
|
208
|
+
binary_scale: int
|
|
209
|
+
decimal_scale: int
|
|
210
|
+
bits_per_value: int
|
|
211
|
+
original_field_type: int
|
|
212
|
+
bitmap_indicator: int
|
|
213
|
+
identification_start: int
|
|
214
|
+
identification_length: int
|
|
215
|
+
grid_start: int
|
|
216
|
+
grid_length: int
|
|
217
|
+
product_start: int
|
|
218
|
+
product_length: int
|
|
219
|
+
data_start: int
|
|
220
|
+
data_end: int
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def template_pair(self) -> tuple[int, int]:
|
|
224
|
+
"""The admitted combination, as the allowlist spells it."""
|
|
225
|
+
|
|
226
|
+
return (self.grid_template, self.packing_template)
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def data_length(self) -> int:
|
|
230
|
+
"""How many bytes of packed values the message holds."""
|
|
231
|
+
|
|
232
|
+
return self.data_end - self.data_start
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def has_bitmap(self) -> bool:
|
|
236
|
+
"""Whether some grid points carry no value."""
|
|
237
|
+
|
|
238
|
+
return self.bitmap_indicator == _BITMAP_PRESENT
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def admit(
|
|
242
|
+
content: bytes,
|
|
243
|
+
budgets: ReaderBudgets,
|
|
244
|
+
*,
|
|
245
|
+
origin: str | None = None,
|
|
246
|
+
) -> MessageAdmission:
|
|
247
|
+
"""Read one message's headers and decide whether this product will open it.
|
|
248
|
+
|
|
249
|
+
Returns a description of the message, or raises ``ReaderError``.
|
|
250
|
+
|
|
251
|
+
``origin`` is for a caller that already knows what it is looking at -- a sample check, a
|
|
252
|
+
tool run over a directory of messages -- and it is absent on the Reader contract's own
|
|
253
|
+
path by design. ``ReaderFamily.decode`` is handed bytes, a pin and budgets and no
|
|
254
|
+
provenance whatever, and that is the property that makes a Reader refusal safe to carry
|
|
255
|
+
out of the confinement: a decoder that never learns a path cannot name one. On the
|
|
256
|
+
production path the recipe's own name for the source is attached one layer up, by
|
|
257
|
+
``sources.adapters._decode_in_clean_room``, which is the layer that knows it.
|
|
258
|
+
|
|
259
|
+
Nothing here reads the data section, and nothing here allocates in proportion to what the
|
|
260
|
+
message declares.
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
where = _origin_clause(origin)
|
|
264
|
+
if not isinstance(content, (bytes, bytearray, memoryview)):
|
|
265
|
+
_refuse("READER_ADMISSION", "reader.grib2.message", "must be exact bytes")
|
|
266
|
+
if not isinstance(budgets, ReaderBudgets):
|
|
267
|
+
_refuse(
|
|
268
|
+
"READER_BUDGET",
|
|
269
|
+
"reader.grib2.budgets",
|
|
270
|
+
"must be the Reader contract's budgets, so a cap cannot be invented at a call site",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
held = len(content)
|
|
274
|
+
if held > budgets.max_input_bytes:
|
|
275
|
+
_refuse(
|
|
276
|
+
"READER_BUDGET",
|
|
277
|
+
"reader.grib2.message",
|
|
278
|
+
f"holds {held} bytes{where}, over the {budgets.max_input_bytes}-byte input budget",
|
|
279
|
+
)
|
|
280
|
+
if held < _INDICATOR_LENGTH + _TRAILER_LENGTH:
|
|
281
|
+
_refuse(
|
|
282
|
+
"READER_ADMISSION",
|
|
283
|
+
"reader.grib2.message",
|
|
284
|
+
f"holds {held} bytes{where}, too few to be a weather message at all",
|
|
285
|
+
)
|
|
286
|
+
if content[:4] != _MAGIC:
|
|
287
|
+
_refuse(
|
|
288
|
+
"READER_ADMISSION",
|
|
289
|
+
"reader.grib2.message",
|
|
290
|
+
f"does not begin with the GRIB magic{where}, so it is not a weather message",
|
|
291
|
+
)
|
|
292
|
+
edition = content[7]
|
|
293
|
+
if edition != _EDITION:
|
|
294
|
+
_refuse(
|
|
295
|
+
"READER_ADMISSION",
|
|
296
|
+
"reader.grib2.message",
|
|
297
|
+
f"declares edition {edition}{where}; this Reader reads edition {_EDITION}, and "
|
|
298
|
+
f"edition 1 is a different format that happens to share the first four bytes",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
total = int.from_bytes(content[8:16], "big")
|
|
302
|
+
if total != held:
|
|
303
|
+
_refuse_a_length_that_is_not_the_file(content, total=total, held=held, where=where)
|
|
304
|
+
if content[total - _TRAILER_LENGTH : total] != _TRAILER:
|
|
305
|
+
_refuse(
|
|
306
|
+
"READER_ADMISSION",
|
|
307
|
+
"reader.grib2.message",
|
|
308
|
+
f"has no trailer at its declared end{where}, so the message it declares is not the "
|
|
309
|
+
f"message it holds",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
sections = _walk(content, total, where)
|
|
313
|
+
grid = _read_grid_section(content, sections[3], where)
|
|
314
|
+
packing_template = _read_packing_template(content, sections[5], where)
|
|
315
|
+
_refuse_a_lossy_codec(packing_template, where)
|
|
316
|
+
_admit_the_pair(grid.template, packing_template, where)
|
|
317
|
+
|
|
318
|
+
declared_points = grid.points
|
|
319
|
+
if declared_points > budgets.max_declared_cells:
|
|
320
|
+
_refuse(
|
|
321
|
+
"READER_BUDGET",
|
|
322
|
+
"reader.grib2.declared_points",
|
|
323
|
+
f"declares a grid of {declared_points} points{where}, over the "
|
|
324
|
+
f"{budgets.max_declared_cells}-point budget; refused from the header, with nothing "
|
|
325
|
+
f"read and nothing allocated",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
packing = _read_packing_parameters(content, sections[5], packing_template, where)
|
|
329
|
+
bitmap = _read_bitmap_indicator(content, sections[6], where)
|
|
330
|
+
_agree_on_the_point_count(declared_points, packing.points, bitmap, where)
|
|
331
|
+
data_start, data_end = _locate_data(sections[7])
|
|
332
|
+
|
|
333
|
+
return MessageAdmission(
|
|
334
|
+
edition=edition,
|
|
335
|
+
discipline=content[6],
|
|
336
|
+
total_length=total,
|
|
337
|
+
grid_template=grid.template,
|
|
338
|
+
packing_template=packing_template,
|
|
339
|
+
declared_points=declared_points,
|
|
340
|
+
packed_points=packing.points,
|
|
341
|
+
reference_value=packing.reference_value,
|
|
342
|
+
binary_scale=packing.binary_scale,
|
|
343
|
+
decimal_scale=packing.decimal_scale,
|
|
344
|
+
bits_per_value=packing.bits_per_value,
|
|
345
|
+
original_field_type=packing.original_field_type,
|
|
346
|
+
bitmap_indicator=bitmap,
|
|
347
|
+
identification_start=sections[1][0],
|
|
348
|
+
identification_length=sections[1][1],
|
|
349
|
+
grid_start=sections[3][0],
|
|
350
|
+
grid_length=sections[3][1],
|
|
351
|
+
product_start=sections[4][0],
|
|
352
|
+
product_length=sections[4][1],
|
|
353
|
+
data_start=data_start,
|
|
354
|
+
data_end=data_end,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _agree_on_the_point_count(declared: int, packed: int, bitmap: int, where: str) -> None:
|
|
359
|
+
"""Hold the grid's point count and the packing's against each other, on the bitmap's terms.
|
|
360
|
+
|
|
361
|
+
Section 3 says how many points the grid holds. Section 5 says how many points a value was
|
|
362
|
+
packed *for*. With no bitmap those are the same question and the two numbers must agree,
|
|
363
|
+
because picking one of them when they disagree is how a field ends up read shifted -- which
|
|
364
|
+
produces real numbers at the wrong coordinates and raises nothing.
|
|
365
|
+
|
|
366
|
+
With a bitmap they are different questions: the bitmap marks points that carry no value, so
|
|
367
|
+
fewer values are packed than the grid holds. Requiring equality there would refuse every
|
|
368
|
+
message that marks a point absent, which is most published fields outside a full-globe
|
|
369
|
+
analysis. What stays refused is a packing covering more points than the grid has, which
|
|
370
|
+
cannot be placed under any reading of either number.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
if bitmap == _BITMAP_PRESENT:
|
|
374
|
+
if packed <= declared:
|
|
375
|
+
return
|
|
376
|
+
_refuse(
|
|
377
|
+
"READER_ADMISSION",
|
|
378
|
+
"reader.grib2.message",
|
|
379
|
+
f"says its grid holds {declared} points and packs values for {packed} of them{where}; "
|
|
380
|
+
f"a bitmap marks points that carry no value, so a message may pack fewer than its "
|
|
381
|
+
f"grid holds and can never pack more",
|
|
382
|
+
)
|
|
383
|
+
if packed != declared:
|
|
384
|
+
_refuse(
|
|
385
|
+
"READER_ADMISSION",
|
|
386
|
+
"reader.grib2.message",
|
|
387
|
+
f"says its grid holds {declared} points and its packing covers {packed}{where}; with "
|
|
388
|
+
f"no bitmap the two must agree, because choosing one of them is how values end up at "
|
|
389
|
+
f"the wrong coordinates",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# --- The walk ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _walk(content: bytes, total: int, where: str) -> Mapping[int, tuple[int, int]]:
|
|
397
|
+
"""Every section of the message as ``number -> (start, length)``, or a refusal.
|
|
398
|
+
|
|
399
|
+
Bounded twice: each declared length is checked against the bytes remaining before it is
|
|
400
|
+
followed, and the section numbers must strictly ascend, which caps the walk at seven steps
|
|
401
|
+
whatever the lengths say.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
end = total - _TRAILER_LENGTH
|
|
405
|
+
offset = _INDICATOR_LENGTH
|
|
406
|
+
previous = 0
|
|
407
|
+
found: dict[int, tuple[int, int]] = {}
|
|
408
|
+
while offset < end:
|
|
409
|
+
if end - offset < _SECTION_HEADER_LENGTH:
|
|
410
|
+
_refuse(
|
|
411
|
+
"READER_ADMISSION",
|
|
412
|
+
"reader.grib2.section",
|
|
413
|
+
f"leaves {end - offset} bytes at byte {offset}{where}, too few for a section "
|
|
414
|
+
f"header, so the message does not account for all of its own bytes",
|
|
415
|
+
)
|
|
416
|
+
length = int.from_bytes(content[offset : offset + 4], "big")
|
|
417
|
+
number = content[offset + 4]
|
|
418
|
+
if length < _SECTION_HEADER_LENGTH:
|
|
419
|
+
_refuse(
|
|
420
|
+
"READER_ADMISSION",
|
|
421
|
+
"reader.grib2.section",
|
|
422
|
+
f"section {number} at byte {offset} declares a length of {length}{where}, which "
|
|
423
|
+
f"is smaller than the {_SECTION_HEADER_LENGTH}-byte header it must contain; a "
|
|
424
|
+
f"walk that followed it would not advance",
|
|
425
|
+
)
|
|
426
|
+
if offset + length > end:
|
|
427
|
+
_refuse(
|
|
428
|
+
"READER_ADMISSION",
|
|
429
|
+
"reader.grib2.section",
|
|
430
|
+
f"section {number} at byte {offset} declares a length of {length}{where}, which "
|
|
431
|
+
f"runs past the end of the message",
|
|
432
|
+
)
|
|
433
|
+
if number not in _SECTION_NUMBERS:
|
|
434
|
+
_refuse(
|
|
435
|
+
"READER_ADMISSION",
|
|
436
|
+
"reader.grib2.section",
|
|
437
|
+
f"section number {number} at byte {offset}{where} is not one edition "
|
|
438
|
+
f"{_EDITION} defines",
|
|
439
|
+
)
|
|
440
|
+
if number <= previous:
|
|
441
|
+
_refuse(
|
|
442
|
+
"READER_ADMISSION",
|
|
443
|
+
"reader.grib2.section",
|
|
444
|
+
f"section {number} at byte {offset} follows section {previous}{where}; sections "
|
|
445
|
+
f"ascend, and a message carrying more than one field is not one this Reader "
|
|
446
|
+
f"opens",
|
|
447
|
+
)
|
|
448
|
+
found[number] = (offset, length)
|
|
449
|
+
previous = number
|
|
450
|
+
offset += length
|
|
451
|
+
|
|
452
|
+
if offset != end:
|
|
453
|
+
_refuse(
|
|
454
|
+
"READER_ADMISSION",
|
|
455
|
+
"reader.grib2.section",
|
|
456
|
+
f"has a section chain ending at byte {offset} rather than at the trailer at "
|
|
457
|
+
f"{end}{where}",
|
|
458
|
+
)
|
|
459
|
+
missing = [number for number in _REQUIRED_SECTIONS if number not in found]
|
|
460
|
+
if missing:
|
|
461
|
+
_refuse(
|
|
462
|
+
"READER_ADMISSION",
|
|
463
|
+
"reader.grib2.section",
|
|
464
|
+
f"does not carry section {', '.join(str(number) for number in missing)}{where}, "
|
|
465
|
+
f"which a single-field message must",
|
|
466
|
+
)
|
|
467
|
+
return found
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# --- The sections this module actually reads ---------------------------------------------
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@dataclass(frozen=True)
|
|
474
|
+
class _Grid:
|
|
475
|
+
points: int
|
|
476
|
+
template: int
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@dataclass(frozen=True)
|
|
480
|
+
class _Packing:
|
|
481
|
+
points: int
|
|
482
|
+
reference_value: float
|
|
483
|
+
binary_scale: int
|
|
484
|
+
decimal_scale: int
|
|
485
|
+
bits_per_value: int
|
|
486
|
+
original_field_type: int
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _read_grid_section(content: bytes, span: tuple[int, int], where: str) -> _Grid:
|
|
490
|
+
"""The declared point count and the grid template number, and nothing else."""
|
|
491
|
+
|
|
492
|
+
start, length = span
|
|
493
|
+
_require_length(length, 14, 3, where)
|
|
494
|
+
source = content[start + 5]
|
|
495
|
+
if source != 0:
|
|
496
|
+
_refuse(
|
|
497
|
+
"READER_ADMISSION",
|
|
498
|
+
"reader.grib2.grid",
|
|
499
|
+
f"defines its grid from source {source}{where} rather than from a template in the "
|
|
500
|
+
f"message, so the geometry is somewhere this Reader cannot see it",
|
|
501
|
+
)
|
|
502
|
+
optional_octets = content[start + 10]
|
|
503
|
+
if optional_octets != 0:
|
|
504
|
+
_refuse(
|
|
505
|
+
"READER_ADMISSION",
|
|
506
|
+
"reader.grib2.grid",
|
|
507
|
+
f"carries a {optional_octets}-octet list of point counts per row{where}, which is a "
|
|
508
|
+
f"quasi-regular grid; this Reader opens grids whose shape is in the template",
|
|
509
|
+
)
|
|
510
|
+
return _Grid(
|
|
511
|
+
points=int.from_bytes(content[start + 6 : start + 10], "big"),
|
|
512
|
+
template=int.from_bytes(content[start + 12 : start + 14], "big"),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _read_packing_template(content: bytes, span: tuple[int, int], where: str) -> int:
|
|
517
|
+
"""The packing template number alone, read before anything is decided about it."""
|
|
518
|
+
|
|
519
|
+
start, length = span
|
|
520
|
+
_require_length(length, 11, 5, where)
|
|
521
|
+
return int.from_bytes(content[start + 9 : start + 11], "big")
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _read_packing_parameters(
|
|
525
|
+
content: bytes,
|
|
526
|
+
span: tuple[int, int],
|
|
527
|
+
template: int,
|
|
528
|
+
where: str,
|
|
529
|
+
) -> _Packing:
|
|
530
|
+
"""The reference value, the two scale factors, and the bit width of an admitted packing."""
|
|
531
|
+
|
|
532
|
+
start, length = span
|
|
533
|
+
_require_length(length, _PACKING_PARAMETERS_END, 5, where)
|
|
534
|
+
return _Packing(
|
|
535
|
+
points=int.from_bytes(content[start + 5 : start + 9], "big"),
|
|
536
|
+
reference_value=struct.unpack(">f", content[start + 11 : start + 15])[0],
|
|
537
|
+
binary_scale=_sign_magnitude(content[start + 15 : start + 17]),
|
|
538
|
+
decimal_scale=_sign_magnitude(content[start + 17 : start + 19]),
|
|
539
|
+
bits_per_value=content[start + 19],
|
|
540
|
+
original_field_type=content[start + 20],
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _read_bitmap_indicator(content: bytes, span: tuple[int, int], where: str) -> int:
|
|
545
|
+
"""Whether every point carries a value, refusing an indicator that resolves to nothing."""
|
|
546
|
+
|
|
547
|
+
start, length = span
|
|
548
|
+
_require_length(length, 6, 6, where)
|
|
549
|
+
indicator = content[start + 5]
|
|
550
|
+
if indicator not in {_NO_BITMAP, _BITMAP_PRESENT}:
|
|
551
|
+
detail = (
|
|
552
|
+
"refers to a bitmap defined in an earlier message, and this Reader admits one "
|
|
553
|
+
"message at a time, so there is no earlier message to refer to"
|
|
554
|
+
if indicator == 254
|
|
555
|
+
else f"uses bitmap indicator {indicator}, which edition {_EDITION} reserves and "
|
|
556
|
+
f"therefore does not define"
|
|
557
|
+
)
|
|
558
|
+
_refuse("READER_ADMISSION", "reader.grib2.bitmap", f"{detail}{where}")
|
|
559
|
+
return indicator
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _locate_data(span: tuple[int, int]) -> tuple[int, int]:
|
|
563
|
+
"""Where the packed values begin and end. This is as close as this module gets to them."""
|
|
564
|
+
|
|
565
|
+
start, length = span
|
|
566
|
+
return start + _SECTION_HEADER_LENGTH, start + length
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# --- The allowlist ------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _refuse_a_lossy_codec(packing: int, where: str) -> None:
|
|
573
|
+
"""Refuse JPEG 2000 packing, in wording that offers no fix, because there is not one.
|
|
574
|
+
|
|
575
|
+
Checked before the allowlist and reported under its own subject, so nothing downstream can
|
|
576
|
+
read this as "a template we have not got round to". It has not got round to nothing: a
|
|
577
|
+
sealed dataset states that its numbers are the publisher's numbers, and a codec free to
|
|
578
|
+
return values the publisher never wrote cannot support that statement at any bit rate.
|
|
579
|
+
"""
|
|
580
|
+
|
|
581
|
+
if packing not in LOSSY_PACKING_TEMPLATES:
|
|
582
|
+
return
|
|
583
|
+
_refuse(
|
|
584
|
+
"READER_ADMISSION",
|
|
585
|
+
"reader.grib2.packing.lossy",
|
|
586
|
+
f"is packed with the JPEG 2000 code stream, template 5.{packing}{where}. This product "
|
|
587
|
+
f"does not build sealed data from a lossy codec, so this is not a matter of approving "
|
|
588
|
+
f"one more packing method: approving it would not help, because the objection is to "
|
|
589
|
+
f"the codec and not to the paperwork. A file published this way has to be republished "
|
|
590
|
+
f"in a lossless packing before it can be read here.",
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _admit_the_pair(grid: int, packing: int, where: str) -> None:
|
|
595
|
+
"""Refuse a grid, a packing, or a combination, each naming the edit that would admit it."""
|
|
596
|
+
|
|
597
|
+
if (grid, packing) in ADMITTED_TEMPLATE_PAIRS:
|
|
598
|
+
return
|
|
599
|
+
admitted_grids = {number for number, _ in ADMITTED_TEMPLATE_PAIRS}
|
|
600
|
+
admitted_packings = {number for _, number in ADMITTED_TEMPLATE_PAIRS}
|
|
601
|
+
pair = _pair_text(grid, packing)
|
|
602
|
+
fix = (
|
|
603
|
+
f"Approving it means adding the pair ({grid}, {packing}) to {ALLOWLIST_CONSTANT}, "
|
|
604
|
+
f"vendoring a message that declares it, and re-certifying the Reader."
|
|
605
|
+
)
|
|
606
|
+
if grid not in admitted_grids:
|
|
607
|
+
_refuse(
|
|
608
|
+
"READER_GRID_UNKNOWN",
|
|
609
|
+
"reader.grib2.grid_template",
|
|
610
|
+
f"is on grid template 3.{grid}{where}, a layout this product has never been checked "
|
|
611
|
+
f"against, so it will not guess where the values belong. {pair} {fix}",
|
|
612
|
+
)
|
|
613
|
+
if packing not in admitted_packings:
|
|
614
|
+
_refuse(
|
|
615
|
+
"READER_PACKING_UNKNOWN",
|
|
616
|
+
"reader.grib2.packing_template",
|
|
617
|
+
f"is packed with template 5.{packing}{where}, a method this product does not "
|
|
618
|
+
f"unpack. {pair} {fix}",
|
|
619
|
+
)
|
|
620
|
+
_refuse(
|
|
621
|
+
"READER_PACKING_UNKNOWN",
|
|
622
|
+
"reader.grib2.packing_template",
|
|
623
|
+
f"pairs grid template 3.{grid} with packing template 5.{packing}{where}, and this "
|
|
624
|
+
f"product has opened each of those but never the two together. {pair} {fix}",
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _pair_text(grid: int, packing: int) -> str:
|
|
629
|
+
"""The one sentence that says this is policy rather than breakage."""
|
|
630
|
+
|
|
631
|
+
return (
|
|
632
|
+
f"The combination this file declares, grid 3.{grid} with packing 5.{packing}, is not on "
|
|
633
|
+
f"the list of combinations this product opens. That list is closed on purpose: a "
|
|
634
|
+
f"combination nobody has checked against a real file is not on it."
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
# --- Shared refusals ----------------------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _refuse_a_length_that_is_not_the_file(
|
|
642
|
+
content: bytes,
|
|
643
|
+
*,
|
|
644
|
+
total: int,
|
|
645
|
+
held: int,
|
|
646
|
+
where: str,
|
|
647
|
+
) -> NoReturn:
|
|
648
|
+
"""Say which of the two things a length mismatch means, because the fixes differ.
|
|
649
|
+
|
|
650
|
+
A message whose declared length ends before the file does is almost never a damaged
|
|
651
|
+
download: it is a **multi-message file**, which is what every published GFS and HRRR file
|
|
652
|
+
is. This Reader opens exactly one message and has no way to be told which one, so such a
|
|
653
|
+
file is refused -- but it has to be refused as what it is. Reporting "it claims fewer
|
|
654
|
+
bytes than arrived" sends a person to check their download, which is the wrong repair for
|
|
655
|
+
a file that is intact and correct.
|
|
656
|
+
|
|
657
|
+
The two are told apart from the bytes themselves: a message that ends with the edition-2
|
|
658
|
+
trailer at its own declared end and is followed by another ``GRIB`` indicator is a
|
|
659
|
+
well-formed message inside a well-formed collection. Anything else -- a short read, a
|
|
660
|
+
truncated transfer, a length field that points into the middle of nothing -- keeps the
|
|
661
|
+
original refusal, because that one really is the file disagreeing with itself.
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
ends_cleanly = (
|
|
665
|
+
_INDICATOR_LENGTH + _TRAILER_LENGTH <= total < held
|
|
666
|
+
and content[total - _TRAILER_LENGTH : total] == _TRAILER
|
|
667
|
+
)
|
|
668
|
+
another_follows = content[total : total + len(_MAGIC)] == _MAGIC
|
|
669
|
+
if ends_cleanly and another_follows:
|
|
670
|
+
_refuse(
|
|
671
|
+
"READER_ADMISSION",
|
|
672
|
+
"reader.grib2.message",
|
|
673
|
+
f"holds more than one weather message{where}: the first declares {total} bytes, "
|
|
674
|
+
f"ends properly at that point, and another begins straight after it in the "
|
|
675
|
+
f"{held} bytes that arrived. Published model files are collections of records and "
|
|
676
|
+
f"this is what one looks like. This Reader opens exactly one message and its "
|
|
677
|
+
f"settings have no way to say which record of a collection to open, so a "
|
|
678
|
+
f"collection is refused rather than opened at its first record. The single record "
|
|
679
|
+
f"you want has to be extracted before the recipe points at it; opening a "
|
|
680
|
+
f"collection is separate, unbuilt work and the file is not damaged",
|
|
681
|
+
)
|
|
682
|
+
direction = "more" if total > held else "fewer"
|
|
683
|
+
_refuse(
|
|
684
|
+
"READER_ADMISSION",
|
|
685
|
+
"reader.grib2.message",
|
|
686
|
+
f"declares {total} bytes{where} but holds {held}, so it claims {direction} bytes "
|
|
687
|
+
f"than arrived; a message is complete or it is not read",
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _origin_clause(origin: str | None) -> str:
|
|
692
|
+
"""The recipe's own name for the source, so a halted refresh names the file."""
|
|
693
|
+
|
|
694
|
+
if origin is None:
|
|
695
|
+
return ""
|
|
696
|
+
if not isinstance(origin, str) or not origin.strip():
|
|
697
|
+
_refuse("READER_ADMISSION", "reader.grib2.origin", "must be a nonempty name or absent")
|
|
698
|
+
return f" in {origin.strip()}"
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _require_length(length: int, needed: int, section: int, where: str) -> None:
|
|
702
|
+
"""Refuse a section too short to hold the field about to be read from it."""
|
|
703
|
+
|
|
704
|
+
if length < needed:
|
|
705
|
+
_refuse(
|
|
706
|
+
"READER_ADMISSION",
|
|
707
|
+
"reader.grib2.section",
|
|
708
|
+
f"section {section} declares {length} bytes{where} but must hold at least {needed} "
|
|
709
|
+
f"to state what it is for",
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _sign_magnitude(raw: bytes) -> int:
|
|
714
|
+
"""A GRIB2 signed integer: a sign bit and a magnitude, never two's complement."""
|
|
715
|
+
|
|
716
|
+
value = int.from_bytes(raw, "big")
|
|
717
|
+
sign = 1 << (8 * len(raw) - 1)
|
|
718
|
+
return -(value & ~sign) if value & sign else value
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _refuse(code: str, subject: str, detail: str) -> NoReturn:
|
|
722
|
+
raise ReaderError(code, subject, detail)
|