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,514 @@
|
|
|
1
|
+
"""The Toolbox: the closed table of Reader families the product ships with.
|
|
2
|
+
|
|
3
|
+
Resolution is on an exact ``(family_id, family_version)`` key. A near version is refused,
|
|
4
|
+
never substituted, because a recipe pins the code that decoded its bytes and a substituted
|
|
5
|
+
decoder would silently answer a different question. ``weather.grib2@1.0.0`` and
|
|
6
|
+
``weather.grib2@2.0.0`` are co-installed so each Recipe resolves its exact pin.
|
|
7
|
+
|
|
8
|
+
The table is closed and has no discovery path. Membership is exactly the frozen descriptors
|
|
9
|
+
written into ``_CERTIFIED_FAMILIES`` below: there is no plugin scan, entry point, environment
|
|
10
|
+
override, or recipe-controlled module. A descriptor imports its one literal module/factory only
|
|
11
|
+
after exact coordinate resolution. Adding a family is a source edit that goes through
|
|
12
|
+
Certification and review.
|
|
13
|
+
|
|
14
|
+
The table is deliberately not derived from ``formats``. The two answer different
|
|
15
|
+
questions: ``formats`` answers "what encoding may be sealed", and this table answers "what
|
|
16
|
+
code may run". A format may be sealable with no family that produces it, and a family may
|
|
17
|
+
be certified before any recipe pins it. Deriving one from the other would make each answer
|
|
18
|
+
the other's question.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import importlib
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
|
|
26
|
+
from mostlyright.data_harness.formats import DATA_FORMATS, READER_CONTRACT_VERSION
|
|
27
|
+
from mostlyright.data_harness.readers.contracts import (
|
|
28
|
+
ReaderBudgets,
|
|
29
|
+
ReaderError,
|
|
30
|
+
ReaderFamily,
|
|
31
|
+
bulk_default_budgets,
|
|
32
|
+
identifier,
|
|
33
|
+
semver,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = ["TOOLBOX", "ReaderRegistry", "certified_descriptors"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_SPREADSHEET_DEFAULT_BUDGETS = ReaderBudgets(
|
|
40
|
+
max_uncompressed_bytes=32 * 1024 * 1024,
|
|
41
|
+
max_declared_cells=250_000,
|
|
42
|
+
max_rows=250_001,
|
|
43
|
+
max_columns=256,
|
|
44
|
+
)
|
|
45
|
+
_JSON_DEFAULT_BUDGETS = bulk_default_budgets()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def spreadsheet_default_budgets() -> ReaderBudgets:
|
|
49
|
+
"""Return the one certified budget value shared by lazy and loaded XLSX metadata."""
|
|
50
|
+
|
|
51
|
+
return _SPREADSHEET_DEFAULT_BUDGETS
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# What a family must answer before it may enter the table. Checked at registration rather
|
|
55
|
+
# than at first use, so a mis-shaped family fails when the process starts and not when a
|
|
56
|
+
# refresh is halfway through.
|
|
57
|
+
_REQUIRED_MEMBERS = (
|
|
58
|
+
"family_id",
|
|
59
|
+
"family_version",
|
|
60
|
+
"contract_version",
|
|
61
|
+
"output_format",
|
|
62
|
+
"accepted_media_types",
|
|
63
|
+
"default_budgets",
|
|
64
|
+
"validate_options",
|
|
65
|
+
"decode",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class _FamilyDescriptor:
|
|
71
|
+
"""Static certified metadata plus one fixed lazy factory coordinate.
|
|
72
|
+
|
|
73
|
+
``settings_summary`` is the one sentence a recipe author reads about this coordinate. It is
|
|
74
|
+
carried here rather than typed into the skill's settings table because that table, the closed
|
|
75
|
+
coordinate tables in the suite, and this tuple were three hand-kept copies of one fact, and
|
|
76
|
+
adding a family meant remembering all of them. ``scripts/generate_registry_tables.py`` emits
|
|
77
|
+
every one of those restatements from this tuple, and a test in the ordinary collection fails
|
|
78
|
+
when a checked-in copy and this tuple disagree. It is inert metadata: nothing in resolution,
|
|
79
|
+
decoding, or admission reads it.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
family_id: str
|
|
83
|
+
family_version: str
|
|
84
|
+
module_name: str
|
|
85
|
+
factory_name: str
|
|
86
|
+
accepted_media_types: tuple[str, ...]
|
|
87
|
+
settings_summary: str
|
|
88
|
+
contract_version: str = READER_CONTRACT_VERSION
|
|
89
|
+
output_format: str = "csv"
|
|
90
|
+
default_budgets: ReaderBudgets = field(default_factory=ReaderBudgets)
|
|
91
|
+
optional_dependency: str | None = None
|
|
92
|
+
|
|
93
|
+
def __post_init__(self) -> None:
|
|
94
|
+
# A blank summary would emit a blank table cell rather than fail, so the emptiness is
|
|
95
|
+
# refused where the entry is written instead of surviving into a page somebody reads.
|
|
96
|
+
# The value has to *be* its one line, not merely split into one. ``splitlines`` honours
|
|
97
|
+
# every separator that would break a table row, including the vertical tab and the
|
|
98
|
+
# paragraph separator, but it drops a trailing one -- so a summary ending in a newline
|
|
99
|
+
# splits into a single line and would push the row after it out of the table anyway.
|
|
100
|
+
summary = self.settings_summary
|
|
101
|
+
lines = summary.splitlines() if isinstance(summary, str) else []
|
|
102
|
+
if (
|
|
103
|
+
not isinstance(summary, str)
|
|
104
|
+
or not summary.strip()
|
|
105
|
+
or "|" in summary
|
|
106
|
+
or len(lines) != 1
|
|
107
|
+
or lines[0] != summary
|
|
108
|
+
):
|
|
109
|
+
raise ReaderError(
|
|
110
|
+
"READER_CONTRACT",
|
|
111
|
+
"toolbox.family.settings_summary",
|
|
112
|
+
"must be one nonempty line holding no table separator",
|
|
113
|
+
)
|
|
114
|
+
# The factory coordinate is written verbatim into a checked-in Python table, so its shape
|
|
115
|
+
# is refused here rather than trusted. A value carrying a quote would change the emitted
|
|
116
|
+
# tuple's arity instead of failing, and this is the field that decides what gets imported.
|
|
117
|
+
if not all(part.isidentifier() for part in self.module_name.split(".")) or (
|
|
118
|
+
not self.factory_name.isidentifier()
|
|
119
|
+
):
|
|
120
|
+
raise ReaderError(
|
|
121
|
+
"READER_CONTRACT",
|
|
122
|
+
"toolbox.family.module_name",
|
|
123
|
+
"must be a dotted identifier naming one module and one factory",
|
|
124
|
+
)
|
|
125
|
+
# The other two fields reach the same emitted tuple through the same interpolation, and
|
|
126
|
+
# a quote in either would change that tuple's arity rather than fail. Validating only
|
|
127
|
+
# the import target would have been an arbitrary half of the same rule.
|
|
128
|
+
quoted = [
|
|
129
|
+
value
|
|
130
|
+
for value in (self.optional_dependency, *self.accepted_media_types)
|
|
131
|
+
if value is not None and ('"' in value or "\\" in value or value != value.strip())
|
|
132
|
+
]
|
|
133
|
+
if quoted:
|
|
134
|
+
raise ReaderError(
|
|
135
|
+
"READER_CONTRACT",
|
|
136
|
+
"toolbox.family.accepted_media_types",
|
|
137
|
+
"must carry no quote, backslash, or surrounding space",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def load(self) -> ReaderFamily:
|
|
141
|
+
"""Import the fixed factory, translating only optional absence/incompatibility."""
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
module = importlib.import_module(self.module_name)
|
|
145
|
+
factory = getattr(module, self.factory_name)
|
|
146
|
+
family = factory()
|
|
147
|
+
except (AttributeError, ImportError) as exc:
|
|
148
|
+
if self.optional_dependency is None:
|
|
149
|
+
raise
|
|
150
|
+
raise ReaderError(
|
|
151
|
+
"READER_DEPENDENCY_UNAVAILABLE",
|
|
152
|
+
"recipe.sources.family_id",
|
|
153
|
+
f"optional dependency {self.optional_dependency} is unavailable or incompatible "
|
|
154
|
+
f"for {self.family_id}@{self.family_version}",
|
|
155
|
+
) from exc
|
|
156
|
+
_validate_family(family)
|
|
157
|
+
for name in (
|
|
158
|
+
"family_id",
|
|
159
|
+
"family_version",
|
|
160
|
+
"contract_version",
|
|
161
|
+
"output_format",
|
|
162
|
+
"accepted_media_types",
|
|
163
|
+
"default_budgets",
|
|
164
|
+
):
|
|
165
|
+
if getattr(family, name) != getattr(self, name):
|
|
166
|
+
raise ReaderError(
|
|
167
|
+
"READER_CONTRACT",
|
|
168
|
+
f"toolbox.family.{name}",
|
|
169
|
+
"loaded family metadata differs from its certified lazy descriptor",
|
|
170
|
+
)
|
|
171
|
+
return family
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _validate_family(family: ReaderFamily | _FamilyDescriptor) -> tuple[str, str]:
|
|
175
|
+
"""Validate one concrete family or static descriptor and return its exact key."""
|
|
176
|
+
|
|
177
|
+
missing = [name for name in _REQUIRED_MEMBERS if not hasattr(family, name)]
|
|
178
|
+
if isinstance(family, _FamilyDescriptor):
|
|
179
|
+
missing = []
|
|
180
|
+
if missing:
|
|
181
|
+
raise ReaderError(
|
|
182
|
+
"READER_CONTRACT",
|
|
183
|
+
"toolbox.family",
|
|
184
|
+
f"does not answer the Reader contract: missing {', '.join(sorted(missing))}",
|
|
185
|
+
)
|
|
186
|
+
identifier(family.family_id, "toolbox.family.family_id")
|
|
187
|
+
semver(family.family_version, "toolbox.family.family_version")
|
|
188
|
+
if family.contract_version != READER_CONTRACT_VERSION:
|
|
189
|
+
raise ReaderError(
|
|
190
|
+
"READER_CONTRACT",
|
|
191
|
+
"toolbox.family.contract_version",
|
|
192
|
+
f"is not {READER_CONTRACT_VERSION}: a family built against another contract "
|
|
193
|
+
"cannot enter the table",
|
|
194
|
+
)
|
|
195
|
+
if family.output_format not in DATA_FORMATS:
|
|
196
|
+
raise ReaderError(
|
|
197
|
+
"READER_CONTRACT",
|
|
198
|
+
"toolbox.family.output_format",
|
|
199
|
+
"is not a format the table admits",
|
|
200
|
+
)
|
|
201
|
+
media_types = family.accepted_media_types
|
|
202
|
+
if (
|
|
203
|
+
not isinstance(media_types, tuple)
|
|
204
|
+
or not media_types
|
|
205
|
+
or any(not isinstance(item, str) or not item for item in media_types)
|
|
206
|
+
or tuple(sorted(set(media_types))) != media_types
|
|
207
|
+
):
|
|
208
|
+
raise ReaderError(
|
|
209
|
+
"READER_CONTRACT",
|
|
210
|
+
"toolbox.family.accepted_media_types",
|
|
211
|
+
"must be a nonempty sorted tuple of distinct media types",
|
|
212
|
+
)
|
|
213
|
+
if not isinstance(family.default_budgets, ReaderBudgets):
|
|
214
|
+
raise ReaderError(
|
|
215
|
+
"READER_CONTRACT",
|
|
216
|
+
"toolbox.family.default_budgets",
|
|
217
|
+
"must be a typed budget object",
|
|
218
|
+
)
|
|
219
|
+
return family.family_id, family.family_version
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ReaderRegistry:
|
|
223
|
+
"""The closed Reader allowlist, keyed on exact identity and exact version."""
|
|
224
|
+
|
|
225
|
+
def __init__(self, families: tuple[ReaderFamily | _FamilyDescriptor, ...] = ()) -> None:
|
|
226
|
+
self._families: dict[tuple[str, str], ReaderFamily | _FamilyDescriptor] = {}
|
|
227
|
+
for family in families:
|
|
228
|
+
self.register(family)
|
|
229
|
+
|
|
230
|
+
def register(self, family: ReaderFamily | _FamilyDescriptor) -> None:
|
|
231
|
+
"""Admit one certified family, refusing anything the contract does not allow."""
|
|
232
|
+
|
|
233
|
+
key = _validate_family(family)
|
|
234
|
+
if key in self._families:
|
|
235
|
+
raise ReaderError(
|
|
236
|
+
"READER_CONTRACT",
|
|
237
|
+
"toolbox",
|
|
238
|
+
f"family {key[0]}@{key[1]} is already registered",
|
|
239
|
+
)
|
|
240
|
+
self._families[key] = family
|
|
241
|
+
|
|
242
|
+
def resolve(self, family_id: str, family_version: str) -> ReaderFamily:
|
|
243
|
+
"""Return the one family registered under this exact coordinate.
|
|
244
|
+
|
|
245
|
+
There is no fallback. A version this table does not hold is refused by name, and
|
|
246
|
+
the refusal names the versions that are held, because "the recipe pins 1.0.1 and
|
|
247
|
+
this build ships 1.0.0" is the fix a person needs to read.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
family = self._families[(family_id, family_version)]
|
|
252
|
+
except KeyError:
|
|
253
|
+
available = sorted(
|
|
254
|
+
version for identity, version in self._families if identity == family_id
|
|
255
|
+
)
|
|
256
|
+
known = f"; this build ships {', '.join(available)}" if available else ""
|
|
257
|
+
raise ReaderError(
|
|
258
|
+
"READER_FAMILY_UNKNOWN",
|
|
259
|
+
"recipe.sources.family_id",
|
|
260
|
+
f"no certified Reader {family_id}@{family_version} is in the Toolbox{known}",
|
|
261
|
+
) from None
|
|
262
|
+
if isinstance(family, _FamilyDescriptor):
|
|
263
|
+
loaded = family.load()
|
|
264
|
+
self._families[(family_id, family_version)] = loaded
|
|
265
|
+
return loaded
|
|
266
|
+
return family
|
|
267
|
+
|
|
268
|
+
def families(self) -> tuple[tuple[str, str], ...]:
|
|
269
|
+
"""The sorted coordinates of every certified family, as a value and not the table."""
|
|
270
|
+
|
|
271
|
+
return tuple(sorted(self._families))
|
|
272
|
+
|
|
273
|
+
def widest_version(self, family_id: str) -> str:
|
|
274
|
+
"""The highest certified version of one family: the coordinate a new source should pin.
|
|
275
|
+
|
|
276
|
+
A weak-label widening lands as a new coordinate rather than an edit to an existing one,
|
|
277
|
+
so that an already-approved recipe cannot silently begin admitting responses its review
|
|
278
|
+
never saw. The cost of that rule is that the *base* version is the wrong default for a
|
|
279
|
+
source nobody has approved yet: pinning ``delimited_text@1.0.0`` for a brand-new source
|
|
280
|
+
refuses a publisher that merely declined to describe its file, and the author has to
|
|
281
|
+
know a version table to avoid it.
|
|
282
|
+
|
|
283
|
+
This is the one place that answers "which coordinate should a new pin name". Highest is
|
|
284
|
+
decided on the parsed numeric version and never on the string: ``families()`` is sorted
|
|
285
|
+
lexicographically, where ``1.10.0`` sorts *before* ``1.9.0``, so taking its last entry
|
|
286
|
+
would answer ``1.9.0`` for a family that had certified ``1.10.0``. No family carries a
|
|
287
|
+
double-digit component today, which is exactly why this would have gone unnoticed until
|
|
288
|
+
one did.
|
|
289
|
+
|
|
290
|
+
Choosing a coordinate is not the same as widening one. This picks among what review has
|
|
291
|
+
already certified; it can never admit a media type no certified family carries.
|
|
292
|
+
|
|
293
|
+
It answers the version question and not the fitness question, and the two come apart on a
|
|
294
|
+
major bump. A minor widening inherits ``decode`` untouched, so the highest version seals
|
|
295
|
+
the bytes the base version would have sealed and only admits more responses. A major one
|
|
296
|
+
need not: ``weather.grib2@2.0.0`` emits columns ``1.0.0`` does not, so the highest
|
|
297
|
+
coordinate there is a different output contract rather than a wider door onto the same
|
|
298
|
+
one. A caller pinning a new source still chooses deliberately for such a family; what
|
|
299
|
+
this removes is having to read a version table to avoid being refused for a label.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
versions = [version for identity, version in self.families() if identity == family_id]
|
|
303
|
+
if not versions:
|
|
304
|
+
raise ReaderError(
|
|
305
|
+
"READER_FAMILY_UNKNOWN",
|
|
306
|
+
"toolbox.family",
|
|
307
|
+
f"{family_id} is not a certified Reader family",
|
|
308
|
+
)
|
|
309
|
+
return max(versions, key=lambda version: tuple(int(part) for part in version.split(".")))
|
|
310
|
+
|
|
311
|
+
def accepted_media_types(self) -> tuple[str, ...]:
|
|
312
|
+
"""Every media type any certified family admits as input, sorted and de-duplicated.
|
|
313
|
+
|
|
314
|
+
A property of the Toolbox rather than of any caller, and the one derivation of it.
|
|
315
|
+
A fetch for a pinned source has to admit media types no wire format claims -- a zipped
|
|
316
|
+
source arrives as an archive -- and the answer to "which ones" is this table's to give.
|
|
317
|
+
Restating a media type outside the module that owns it is the drift the format table's
|
|
318
|
+
anti-duplication gate exists to catch, and it scans media types as well as format names
|
|
319
|
+
(the format table's own; a Reader-only media type such as an archive's is bound by each
|
|
320
|
+
family's certified descriptor and the load-time equality check instead).
|
|
321
|
+
|
|
322
|
+
The union is the outer bound. A single pinned fetch narrows further, to the one
|
|
323
|
+
family's own declaration, because a source pinned to a zip Reader has no business
|
|
324
|
+
admitting a spreadsheet.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
return tuple(
|
|
328
|
+
sorted(
|
|
329
|
+
{
|
|
330
|
+
media_type
|
|
331
|
+
for family in self._families.values()
|
|
332
|
+
for media_type in family.accepted_media_types
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Certified Reader families. Membership requires adversarial review, an exact decoder pin, and
|
|
339
|
+
# known-good samples. The tuple has no provisional state.
|
|
340
|
+
_CERTIFIED_FAMILIES: tuple[_FamilyDescriptor, ...] = (
|
|
341
|
+
_FamilyDescriptor(
|
|
342
|
+
"archive.zip",
|
|
343
|
+
"1.0.0",
|
|
344
|
+
"mostlyright.data_harness.readers.containers",
|
|
345
|
+
"ZipReader",
|
|
346
|
+
("application/x-zip-compressed", "application/zip"),
|
|
347
|
+
settings_summary="`member`; `member_format` (`csv` or `tsv`); optional `header: true`.",
|
|
348
|
+
default_budgets=bulk_default_budgets(),
|
|
349
|
+
),
|
|
350
|
+
_FamilyDescriptor(
|
|
351
|
+
"archive.zip",
|
|
352
|
+
"1.1.0",
|
|
353
|
+
"mostlyright.data_harness.readers.containers",
|
|
354
|
+
"ZipReaderV1_1",
|
|
355
|
+
("application/x-zip-compressed", "application/zip"),
|
|
356
|
+
settings_summary="Same, plus `member_format: semicolon` for publishers such as DWD.",
|
|
357
|
+
default_budgets=bulk_default_budgets(),
|
|
358
|
+
),
|
|
359
|
+
_FamilyDescriptor(
|
|
360
|
+
"archive.zip",
|
|
361
|
+
"1.2.0",
|
|
362
|
+
"mostlyright.data_harness.readers.containers",
|
|
363
|
+
"ZipReaderV1_2",
|
|
364
|
+
("application/octet-stream", "application/x-zip-compressed", "application/zip"),
|
|
365
|
+
settings_summary=(
|
|
366
|
+
"Same as `archive.zip@1.1.0`; also accepts a response served as "
|
|
367
|
+
"`application/octet-stream`."
|
|
368
|
+
),
|
|
369
|
+
default_budgets=bulk_default_budgets(),
|
|
370
|
+
),
|
|
371
|
+
_FamilyDescriptor(
|
|
372
|
+
"archive.gzip",
|
|
373
|
+
"1.0.0",
|
|
374
|
+
"mostlyright.data_harness.readers.containers",
|
|
375
|
+
"GzipReader",
|
|
376
|
+
("application/gzip", "application/x-gzip"),
|
|
377
|
+
settings_summary="`member_format`; optional `header: true`. `member` is invalid.",
|
|
378
|
+
default_budgets=bulk_default_budgets(),
|
|
379
|
+
),
|
|
380
|
+
_FamilyDescriptor(
|
|
381
|
+
"archive.gzip",
|
|
382
|
+
"1.1.0",
|
|
383
|
+
"mostlyright.data_harness.readers.containers",
|
|
384
|
+
"GzipReaderV1_1",
|
|
385
|
+
("application/gzip", "application/octet-stream", "application/x-gzip"),
|
|
386
|
+
settings_summary="Same; also accepts a response served as `application/octet-stream`.",
|
|
387
|
+
default_budgets=bulk_default_budgets(),
|
|
388
|
+
),
|
|
389
|
+
_FamilyDescriptor(
|
|
390
|
+
"archive.tar",
|
|
391
|
+
"1.0.0",
|
|
392
|
+
"mostlyright.data_harness.readers.containers",
|
|
393
|
+
"TarReader",
|
|
394
|
+
("application/x-tar",),
|
|
395
|
+
settings_summary="Same as `archive.zip@1.0.0`.",
|
|
396
|
+
default_budgets=bulk_default_budgets(),
|
|
397
|
+
),
|
|
398
|
+
_FamilyDescriptor(
|
|
399
|
+
"archive.tar",
|
|
400
|
+
"1.1.0",
|
|
401
|
+
"mostlyright.data_harness.readers.containers",
|
|
402
|
+
"TarReaderV1_1",
|
|
403
|
+
("application/octet-stream", "application/x-tar"),
|
|
404
|
+
settings_summary="Same; also accepts a response served as `application/octet-stream`.",
|
|
405
|
+
default_budgets=bulk_default_budgets(),
|
|
406
|
+
),
|
|
407
|
+
_FamilyDescriptor(
|
|
408
|
+
"spreadsheet.xlsx",
|
|
409
|
+
"1.0.0",
|
|
410
|
+
"mostlyright.data_harness.readers.spreadsheet",
|
|
411
|
+
"SpreadsheetReader",
|
|
412
|
+
("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",),
|
|
413
|
+
settings_summary=(
|
|
414
|
+
"`sheet`, `header_row`, and `range`. The heading row must be the first row of the "
|
|
415
|
+
"range."
|
|
416
|
+
),
|
|
417
|
+
default_budgets=spreadsheet_default_budgets(),
|
|
418
|
+
),
|
|
419
|
+
_FamilyDescriptor(
|
|
420
|
+
"spreadsheet.xlsx",
|
|
421
|
+
"1.1.0",
|
|
422
|
+
"mostlyright.data_harness.readers.spreadsheet",
|
|
423
|
+
"SpreadsheetReaderV1_1",
|
|
424
|
+
(
|
|
425
|
+
"application/octet-stream",
|
|
426
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
427
|
+
),
|
|
428
|
+
settings_summary="Same; also accepts a response served as `application/octet-stream`.",
|
|
429
|
+
default_budgets=spreadsheet_default_budgets(),
|
|
430
|
+
),
|
|
431
|
+
_FamilyDescriptor(
|
|
432
|
+
"delimited_text",
|
|
433
|
+
"1.0.0",
|
|
434
|
+
"mostlyright.data_harness.readers.delimited",
|
|
435
|
+
"DelimitedTextReader",
|
|
436
|
+
("text/csv", "text/tab-separated-values"),
|
|
437
|
+
settings_summary=(
|
|
438
|
+
"`delimiter` (comma or tab); optional `header: true`; optional `encoding: utf-8`."
|
|
439
|
+
),
|
|
440
|
+
default_budgets=bulk_default_budgets(),
|
|
441
|
+
),
|
|
442
|
+
_FamilyDescriptor(
|
|
443
|
+
"delimited_text",
|
|
444
|
+
"1.1.0",
|
|
445
|
+
"mostlyright.data_harness.readers.delimited",
|
|
446
|
+
"DelimitedTextReaderV1_1",
|
|
447
|
+
("text/csv", "text/plain", "text/tab-separated-values"),
|
|
448
|
+
settings_summary=(
|
|
449
|
+
"Same; also accepts a response served as `text/plain` (common for public agency CSV "
|
|
450
|
+
"such as IEM)."
|
|
451
|
+
),
|
|
452
|
+
default_budgets=bulk_default_budgets(),
|
|
453
|
+
),
|
|
454
|
+
_FamilyDescriptor(
|
|
455
|
+
"json.tabular",
|
|
456
|
+
"1.0.0",
|
|
457
|
+
"mostlyright.data_harness.readers.json_tabular",
|
|
458
|
+
"JsonTabularReader",
|
|
459
|
+
("application/json", "application/x-ndjson"),
|
|
460
|
+
settings_summary=(
|
|
461
|
+
"`columns` with output names and RFC 6901 pointers; optional `document_format` (`json` "
|
|
462
|
+
"or `ndjson`), `records_pointer`, and sequential `expand` pointers. Columns are "
|
|
463
|
+
"required unless they state `required: false`."
|
|
464
|
+
),
|
|
465
|
+
default_budgets=_JSON_DEFAULT_BUDGETS,
|
|
466
|
+
),
|
|
467
|
+
_FamilyDescriptor(
|
|
468
|
+
"json.tabular",
|
|
469
|
+
"1.1.0",
|
|
470
|
+
"mostlyright.data_harness.readers.json_tabular",
|
|
471
|
+
"JsonTabularReaderV1_1",
|
|
472
|
+
("application/json", "application/x-ndjson", "text/plain"),
|
|
473
|
+
settings_summary=(
|
|
474
|
+
"Same; also accepts a response served as `text/plain` (common for public agency JSON "
|
|
475
|
+
"and NDJSON)."
|
|
476
|
+
),
|
|
477
|
+
default_budgets=_JSON_DEFAULT_BUDGETS,
|
|
478
|
+
),
|
|
479
|
+
_FamilyDescriptor(
|
|
480
|
+
"weather.grib2",
|
|
481
|
+
"1.0.0",
|
|
482
|
+
"mostlyright.data_harness.readers.grib2.decode",
|
|
483
|
+
"WeatherGrib2Reader",
|
|
484
|
+
("application/octet-stream", "application/wmo-grib2"),
|
|
485
|
+
settings_summary="`variable`, `points`, and `admission_allowlist_version`.",
|
|
486
|
+
optional_dependency="mostlyright-grib",
|
|
487
|
+
),
|
|
488
|
+
_FamilyDescriptor(
|
|
489
|
+
"weather.grib2",
|
|
490
|
+
"2.0.0",
|
|
491
|
+
"mostlyright.data_harness.readers.grib2.decode",
|
|
492
|
+
"WeatherGrib2ReaderV2",
|
|
493
|
+
("application/octet-stream", "application/wmo-grib2"),
|
|
494
|
+
settings_summary=(
|
|
495
|
+
"Same settings as 1.0.0; output also includes time, level, matched coordinates, and "
|
|
496
|
+
"message digest."
|
|
497
|
+
),
|
|
498
|
+
optional_dependency="mostlyright-grib",
|
|
499
|
+
),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
TOOLBOX = ReaderRegistry(_CERTIFIED_FAMILIES)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def certified_descriptors() -> tuple[_FamilyDescriptor, ...]:
|
|
506
|
+
"""The frozen certified descriptors, in table order, without loading a single decoder.
|
|
507
|
+
|
|
508
|
+
``TOOLBOX.resolve`` imports the family it answers with, which for ``weather.grib2`` needs an
|
|
509
|
+
optional binding that a plain install does not carry. Everything restated about a coordinate
|
|
510
|
+
-- its media types, its factory, the sentence a recipe author reads -- is already on the
|
|
511
|
+
frozen descriptor, so the table generator reads it from here and never imports anything.
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
return _CERTIFIED_FAMILIES
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Known-good samples
|
|
2
|
+
|
|
3
|
+
A known-good sample is a real file with its correct answer written down beside it. Certification
|
|
4
|
+
requires one: a Reader lands with samples or it does not land.
|
|
5
|
+
|
|
6
|
+
These samples have two consumers and one purpose. CI decodes every sample on every leg and fails
|
|
7
|
+
if any answer has moved. A refresh re-opens one designated sample before it fetches anything, and
|
|
8
|
+
halts if the answer has moved. Both read the bytes and the recorded answer from this tree through
|
|
9
|
+
one loader (`readers/samples.py`) and compare through one function, so the number CI asserts and
|
|
10
|
+
the number a refresh checks against cannot drift apart. Two copies of one constant is the defect
|
|
11
|
+
this convention exists to prevent.
|
|
12
|
+
|
|
13
|
+
The tree ships inside the package rather than under `tests/`, because the warm-up check runs in
|
|
14
|
+
production and its sample must be installed with the wheel.
|
|
15
|
+
|
|
16
|
+
## Layout
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
samples/<family_id>/<family_version>/<sample_name>/
|
|
20
|
+
<the sample file under its natural name>
|
|
21
|
+
expected.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
One directory per sample. The directory names are the identity: an `expected.json` that states a
|
|
25
|
+
`family_id` or `family_version` its directory does not is refused rather than believed. A sample
|
|
26
|
+
directory with no `expected.json` is a failure that names the directory; it is never a skip.
|
|
27
|
+
|
|
28
|
+
## expected.json
|
|
29
|
+
|
|
30
|
+
The key set is closed. Every key below is required except `warm_up`, and a key outside the set is
|
|
31
|
+
refused by name.
|
|
32
|
+
|
|
33
|
+
| Key | Meaning |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `family_id` | The Reader that opens this file. Must equal the directory. |
|
|
36
|
+
| `family_version` | The exact version of that Reader. Must equal the directory. |
|
|
37
|
+
| `decode_options` | The settings the Reader runs with, exactly as a recipe would pin them. |
|
|
38
|
+
| `sample_file` | The sample's filename. A bare name in this directory; never a path. |
|
|
39
|
+
| `normalized_sha256` | The sha256 of the Reader's output bytes — the sealed form, not the input. |
|
|
40
|
+
| `row_count` | How many rows the Reader produces. |
|
|
41
|
+
| `column_names` | The column names it produces, in order. |
|
|
42
|
+
| `flags` | The facts the decode reports about itself, from the closed decode vocabulary. |
|
|
43
|
+
| `provenance` | Where this sample came from and what it proves. See below. |
|
|
44
|
+
| `warm_up` | Optional, default false. Marks the one sample a refresh re-opens. |
|
|
45
|
+
|
|
46
|
+
Exactly one sample per Reader version sets `warm_up`: it is the file a refresh re-opens before it
|
|
47
|
+
fetches anything. A Reader that designates none cannot be warmed up, and the warm-up check refuses
|
|
48
|
+
rather than passing — an unwarmed Reader is not a warmed one. A Reader that designates two is
|
|
49
|
+
refused when the tree loads, because "one known file" is a property of this tree and not of
|
|
50
|
+
whoever calls the check.
|
|
51
|
+
|
|
52
|
+
### provenance
|
|
53
|
+
|
|
54
|
+
Four facts, no more and no fewer:
|
|
55
|
+
|
|
56
|
+
| Key | Meaning |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `origin` | Where the bytes came from: a URL, or `authored for this test`. |
|
|
59
|
+
| `retrieved` | The date they were obtained, as `YYYY-MM-DD`. |
|
|
60
|
+
| `licence` | The terms the bytes may be redistributed under. |
|
|
61
|
+
| `why` | One sentence on what this sample proves. |
|
|
62
|
+
|
|
63
|
+
A sample with no provenance cannot pass Certification, because there is nothing to review. The
|
|
64
|
+
loader refuses one, so a sample cannot enter this tree without a claim a person can check.
|
|
65
|
+
|
|
66
|
+
## Size discipline
|
|
67
|
+
|
|
68
|
+
Samples are committed bytes that ship in the wheel and are decoded before every refresh. Keep each
|
|
69
|
+
one under 256 KiB. A larger sample is admissible only when the Reader's own plan records why a
|
|
70
|
+
smaller one could not prove the same thing.
|
|
71
|
+
|
|
72
|
+
That "only when" is checkable rather than a habit. `tests/test_reader_samples.py` holds
|
|
73
|
+
`_OVERSIZE_BY_REVIEW`, one entry per oversize sample, each a path and the written reason. A sample
|
|
74
|
+
over the ceiling with no entry fails; an entry naming a file that is absent, or one that now fits,
|
|
75
|
+
fails too. So the ceiling still applies to every sample that is not on the list, and putting one on
|
|
76
|
+
it is a reviewed source edit with the reason attached — not a raised ceiling.
|
|
77
|
+
|
|
78
|
+
## Known-good samples are not goldens
|
|
79
|
+
|
|
80
|
+
This is a rule with a reason, and the reason is the part that matters.
|
|
81
|
+
|
|
82
|
+
Golden digests in this repository are minted on one authority: the x86_64 Linux `verify` job. Every
|
|
83
|
+
golden-digest assertion therefore sits behind a check on the running machine's architecture, and
|
|
84
|
+
skips when it does not match. That is correct for goldens and wrong for samples: a sample assertion
|
|
85
|
+
built the same way would skip on every arm64 development host, leaving the gate live on CI alone
|
|
86
|
+
while still reporting green on the machine a person is actually working on. (Until ADR 0021
|
|
87
|
+
ADR 0021 there was a third case, the `macos-14` parity leg, which made it one live leg out of
|
|
88
|
+
three; that leg retired with the local lane and the argument did not need it.)
|
|
89
|
+
|
|
90
|
+
So, concretely:
|
|
91
|
+
|
|
92
|
+
- Samples live here, never under `tests/fixtures/goldens/`.
|
|
93
|
+
- Samples are never produced by `scripts/mint_goldens.py`, which has no discover-and-overwrite mode
|
|
94
|
+
by design and knows nothing about this tree.
|
|
95
|
+
- An `expected.json` carrying an `environment` key is refused by name. An identity stamp is how a
|
|
96
|
+
golden records the machine that produced it, and a sample that recorded one would be asserting a
|
|
97
|
+
fact about a host instead of a fact about a Reader.
|
|
98
|
+
- Sample assertions carry no architecture guard and no skip. They run, and can fail, everywhere.
|
|
99
|
+
|
|
100
|
+
A sample's answer is a property of the Reader and the bytes. If it depends on the machine, the
|
|
101
|
+
Reader is not deterministic and that is the finding, not an excuse to gate the assertion.
|
|
102
|
+
|
|
103
|
+
## Adding a sample
|
|
104
|
+
|
|
105
|
+
1. Put the file and its `expected.json` in a new directory under the Reader's version.
|
|
106
|
+
2. Record the answer by decoding through the Reader itself — never by copying a number from a failing
|
|
107
|
+
test. A recorded answer that came from an observed failure records the bug.
|
|
108
|
+
3. State the provenance, including what the sample proves that the existing ones do not.
|
|
109
|
+
4. If it is the sample a refresh should warm up on, set `warm_up` to true and clear the flag from the
|
|
110
|
+
previous one.
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"column_names": [
|
|
3
|
+
"city",
|
|
4
|
+
"population",
|
|
5
|
+
"note"
|
|
6
|
+
],
|
|
7
|
+
"decode_options": {
|
|
8
|
+
"header": true,
|
|
9
|
+
"member_format": "csv"
|
|
10
|
+
},
|
|
11
|
+
"family_id": "archive.gzip",
|
|
12
|
+
"family_version": "1.0.0",
|
|
13
|
+
"flags": [],
|
|
14
|
+
"normalized_sha256": "09ffd82e7942d837974824bd381d76ad9fee3f916f51b9217e5df0cf034e64dd",
|
|
15
|
+
"provenance": {
|
|
16
|
+
"licence": "project-owned test fixture, redistributable with the package",
|
|
17
|
+
"origin": "authored for this test",
|
|
18
|
+
"retrieved": "2026-08-07",
|
|
19
|
+
"why": "proves the degenerate form of clause 2: one gzip stream with no member name, whose inner form the recipe still states, carrying a missing cell so the empty cell survives the round trip through the canonical emitter"
|
|
20
|
+
},
|
|
21
|
+
"row_count": 2,
|
|
22
|
+
"sample_file": "cities.csv.gz",
|
|
23
|
+
"warm_up": true
|
|
24
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"column_names": [
|
|
3
|
+
"city",
|
|
4
|
+
"population",
|
|
5
|
+
"note"
|
|
6
|
+
],
|
|
7
|
+
"decode_options": {
|
|
8
|
+
"header": true,
|
|
9
|
+
"member_format": "csv"
|
|
10
|
+
},
|
|
11
|
+
"family_id": "archive.gzip",
|
|
12
|
+
"family_version": "1.1.0",
|
|
13
|
+
"flags": [],
|
|
14
|
+
"normalized_sha256": "09ffd82e7942d837974824bd381d76ad9fee3f916f51b9217e5df0cf034e64dd",
|
|
15
|
+
"provenance": {
|
|
16
|
+
"licence": "project-owned test fixture, redistributable with the package",
|
|
17
|
+
"origin": "authored for this test",
|
|
18
|
+
"retrieved": "2026-08-07",
|
|
19
|
+
"why": "proves the degenerate form of clause 2 \u2014 one gzip stream with no member name, whose inner form the recipe still states, carrying a missing cell so the empty cell survives the round trip through the canonical emitter \u2014 carried byte-for-byte from the archive.gzip@1.0.0 warm-up sample because this coordinate widens only the label a response may arrive under and inherits the decode unchanged."
|
|
20
|
+
},
|
|
21
|
+
"row_count": 2,
|
|
22
|
+
"sample_file": "cities.csv.gz",
|
|
23
|
+
"warm_up": true
|
|
24
|
+
}
|