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
mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"column_names": [
|
|
3
|
+
"point_id",
|
|
4
|
+
"variable",
|
|
5
|
+
"level",
|
|
6
|
+
"run_time",
|
|
7
|
+
"valid_time",
|
|
8
|
+
"value",
|
|
9
|
+
"matched_grid_lat",
|
|
10
|
+
"matched_grid_lon",
|
|
11
|
+
"message_sha256"
|
|
12
|
+
],
|
|
13
|
+
"decode_options": {
|
|
14
|
+
"admission_allowlist_version": 1,
|
|
15
|
+
"points": [
|
|
16
|
+
{
|
|
17
|
+
"id": "denver",
|
|
18
|
+
"latitude": "39.7392",
|
|
19
|
+
"longitude": "-104.9903"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "chicago",
|
|
23
|
+
"latitude": "41.8781",
|
|
24
|
+
"longitude": "-87.6298"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "houston",
|
|
28
|
+
"latitude": "29.7604",
|
|
29
|
+
"longitude": "-95.3698"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"variable": {
|
|
33
|
+
"discipline": 0,
|
|
34
|
+
"name": "temperature_2m",
|
|
35
|
+
"parameter_category": 0,
|
|
36
|
+
"parameter_number": 0,
|
|
37
|
+
"surface_type": 103,
|
|
38
|
+
"surface_value": 2
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"family_id": "weather.grib2",
|
|
42
|
+
"family_version": "2.0.0",
|
|
43
|
+
"flags": [],
|
|
44
|
+
"normalized_sha256": "5b3daf9a43608fbd424e119393ef19973d003ecba8f14fc9c39ece82b7feb801",
|
|
45
|
+
"provenance": {
|
|
46
|
+
"licence": "Public domain. A work of the United States Government under 17 U.S.C. 105, redistributed by NOAA through the NOAA Open Data Dissemination programme with no restriction on reuse or redistribution.",
|
|
47
|
+
"origin": "https://noaa-hrrr-bdp-pds.s3.amazonaws.com/hrrr.20260805/conus/hrrr.t12z.wrfsfcf00.grib2, bytes 34694688-35889177 (record 71 of the .grib2.idx sidecar)",
|
|
48
|
+
"retrieved": "2026-08-07",
|
|
49
|
+
"why": "The same certified HRRR message as 1.0.0 discriminates the versioned evidence-bearing output: level, run time, valid time, matched coordinates and message digest are all derived from these exact admitted bytes."
|
|
50
|
+
},
|
|
51
|
+
"row_count": 3,
|
|
52
|
+
"sample_file": "hrrr-2m-temperature.grib2",
|
|
53
|
+
"warm_up": true
|
|
54
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""Known-good samples: one packaged constant, one loader, one comparison.
|
|
2
|
+
|
|
3
|
+
A known-good sample is a real file with its correct answer recorded beside it. Certification
|
|
4
|
+
requires one per Reader family, and this module is what makes "or it does not land" a test
|
|
5
|
+
rather than a habit: ``load_samples`` reads the packaged tree, and the coverage gate in
|
|
6
|
+
``tests/test_reader_samples.py`` fails naming any family in the Toolbox that has no sample.
|
|
7
|
+
|
|
8
|
+
The constant has exactly one home. CI asserts the recorded answer and a refresh checks the
|
|
9
|
+
same recorded answer before it fetches anything, and both reach it through this loader. Two
|
|
10
|
+
copies of one number drift, and a check comparing against a drifted number reports confidence
|
|
11
|
+
it does not have.
|
|
12
|
+
|
|
13
|
+
``warm_up`` is the pre-refresh half of the same machinery: it re-opens the one sample a family
|
|
14
|
+
designates and halts the refresh when the answer has moved. It takes its decode route from
|
|
15
|
+
its caller -- in process for CI, through the clean room for a refresh -- and does the
|
|
16
|
+
comparison itself, so a caller may change how the answer was obtained and can never weaken
|
|
17
|
+
what counts as the right answer.
|
|
18
|
+
|
|
19
|
+
What a sample records is four facts and no more -- the digest of the Reader's output bytes, the
|
|
20
|
+
row count, the column names, and the decode flags. ``DecodedFacts`` is that same four-field
|
|
21
|
+
shape, so a decode route can only offer what an expectation actually states. ``compare_facts``
|
|
22
|
+
is the single place that knows how a recorded expectation is checked; it returns a verdict
|
|
23
|
+
rather than raising, so each consumer chooses its own reaction to a moved answer.
|
|
24
|
+
|
|
25
|
+
Not goldens
|
|
26
|
+
-----------
|
|
27
|
+
Samples are not golden fixtures and must never acquire their machinery. A golden digest in
|
|
28
|
+
this repository is minted on the x86_64 Linux authority and every golden assertion is gated on
|
|
29
|
+
the running architecture; a sample assertion built that way would skip on the arm64 development
|
|
30
|
+
host and on the macOS parity leg, so the gate would be vacuous on two legs out of three while
|
|
31
|
+
reporting green on all of them. An expectation carrying an ``environment`` key is therefore
|
|
32
|
+
refused by name -- that key is how a golden records the host that produced it, and a sample's
|
|
33
|
+
answer is a property of the Reader and the bytes, never of the host. ``samples/README.md``
|
|
34
|
+
states the convention in full.
|
|
35
|
+
|
|
36
|
+
Import discipline
|
|
37
|
+
-----------------
|
|
38
|
+
This module may reach ``readers.contracts``, ``readers.registry``, ``canonical``, and the
|
|
39
|
+
standard library. It may not import ``acquisition`` -- that back-edge would close an import
|
|
40
|
+
cycle -- which is why the clean-room decode route arrives as an injected callable rather than
|
|
41
|
+
as a sandbox call made here.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import datetime
|
|
47
|
+
import json
|
|
48
|
+
import pathlib
|
|
49
|
+
import re
|
|
50
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
from types import MappingProxyType
|
|
53
|
+
from typing import Any, NoReturn
|
|
54
|
+
|
|
55
|
+
from mostlyright.data_harness.canonical import sha256_bytes
|
|
56
|
+
from mostlyright.data_harness.readers.contracts import (
|
|
57
|
+
DECODE_FLAGS,
|
|
58
|
+
ReaderError,
|
|
59
|
+
ReaderPin,
|
|
60
|
+
ReaderResult,
|
|
61
|
+
identifier,
|
|
62
|
+
semver,
|
|
63
|
+
)
|
|
64
|
+
from mostlyright.data_harness.readers.registry import TOOLBOX, ReaderRegistry
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"SAMPLES_ROOT",
|
|
68
|
+
"DecodedFacts",
|
|
69
|
+
"FactComparison",
|
|
70
|
+
"ReaderSample",
|
|
71
|
+
"SampleProvenance",
|
|
72
|
+
"compare_facts",
|
|
73
|
+
"decode_in_process",
|
|
74
|
+
"load_samples",
|
|
75
|
+
"samples_for",
|
|
76
|
+
"verify_sample",
|
|
77
|
+
"warm_up",
|
|
78
|
+
"warm_up_sample",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# The packaged tree. It ships inside the wheel because the warm-up check runs in production,
|
|
82
|
+
# and a sample that lived under ``tests/`` would not be installed with the code that needs it.
|
|
83
|
+
SAMPLES_ROOT = pathlib.Path(__file__).resolve().parent / "samples"
|
|
84
|
+
|
|
85
|
+
_EXPECTATION_FILENAME = "expected.json"
|
|
86
|
+
|
|
87
|
+
# The closed expectation vocabulary. ``warm_up`` is the one optional key; everything else is
|
|
88
|
+
# required, and anything outside the set is refused by name rather than ignored, because an
|
|
89
|
+
# ignored key is an expectation someone believes they recorded.
|
|
90
|
+
_REQUIRED_KEYS = frozenset(
|
|
91
|
+
{
|
|
92
|
+
"family_id",
|
|
93
|
+
"family_version",
|
|
94
|
+
"decode_options",
|
|
95
|
+
"sample_file",
|
|
96
|
+
"normalized_sha256",
|
|
97
|
+
"row_count",
|
|
98
|
+
"column_names",
|
|
99
|
+
"flags",
|
|
100
|
+
"provenance",
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
_OPTIONAL_KEYS = frozenset({"warm_up"})
|
|
104
|
+
_EXPECTATION_KEYS = _REQUIRED_KEYS | _OPTIONAL_KEYS
|
|
105
|
+
|
|
106
|
+
# The four reviewable provenance facts. Exactly these: a sample with fewer cannot be reviewed,
|
|
107
|
+
# and one with more has grown a field no reviewer was told to read.
|
|
108
|
+
_PROVENANCE_KEYS = ("origin", "retrieved", "licence", "why")
|
|
109
|
+
|
|
110
|
+
# A recorded digest is lower-case hex, so two spellings of one digest cannot both be admitted.
|
|
111
|
+
_DIGEST = re.compile(r"^[0-9a-f]{64}$")
|
|
112
|
+
|
|
113
|
+
# A sample filename is a bare name in the sample's own directory: no separator, no parent
|
|
114
|
+
# reference, no leading dot. Same shape the Reader contract admits for a sealed snapshot.
|
|
115
|
+
_SAMPLE_FILENAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,254}$")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _refuse(subject: str, detail: str) -> NoReturn:
|
|
119
|
+
raise ReaderError("READER_CONTRACT", subject, detail)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class SampleProvenance:
|
|
124
|
+
"""Where a sample came from and what it proves, in four reviewable facts."""
|
|
125
|
+
|
|
126
|
+
origin: str
|
|
127
|
+
retrieved: str
|
|
128
|
+
licence: str
|
|
129
|
+
why: str
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True)
|
|
133
|
+
class ReaderSample:
|
|
134
|
+
"""One known-good sample: the bytes, the settings, and the answer they must produce."""
|
|
135
|
+
|
|
136
|
+
family_id: str
|
|
137
|
+
family_version: str
|
|
138
|
+
sample_name: str
|
|
139
|
+
sample_file: str
|
|
140
|
+
decode_options: Mapping[str, Any]
|
|
141
|
+
content: bytes
|
|
142
|
+
normalized_sha256: str
|
|
143
|
+
row_count: int
|
|
144
|
+
column_names: tuple[str, ...]
|
|
145
|
+
flags: tuple[str, ...]
|
|
146
|
+
provenance: SampleProvenance
|
|
147
|
+
warm_up: bool = False
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def coordinate(self) -> str:
|
|
151
|
+
"""The ``id@version`` coordinate of the family this sample belongs to."""
|
|
152
|
+
|
|
153
|
+
return f"{self.family_id}@{self.family_version}"
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def label(self) -> str:
|
|
157
|
+
"""The name a refusal message carries, so an operator can find the file."""
|
|
158
|
+
|
|
159
|
+
return f"{self.coordinate}/{self.sample_name}"
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def pin(self) -> ReaderPin:
|
|
163
|
+
"""The pin a recipe would seal to decode this sample the way it was recorded."""
|
|
164
|
+
|
|
165
|
+
return ReaderPin(self.family_id, self.family_version, self.decode_options)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class DecodedFacts:
|
|
170
|
+
"""Exactly the four facts an expectation records about one decode.
|
|
171
|
+
|
|
172
|
+
Four fields on purpose. This record is the seam a decode route is injected across, and a
|
|
173
|
+
richer one would let a caller supply something that compares unequal for a reason the
|
|
174
|
+
expectation never stated.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
normalized_sha256: str
|
|
178
|
+
row_count: int
|
|
179
|
+
column_names: tuple[str, ...]
|
|
180
|
+
flags: tuple[str, ...]
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def from_result(cls, result: ReaderResult) -> DecodedFacts:
|
|
184
|
+
"""Reduce a decode result to the recorded facts, digesting the output bytes."""
|
|
185
|
+
|
|
186
|
+
return cls(
|
|
187
|
+
sha256_bytes(result.content),
|
|
188
|
+
result.row_count,
|
|
189
|
+
tuple(result.column_names),
|
|
190
|
+
tuple(result.flags),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(frozen=True)
|
|
195
|
+
class FactComparison:
|
|
196
|
+
"""The verdict of one comparison: what was expected, what was observed, what moved."""
|
|
197
|
+
|
|
198
|
+
sample: ReaderSample
|
|
199
|
+
observed: DecodedFacts
|
|
200
|
+
differences: tuple[str, ...]
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def matches(self) -> bool:
|
|
204
|
+
"""True when every recorded fact was reproduced exactly."""
|
|
205
|
+
|
|
206
|
+
return not self.differences
|
|
207
|
+
|
|
208
|
+
def describe(self) -> str:
|
|
209
|
+
"""One line an operator can act on, naming the sample and every fact that moved."""
|
|
210
|
+
|
|
211
|
+
if self.matches:
|
|
212
|
+
return f"{self.sample.label}: every recorded fact was reproduced"
|
|
213
|
+
return f"{self.sample.label}: " + "; ".join(self.differences)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def compare_facts(sample: ReaderSample, facts: DecodedFacts) -> FactComparison:
|
|
217
|
+
"""The one comparison, shared by CI and by the pre-refresh check.
|
|
218
|
+
|
|
219
|
+
Returns a verdict rather than raising. The CI gate turns a mismatch into a failed
|
|
220
|
+
assertion and a refresh turns the same mismatch into a halt, and both reach that decision
|
|
221
|
+
from one implementation of "did the answer move".
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
if not isinstance(facts, DecodedFacts):
|
|
225
|
+
_refuse(
|
|
226
|
+
f"reader.sample.{sample.label}",
|
|
227
|
+
"a decode route must return the four recorded facts and nothing else",
|
|
228
|
+
)
|
|
229
|
+
differences = [
|
|
230
|
+
f"{name}: expected {expected!r}, observed {observed!r}"
|
|
231
|
+
for name, expected, observed in (
|
|
232
|
+
("normalized_sha256", sample.normalized_sha256, facts.normalized_sha256),
|
|
233
|
+
("row_count", sample.row_count, facts.row_count),
|
|
234
|
+
("column_names", sample.column_names, facts.column_names),
|
|
235
|
+
("flags", sample.flags, facts.flags),
|
|
236
|
+
)
|
|
237
|
+
if expected != observed
|
|
238
|
+
]
|
|
239
|
+
return FactComparison(sample, facts, tuple(differences))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def decode_in_process(
|
|
243
|
+
sample: ReaderSample,
|
|
244
|
+
*,
|
|
245
|
+
registry: ReaderRegistry = TOOLBOX,
|
|
246
|
+
) -> DecodedFacts:
|
|
247
|
+
"""Decode one sample here, in this process, through the family it pins.
|
|
248
|
+
|
|
249
|
+
The default route. It resolves the family from the closed Toolbox on the sample's exact
|
|
250
|
+
coordinate and calls it directly, which is the only route available on a leg with no
|
|
251
|
+
sandbox -- so this is what CI exercises. ``registry`` is a keyword with a default, which
|
|
252
|
+
keeps the positional signature exactly ``(ReaderSample) -> DecodedFacts``.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
family = registry.resolve(sample.family_id, sample.family_version)
|
|
256
|
+
result = family.decode(sample.content, sample.pin, family.default_budgets)
|
|
257
|
+
if not isinstance(result, ReaderResult):
|
|
258
|
+
raise ReaderError(
|
|
259
|
+
"READER_OUTPUT",
|
|
260
|
+
f"reader.sample.{sample.label}",
|
|
261
|
+
"the family returned something that is not a Reader result",
|
|
262
|
+
)
|
|
263
|
+
result.validate_for(family.output_format, family.default_budgets)
|
|
264
|
+
return DecodedFacts.from_result(result)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def verify_sample(
|
|
268
|
+
sample: ReaderSample,
|
|
269
|
+
*,
|
|
270
|
+
registry: ReaderRegistry = TOOLBOX,
|
|
271
|
+
) -> FactComparison:
|
|
272
|
+
"""Decode one sample and check it against its recorded answer.
|
|
273
|
+
|
|
274
|
+
A two-line composition on purpose: there is exactly one place that knows how a recorded
|
|
275
|
+
expectation is checked, and every consumer -- the CI gate and the pre-refresh check --
|
|
276
|
+
goes through it.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
return compare_facts(sample, decode_in_process(sample, registry=registry))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def warm_up(
|
|
283
|
+
family_id: str,
|
|
284
|
+
family_version: str,
|
|
285
|
+
*,
|
|
286
|
+
decode: Callable[[ReaderSample], DecodedFacts] = decode_in_process,
|
|
287
|
+
root: pathlib.Path | None = None,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Re-open one known file and halt if the answer has moved. Returns silently otherwise.
|
|
290
|
+
|
|
291
|
+
A decoder that answers differently on a file whose answer is known has changed the meaning
|
|
292
|
+
of every dataset it has ever produced. That is why this is a halt and not a warning: the
|
|
293
|
+
datasets already sealed cannot be un-sealed, so the only useful moment to stop is before
|
|
294
|
+
the next one is built. Stated for the user as: before every refresh, the Reader re-opens
|
|
295
|
+
one known file; a wrong answer halts the refresh.
|
|
296
|
+
|
|
297
|
+
The decode route is supplied by the caller, and there are exactly two callers.
|
|
298
|
+
|
|
299
|
+
* **The CI coverage gate** takes the default, ``decode_in_process``. It resolves the
|
|
300
|
+
family from the closed Toolbox and calls it here, which is the only route available on a
|
|
301
|
+
leg with no sandbox. It answers "does this family decode this file correctly".
|
|
302
|
+
* **A refresh** injects a callable backed by the clean room's ``decode_and_parse``. It
|
|
303
|
+
answers "does this family decode this file correctly on the path production takes, under
|
|
304
|
+
the attested policy". The production-path check matters because a warm-up
|
|
305
|
+
that decoded by a route production does not take would clear a family whose failure
|
|
306
|
+
lives in the clean room.
|
|
307
|
+
|
|
308
|
+
The route arrives as a parameter rather than being called from here because ``readers/``
|
|
309
|
+
may not import ``acquisition/``; that back-edge would close an import cycle. What the
|
|
310
|
+
parameter may supply is a decode and never a verdict: the callable returns
|
|
311
|
+
``DecodedFacts`` and this function does the comparison itself through ``compare_facts``,
|
|
312
|
+
against the expectation the packaged tree records. So a caller can change how the answer
|
|
313
|
+
was obtained and can never weaken what counts as the right answer.
|
|
314
|
+
|
|
315
|
+
A route that raises is a halt, not a pass. A clean room refusing during warm-up is
|
|
316
|
+
exactly the condition a refresh must stop on, and treating the refusal as an absent check
|
|
317
|
+
would turn the loudest possible failure into silence.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
sample = warm_up_sample(family_id, family_version, root=root)
|
|
321
|
+
subject = f"reader.warmup.{sample.label}"
|
|
322
|
+
try:
|
|
323
|
+
facts = decode(sample)
|
|
324
|
+
except ReaderError as error:
|
|
325
|
+
raise ReaderError(
|
|
326
|
+
"READER_WARMUP",
|
|
327
|
+
subject,
|
|
328
|
+
f"the decode route refused the warm-up sample: {error}",
|
|
329
|
+
) from error
|
|
330
|
+
# Deliberately broad: any way a decode route can fail is a halt, and a route that failed
|
|
331
|
+
# in an unforeseen way is exactly the one that must not read as an absent check.
|
|
332
|
+
except Exception as error:
|
|
333
|
+
raise ReaderError(
|
|
334
|
+
"READER_WARMUP",
|
|
335
|
+
subject,
|
|
336
|
+
f"the decode route failed on the warm-up sample: {error!r}",
|
|
337
|
+
) from error
|
|
338
|
+
|
|
339
|
+
outcome = compare_facts(sample, facts)
|
|
340
|
+
if outcome.matches:
|
|
341
|
+
return
|
|
342
|
+
raise ReaderError(
|
|
343
|
+
"READER_WARMUP",
|
|
344
|
+
subject,
|
|
345
|
+
f"{sample.coordinate} no longer reproduces its known-good sample "
|
|
346
|
+
f"{sample.sample_name}: expected sha256 {sample.normalized_sha256}, observed sha256 "
|
|
347
|
+
f"{facts.normalized_sha256} ({outcome.describe()})",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def warm_up_sample(
|
|
352
|
+
family_id: str,
|
|
353
|
+
family_version: str,
|
|
354
|
+
*,
|
|
355
|
+
root: pathlib.Path | None = None,
|
|
356
|
+
) -> ReaderSample:
|
|
357
|
+
"""The one sample a refresh re-opens for this family, or a refusal.
|
|
358
|
+
|
|
359
|
+
Designation lives in the tree rather than in the caller, so "one known file" cannot vary
|
|
360
|
+
by who is asking. A family with no sample at all raises here too: an unwarmed family is
|
|
361
|
+
not a warmed one, and a check that quietly passed when it had nothing to check would be
|
|
362
|
+
worse than no check.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
coordinate = f"{family_id}@{family_version}"
|
|
366
|
+
recorded = samples_for(family_id, family_version, root=root)
|
|
367
|
+
designated = tuple(sample for sample in recorded if sample.warm_up)
|
|
368
|
+
if not designated:
|
|
369
|
+
detail = (
|
|
370
|
+
"ships no known-good sample, so there is nothing to warm up on"
|
|
371
|
+
if not recorded
|
|
372
|
+
else "designates no warm-up sample; set warm_up to true on exactly one of: "
|
|
373
|
+
+ ", ".join(sample.sample_name for sample in recorded)
|
|
374
|
+
)
|
|
375
|
+
raise ReaderError("READER_WARMUP", f"reader.warmup.{coordinate}", detail)
|
|
376
|
+
return designated[0]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def load_samples(root: pathlib.Path | None = None) -> tuple[ReaderSample, ...]:
|
|
380
|
+
"""Every sample in the tree, in coordinate order, or a refusal naming what is malformed.
|
|
381
|
+
|
|
382
|
+
``root`` defaults to the packaged tree. It is a parameter so a test can load a copied
|
|
383
|
+
tree; production reads the installed one and passes nothing.
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
base = SAMPLES_ROOT if root is None else pathlib.Path(root)
|
|
387
|
+
if not base.is_dir():
|
|
388
|
+
_refuse("reader.samples", f"{base} is not a sample tree")
|
|
389
|
+
loaded: list[ReaderSample] = []
|
|
390
|
+
for family_dir in _child_directories(base, base, "holds no Reader version directory"):
|
|
391
|
+
for version_dir in _child_directories(family_dir, base, "holds no sample directory"):
|
|
392
|
+
for sample_dir in _child_directories(version_dir, base, "holds no sample"):
|
|
393
|
+
loaded.append(_load_one(sample_dir, base))
|
|
394
|
+
_refuse_a_second_warm_up_sample(loaded)
|
|
395
|
+
return tuple(loaded)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _refuse_a_second_warm_up_sample(loaded: Sequence[ReaderSample]) -> None:
|
|
399
|
+
"""Two designated samples for one family is a malformed tree, refused when it loads.
|
|
400
|
+
|
|
401
|
+
Refused here rather than where the check runs, so the ambiguity cannot survive until a
|
|
402
|
+
refresh and then be resolved by whichever sample happened to sort first.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
designated: dict[str, list[str]] = {}
|
|
406
|
+
for sample in loaded:
|
|
407
|
+
if sample.warm_up:
|
|
408
|
+
designated.setdefault(sample.coordinate, []).append(sample.sample_name)
|
|
409
|
+
for coordinate, names in sorted(designated.items()):
|
|
410
|
+
if len(names) > 1:
|
|
411
|
+
_refuse(
|
|
412
|
+
f"reader.samples.{coordinate}",
|
|
413
|
+
"designates more than one warm-up sample: "
|
|
414
|
+
+ ", ".join(sorted(names))
|
|
415
|
+
+ "; one known file is a property of this tree, not of the caller",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def samples_for(
|
|
420
|
+
family_id: str,
|
|
421
|
+
family_version: str,
|
|
422
|
+
*,
|
|
423
|
+
root: pathlib.Path | None = None,
|
|
424
|
+
) -> tuple[ReaderSample, ...]:
|
|
425
|
+
"""The samples recorded for one exact family coordinate, and never a nearby one."""
|
|
426
|
+
|
|
427
|
+
return tuple(
|
|
428
|
+
sample
|
|
429
|
+
for sample in load_samples(root=root)
|
|
430
|
+
if (sample.family_id, sample.family_version) == (family_id, family_version)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _child_directories(
|
|
435
|
+
directory: pathlib.Path,
|
|
436
|
+
base: pathlib.Path,
|
|
437
|
+
complaint: str,
|
|
438
|
+
) -> tuple[pathlib.Path, ...]:
|
|
439
|
+
"""The sorted sub-directories of one level, refusing a level that holds none.
|
|
440
|
+
|
|
441
|
+
An empty level is refused rather than skipped: a sample tree missing its version layer
|
|
442
|
+
would otherwise load zero samples and report nothing, which is the silence the coverage
|
|
443
|
+
gate exists to break. Files at a level are ignored, so ``README.md`` can live beside the
|
|
444
|
+
families it documents.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
children = tuple(sorted(child for child in directory.iterdir() if child.is_dir()))
|
|
448
|
+
if not children and directory != base:
|
|
449
|
+
_refuse(f"reader.sample.{directory.relative_to(base).as_posix()}", complaint)
|
|
450
|
+
return children
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _load_one(directory: pathlib.Path, base: pathlib.Path) -> ReaderSample:
|
|
454
|
+
relative = directory.relative_to(base)
|
|
455
|
+
subject = f"reader.sample.{relative.as_posix()}"
|
|
456
|
+
document = _expectation_document(directory, subject)
|
|
457
|
+
|
|
458
|
+
family_id = identifier(document["family_id"], f"{subject}.family_id")
|
|
459
|
+
family_version = semver(document["family_version"], f"{subject}.family_version")
|
|
460
|
+
if (family_id, family_version) != (relative.parts[0], relative.parts[1]):
|
|
461
|
+
_refuse(
|
|
462
|
+
subject,
|
|
463
|
+
f"records {family_id}@{family_version}, which is not the identity its directory "
|
|
464
|
+
"states; the directory is the identity",
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
sample_file = document["sample_file"]
|
|
468
|
+
if (
|
|
469
|
+
not isinstance(sample_file, str)
|
|
470
|
+
or _SAMPLE_FILENAME.fullmatch(sample_file) is None
|
|
471
|
+
or sample_file == _EXPECTATION_FILENAME
|
|
472
|
+
):
|
|
473
|
+
_refuse(subject, "sample_file must be a bare filename in this directory")
|
|
474
|
+
sample_path = directory / sample_file
|
|
475
|
+
if not sample_path.is_file():
|
|
476
|
+
_refuse(subject, f"names {sample_file}, which is not in the sample directory")
|
|
477
|
+
|
|
478
|
+
warm_up_flag = document.get("warm_up", False)
|
|
479
|
+
if not isinstance(warm_up_flag, bool):
|
|
480
|
+
_refuse(subject, "warm_up must be true or false")
|
|
481
|
+
|
|
482
|
+
return ReaderSample(
|
|
483
|
+
family_id=family_id,
|
|
484
|
+
family_version=family_version,
|
|
485
|
+
sample_name=relative.parts[2],
|
|
486
|
+
sample_file=sample_file,
|
|
487
|
+
decode_options=MappingProxyType(dict(_decode_options(document, subject))),
|
|
488
|
+
content=sample_path.read_bytes(),
|
|
489
|
+
normalized_sha256=_digest(document["normalized_sha256"], subject),
|
|
490
|
+
row_count=_row_count(document["row_count"], subject),
|
|
491
|
+
column_names=_column_names(document["column_names"], subject),
|
|
492
|
+
flags=_flags(document["flags"], subject),
|
|
493
|
+
provenance=_provenance(document["provenance"], subject),
|
|
494
|
+
warm_up=warm_up_flag,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _expectation_document(directory: pathlib.Path, subject: str) -> dict[str, Any]:
|
|
499
|
+
path = directory / _EXPECTATION_FILENAME
|
|
500
|
+
if not path.is_file():
|
|
501
|
+
_refuse(
|
|
502
|
+
subject,
|
|
503
|
+
f"has no {_EXPECTATION_FILENAME}; a sample without its recorded answer is not a "
|
|
504
|
+
"known-good sample",
|
|
505
|
+
)
|
|
506
|
+
try:
|
|
507
|
+
document = json.loads(path.read_text(encoding="utf-8"))
|
|
508
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
509
|
+
_refuse(subject, f"has an unreadable {_EXPECTATION_FILENAME}: {error}")
|
|
510
|
+
if not isinstance(document, dict):
|
|
511
|
+
_refuse(subject, f"{_EXPECTATION_FILENAME} must be a JSON object")
|
|
512
|
+
if "environment" in document:
|
|
513
|
+
_refuse(
|
|
514
|
+
subject,
|
|
515
|
+
"carries an environment key: that is the golden machinery leaking in. A sample "
|
|
516
|
+
"records what a Reader answers, never the host it answered on",
|
|
517
|
+
)
|
|
518
|
+
unknown = sorted(str(key) for key in document if key not in _EXPECTATION_KEYS)
|
|
519
|
+
if unknown:
|
|
520
|
+
_refuse(subject, f"states a key the convention does not: {', '.join(unknown)}")
|
|
521
|
+
missing = sorted(_REQUIRED_KEYS - set(document))
|
|
522
|
+
if missing:
|
|
523
|
+
_refuse(subject, f"does not state: {', '.join(missing)}")
|
|
524
|
+
return document
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _decode_options(document: Mapping[str, Any], subject: str) -> Mapping[str, Any]:
|
|
528
|
+
"""The settings, admitted by building the pin a recipe would have sealed."""
|
|
529
|
+
|
|
530
|
+
options = document["decode_options"]
|
|
531
|
+
if not isinstance(options, Mapping):
|
|
532
|
+
_refuse(f"{subject}.decode_options", "must be an object")
|
|
533
|
+
return ReaderPin(document["family_id"], document["family_version"], options).decode_options
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _digest(value: Any, subject: str) -> str:
|
|
537
|
+
if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
|
|
538
|
+
_refuse(f"{subject}.normalized_sha256", "must be a lower-case hex sha256")
|
|
539
|
+
return value
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _row_count(value: Any, subject: str) -> int:
|
|
543
|
+
if type(value) is not int or value < 0:
|
|
544
|
+
_refuse(f"{subject}.row_count", "must be a non-negative integer")
|
|
545
|
+
return value
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _column_names(value: Any, subject: str) -> tuple[str, ...]:
|
|
549
|
+
if isinstance(value, str) or not isinstance(value, Sequence) or not value:
|
|
550
|
+
_refuse(f"{subject}.column_names", "must be a nonempty list of column names")
|
|
551
|
+
if any(not isinstance(name, str) or not name or "\x00" in name for name in value):
|
|
552
|
+
_refuse(f"{subject}.column_names", "must be nonempty text without a NUL byte")
|
|
553
|
+
if len(set(value)) != len(value):
|
|
554
|
+
_refuse(f"{subject}.column_names", "must not repeat a column name")
|
|
555
|
+
return tuple(value)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _flags(value: Any, subject: str) -> tuple[str, ...]:
|
|
559
|
+
if isinstance(value, str) or not isinstance(value, Sequence):
|
|
560
|
+
_refuse(f"{subject}.flags", "must be a list of decode flags")
|
|
561
|
+
unknown = sorted(str(flag) for flag in value if flag not in DECODE_FLAGS)
|
|
562
|
+
if unknown:
|
|
563
|
+
_refuse(f"{subject}.flags", f"names no such decode flag: {', '.join(unknown)}")
|
|
564
|
+
return tuple(sorted(set(value)))
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _provenance(value: Any, subject: str) -> SampleProvenance:
|
|
568
|
+
"""Admit the four reviewable facts, refusing a sample no one could review."""
|
|
569
|
+
|
|
570
|
+
where = f"{subject}.provenance"
|
|
571
|
+
if not isinstance(value, Mapping):
|
|
572
|
+
_refuse(where, "must be an object")
|
|
573
|
+
if sorted(str(key) for key in value) != sorted(_PROVENANCE_KEYS):
|
|
574
|
+
_refuse(where, f"must state exactly: {', '.join(sorted(_PROVENANCE_KEYS))}")
|
|
575
|
+
for key in _PROVENANCE_KEYS:
|
|
576
|
+
if not isinstance(value[key], str) or not value[key].strip():
|
|
577
|
+
_refuse(where, f"{key} must be a nonempty statement")
|
|
578
|
+
try:
|
|
579
|
+
datetime.date.fromisoformat(value["retrieved"])
|
|
580
|
+
except ValueError:
|
|
581
|
+
_refuse(where, "retrieved must be an ISO date, as YYYY-MM-DD")
|
|
582
|
+
return SampleProvenance(**{key: value[key] for key in _PROVENANCE_KEYS})
|