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,825 @@
|
|
|
1
|
+
"""Versioned catalog facts about one public source.
|
|
2
|
+
|
|
3
|
+
A ``CatalogEntry`` names one public source and carries the facts an agent needs to choose it:
|
|
4
|
+
coverage and cadence (through ``SourceObservation``), payload formats, access shape, rights
|
|
5
|
+
evidence, published column and vocabulary names, and the quirks a reader will trip over. Its
|
|
6
|
+
identity reads ``faostat_qcl@v3`` -- an entry id plus a version that is chained to its
|
|
7
|
+
predecessor's canonical **facts** digest, so the version cannot drift from the evidence and the
|
|
8
|
+
chain reads the same before and after the catalog is sealed.
|
|
9
|
+
|
|
10
|
+
Three properties of this module are load-bearing and must not be relaxed.
|
|
11
|
+
|
|
12
|
+
**No float, anywhere.** ``canonical_json_bytes`` refuses every non-integer JSON number, and
|
|
13
|
+
``_CanonicalContract.digest`` routes through it. Embedding vectors are therefore integer-quantized
|
|
14
|
+
and referenced by digest through ``LayerVectorReference`` rather than inlined; that also keeps an
|
|
15
|
+
entry far below the canonical member cap.
|
|
16
|
+
|
|
17
|
+
**No tenant data, by construction.** There is deliberately no field able to hold a dataset, a
|
|
18
|
+
recipe, a Reader, a row sample, or a profile drawn from a tenant's private source. Adding one
|
|
19
|
+
would break the tenancy rule at the type level. ``declared_columns`` and ``declared_vocabulary``
|
|
20
|
+
hold *published names only* -- the headers a source documents and the item, category, or unit
|
|
21
|
+
terms it publishes -- never values read out of anyone's data. Both are bounded, and the bound is
|
|
22
|
+
part of the defence: a name list capped at 256 and 512 entries of 128 characters cannot smuggle a
|
|
23
|
+
dataset. The ``profiles`` embedding *layer* describes the public source's shape and is sealed as
|
|
24
|
+
an integer vector reference, never as data.
|
|
25
|
+
|
|
26
|
+
**No public citation, no entry.** An entry cannot be constructed without at least one
|
|
27
|
+
``EvidenceReference``, whose ``uri`` must already be HTTPS. A fact nobody can cite is not a
|
|
28
|
+
catalog fact.
|
|
29
|
+
|
|
30
|
+
Rights are never adjudicated here. ``sources.rights_rule.rights_disposition`` is the one authority
|
|
31
|
+
that decides what a rights status means.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import re
|
|
37
|
+
from collections.abc import Mapping
|
|
38
|
+
from dataclasses import dataclass, replace
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from mostlyright.data_harness.canonical import canonical_sha256
|
|
42
|
+
from mostlyright.data_harness.sources.contracts import (
|
|
43
|
+
DATA_FORMATS,
|
|
44
|
+
MAX_EVIDENCE,
|
|
45
|
+
MAX_SAFE_INTEGER,
|
|
46
|
+
EvidenceReference,
|
|
47
|
+
LicenseObligation,
|
|
48
|
+
RightsEvidence,
|
|
49
|
+
SourceContractError,
|
|
50
|
+
SourceObservation,
|
|
51
|
+
_CanonicalContract,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
CATALOG_ENTRY_CONTRACT_VERSION = "harness-source-catalog-entry.v1"
|
|
55
|
+
CATALOG_INDEX_CONTRACT_VERSION = "harness-source-catalog-index.v1"
|
|
56
|
+
|
|
57
|
+
# The four retrieval layers, in order. Retrieval finds; facts gate.
|
|
58
|
+
EMBEDDING_LAYERS = ("description", "metadata", "columns", "profiles")
|
|
59
|
+
|
|
60
|
+
# Every access kind is an open-access shape by construction. There is deliberately no
|
|
61
|
+
# authenticated member: harvesters cover open endpoints only, and a 401/403 is a typed refusal
|
|
62
|
+
# naming the parked authenticated-adapter item rather than a credential prompt.
|
|
63
|
+
ACCESS_KINDS = frozenset({"bulk_download", "open_api", "open_catalog"})
|
|
64
|
+
|
|
65
|
+
# One sealed quantization scheme. A changed scale silently changes retrieval, so the scheme is
|
|
66
|
+
# named in the entry and verified when a sealed catalog is loaded.
|
|
67
|
+
QUANTIZATIONS = frozenset({"int32_fixed_point_10000"})
|
|
68
|
+
|
|
69
|
+
MAX_QUIRKS = 32
|
|
70
|
+
MAX_QUIRK_DETAIL = 500
|
|
71
|
+
MAX_SPATIAL_SCOPE = 64
|
|
72
|
+
MAX_ENTRY_VERSION = 10_000
|
|
73
|
+
MAX_DECLARED_COLUMNS = 256
|
|
74
|
+
MAX_DECLARED_VOCABULARY = 512
|
|
75
|
+
MAX_DECLARED_NAME = 128
|
|
76
|
+
MAX_DESCRIPTION = 4_000
|
|
77
|
+
MAX_DIMENSIONS = 4_096
|
|
78
|
+
|
|
79
|
+
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$")
|
|
80
|
+
_SEMVER = re.compile(r"^[1-9][0-9]*\.[0-9]+\.[0-9]+$")
|
|
81
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class SourceQuirk(_CanonicalContract):
|
|
86
|
+
"""One published-data trap a reader of this source will hit.
|
|
87
|
+
|
|
88
|
+
The worked examples are real: FAOSTAT publishes code columns with a leading apostrophe (an
|
|
89
|
+
Excel text-guard artifact) and ships its CSV with a UTF-8 byte-order mark. Both silently
|
|
90
|
+
corrupt a naive read, and both are facts about the public source rather than about anyone's
|
|
91
|
+
data.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
code: str
|
|
95
|
+
detail: str
|
|
96
|
+
|
|
97
|
+
def __post_init__(self) -> None:
|
|
98
|
+
_identifier(self.code, "quirk.code")
|
|
99
|
+
_text(self.detail, "quirk.detail", maximum=MAX_QUIRK_DETAIL)
|
|
100
|
+
|
|
101
|
+
def to_dict(self) -> dict[str, str]:
|
|
102
|
+
return {"code": self.code, "detail": self.detail}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class LayerVectorReference(_CanonicalContract):
|
|
107
|
+
"""A sealed integer embedding vector, referenced by digest and never inlined.
|
|
108
|
+
|
|
109
|
+
Carrying the vector itself would put thousands of numbers inside a digested contract. Instead
|
|
110
|
+
the entry names the layer, the backend that produced the vector, the quantization scheme, the
|
|
111
|
+
dimension count, and the SHA-256 of the sealed bytes -- the same shape ``SnapshotReference``
|
|
112
|
+
already uses for sealed payloads.
|
|
113
|
+
|
|
114
|
+
``entry_facts_digest`` is what binds the vector to the facts it was computed from. Without it a
|
|
115
|
+
vector is just four self-consistent numbers: a sealed catalog assembled from entries carrying
|
|
116
|
+
some *other* entry's vectors passes every integrity check -- four layers present, one backend
|
|
117
|
+
identity, files present and digest-matching -- and then ranks the wrong source on the wrong
|
|
118
|
+
text, with no error at any point. That is the one failure mode of this module that is silent,
|
|
119
|
+
so the binding is a field rather than a convention. Checking it costs a digest comparison and
|
|
120
|
+
computes no embedding, which is what keeps it legal on the query path.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
layer: str
|
|
124
|
+
backend_id: str
|
|
125
|
+
backend_version: str
|
|
126
|
+
dimensions: int
|
|
127
|
+
quantization: str
|
|
128
|
+
vector_sha256: str
|
|
129
|
+
entry_facts_digest: str
|
|
130
|
+
|
|
131
|
+
def __post_init__(self) -> None:
|
|
132
|
+
_choice(self.layer, frozenset(EMBEDDING_LAYERS), "vector.layer")
|
|
133
|
+
_identifier(self.backend_id, "vector.backend_id")
|
|
134
|
+
_semver(self.backend_version, "vector.backend_version")
|
|
135
|
+
_integer(self.dimensions, "vector.dimensions", minimum=1, maximum=MAX_DIMENSIONS)
|
|
136
|
+
_choice(self.quantization, QUANTIZATIONS, "vector.quantization")
|
|
137
|
+
_digest(self.vector_sha256, "vector.vector_sha256")
|
|
138
|
+
_digest(self.entry_facts_digest, "vector.entry_facts_digest")
|
|
139
|
+
|
|
140
|
+
def to_dict(self) -> dict[str, Any]:
|
|
141
|
+
return {
|
|
142
|
+
"layer": self.layer,
|
|
143
|
+
"backend_id": self.backend_id,
|
|
144
|
+
"backend_version": self.backend_version,
|
|
145
|
+
"dimensions": self.dimensions,
|
|
146
|
+
"quantization": self.quantization,
|
|
147
|
+
"vector_sha256": self.vector_sha256,
|
|
148
|
+
"entry_facts_digest": self.entry_facts_digest,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class CatalogEntry(_CanonicalContract):
|
|
154
|
+
"""One versioned public source, described entirely by citable facts."""
|
|
155
|
+
|
|
156
|
+
entry_id: str
|
|
157
|
+
entry_version: int
|
|
158
|
+
publisher: str
|
|
159
|
+
title: str
|
|
160
|
+
description: str
|
|
161
|
+
spatial_scope: tuple[str, ...]
|
|
162
|
+
data_formats: tuple[str, ...]
|
|
163
|
+
access_kind: str
|
|
164
|
+
authentication_required: bool
|
|
165
|
+
observation: SourceObservation
|
|
166
|
+
rights: RightsEvidence
|
|
167
|
+
quirks: tuple[SourceQuirk, ...] = ()
|
|
168
|
+
declared_columns: tuple[str, ...] = ()
|
|
169
|
+
declared_vocabulary: tuple[str, ...] = ()
|
|
170
|
+
declared_row_count: int | None = None
|
|
171
|
+
evidence: tuple[EvidenceReference, ...] = ()
|
|
172
|
+
vectors: tuple[LayerVectorReference, ...] = ()
|
|
173
|
+
previous_digest: str | None = None
|
|
174
|
+
schema_version: str = CATALOG_ENTRY_CONTRACT_VERSION
|
|
175
|
+
|
|
176
|
+
def __post_init__(self) -> None:
|
|
177
|
+
if self.schema_version != CATALOG_ENTRY_CONTRACT_VERSION:
|
|
178
|
+
raise SourceContractError(
|
|
179
|
+
"VERSION",
|
|
180
|
+
"catalog_entry.schema_version",
|
|
181
|
+
f"must equal {CATALOG_ENTRY_CONTRACT_VERSION!r}",
|
|
182
|
+
)
|
|
183
|
+
_identifier(self.entry_id, "catalog_entry.entry_id")
|
|
184
|
+
if type(self.entry_version) is not int or not 1 <= self.entry_version <= MAX_ENTRY_VERSION:
|
|
185
|
+
raise SourceContractError(
|
|
186
|
+
"CATALOG_VERSION",
|
|
187
|
+
"catalog_entry.entry_version",
|
|
188
|
+
f"must be an integer in [1, {MAX_ENTRY_VERSION}]",
|
|
189
|
+
)
|
|
190
|
+
_text(self.publisher, "catalog_entry.publisher", maximum=200)
|
|
191
|
+
_text(self.title, "catalog_entry.title", maximum=200)
|
|
192
|
+
_text(self.description, "catalog_entry.description", maximum=MAX_DESCRIPTION)
|
|
193
|
+
_texts(
|
|
194
|
+
self.spatial_scope,
|
|
195
|
+
"catalog_entry.spatial_scope",
|
|
196
|
+
nonempty=True,
|
|
197
|
+
maximum=MAX_SPATIAL_SCOPE,
|
|
198
|
+
item_maximum=MAX_DECLARED_NAME,
|
|
199
|
+
)
|
|
200
|
+
_choices(
|
|
201
|
+
self.data_formats,
|
|
202
|
+
DATA_FORMATS,
|
|
203
|
+
"catalog_entry.data_formats",
|
|
204
|
+
nonempty=True,
|
|
205
|
+
maximum=len(DATA_FORMATS),
|
|
206
|
+
)
|
|
207
|
+
_choice(self.access_kind, ACCESS_KINDS, "catalog_entry.access_kind")
|
|
208
|
+
_boolean(self.authentication_required, "catalog_entry.authentication_required")
|
|
209
|
+
if not isinstance(self.observation, SourceObservation):
|
|
210
|
+
raise SourceContractError(
|
|
211
|
+
"TYPE",
|
|
212
|
+
"catalog_entry.observation",
|
|
213
|
+
"must be a SourceObservation",
|
|
214
|
+
)
|
|
215
|
+
if not isinstance(self.rights, RightsEvidence):
|
|
216
|
+
raise SourceContractError(
|
|
217
|
+
"TYPE",
|
|
218
|
+
"catalog_entry.rights",
|
|
219
|
+
"must be a RightsEvidence",
|
|
220
|
+
)
|
|
221
|
+
if self.observation.source_id != self.entry_id:
|
|
222
|
+
raise SourceContractError(
|
|
223
|
+
"CATALOG_SOURCE_BINDING",
|
|
224
|
+
"catalog_entry.observation",
|
|
225
|
+
"the observation and the entry must describe the same source",
|
|
226
|
+
)
|
|
227
|
+
_typed_tuple(
|
|
228
|
+
self.quirks,
|
|
229
|
+
SourceQuirk,
|
|
230
|
+
"catalog_entry.quirks",
|
|
231
|
+
maximum=MAX_QUIRKS,
|
|
232
|
+
)
|
|
233
|
+
if len({item.code for item in self.quirks}) != len(self.quirks):
|
|
234
|
+
raise SourceContractError(
|
|
235
|
+
"DUPLICATE",
|
|
236
|
+
"catalog_entry.quirks",
|
|
237
|
+
"quirk codes must be unique",
|
|
238
|
+
)
|
|
239
|
+
_texts(
|
|
240
|
+
self.declared_columns,
|
|
241
|
+
"catalog_entry.declared_columns",
|
|
242
|
+
nonempty=False,
|
|
243
|
+
maximum=MAX_DECLARED_COLUMNS,
|
|
244
|
+
item_maximum=MAX_DECLARED_NAME,
|
|
245
|
+
)
|
|
246
|
+
_texts(
|
|
247
|
+
self.declared_vocabulary,
|
|
248
|
+
"catalog_entry.declared_vocabulary",
|
|
249
|
+
nonempty=False,
|
|
250
|
+
maximum=MAX_DECLARED_VOCABULARY,
|
|
251
|
+
item_maximum=MAX_DECLARED_NAME,
|
|
252
|
+
)
|
|
253
|
+
if self.declared_row_count is not None:
|
|
254
|
+
_integer(
|
|
255
|
+
self.declared_row_count,
|
|
256
|
+
"catalog_entry.declared_row_count",
|
|
257
|
+
minimum=0,
|
|
258
|
+
maximum=MAX_SAFE_INTEGER,
|
|
259
|
+
)
|
|
260
|
+
_typed_tuple(
|
|
261
|
+
self.evidence,
|
|
262
|
+
EvidenceReference,
|
|
263
|
+
"catalog_entry.evidence",
|
|
264
|
+
maximum=MAX_EVIDENCE,
|
|
265
|
+
)
|
|
266
|
+
if not self.evidence:
|
|
267
|
+
raise SourceContractError(
|
|
268
|
+
"CATALOG_EVIDENCE",
|
|
269
|
+
"catalog_entry.evidence",
|
|
270
|
+
"a catalog entry requires at least one public citation",
|
|
271
|
+
)
|
|
272
|
+
_typed_tuple(
|
|
273
|
+
self.vectors,
|
|
274
|
+
LayerVectorReference,
|
|
275
|
+
"catalog_entry.vectors",
|
|
276
|
+
maximum=len(EMBEDDING_LAYERS),
|
|
277
|
+
)
|
|
278
|
+
if self.vectors:
|
|
279
|
+
layers = tuple(item.layer for item in self.vectors)
|
|
280
|
+
if sorted(layers) != sorted(EMBEDDING_LAYERS):
|
|
281
|
+
raise SourceContractError(
|
|
282
|
+
"CATALOG_VECTOR_LAYERS",
|
|
283
|
+
"catalog_entry.vectors",
|
|
284
|
+
f"must carry exactly one vector per layer {list(EMBEDDING_LAYERS)}, "
|
|
285
|
+
"or none at all",
|
|
286
|
+
)
|
|
287
|
+
if self.previous_digest is not None:
|
|
288
|
+
_digest(self.previous_digest, "catalog_entry.previous_digest")
|
|
289
|
+
if self.entry_version == 1 and self.previous_digest is not None:
|
|
290
|
+
raise SourceContractError(
|
|
291
|
+
"CATALOG_VERSION_CHAIN",
|
|
292
|
+
"catalog_entry.previous_digest",
|
|
293
|
+
"a first version has no predecessor to link to",
|
|
294
|
+
)
|
|
295
|
+
if self.entry_version > 1 and self.previous_digest is None:
|
|
296
|
+
raise SourceContractError(
|
|
297
|
+
"CATALOG_VERSION_CHAIN",
|
|
298
|
+
"catalog_entry.previous_digest",
|
|
299
|
+
"a successor version must link to its predecessor's digest",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def coordinate(self) -> str:
|
|
304
|
+
"""Return the entry identity, for example ``faostat_qcl@v3``."""
|
|
305
|
+
|
|
306
|
+
return f"{self.entry_id}@v{self.entry_version}"
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def facts_digest(self) -> str:
|
|
310
|
+
"""The digest of everything this entry *claims*, with its sealed vectors excluded.
|
|
311
|
+
|
|
312
|
+
The version chain runs on this rather than on ``digest``, and it has to. Sealing replaces
|
|
313
|
+
an entry's ``vectors``, which changes ``digest``; a chain linked on ``digest`` would
|
|
314
|
+
therefore verify before sealing and break after it, so the entries a sealed catalog holds
|
|
315
|
+
and the authored entry files they came from would be two permanently incompatible spellings
|
|
316
|
+
of one history. Excluding the vectors makes the link stable across sealing while leaving
|
|
317
|
+
every fact -- including ``previous_digest`` itself -- inside the digest, so a tampered
|
|
318
|
+
history still cannot re-link.
|
|
319
|
+
|
|
320
|
+
This is also the value a ``LayerVectorReference`` carries, which is what binds a sealed
|
|
321
|
+
vector to the facts it was computed from.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
payload = self.to_dict()
|
|
325
|
+
del payload["vectors"]
|
|
326
|
+
return canonical_sha256(payload)
|
|
327
|
+
|
|
328
|
+
def to_dict(self) -> dict[str, Any]:
|
|
329
|
+
return {
|
|
330
|
+
"schema_version": self.schema_version,
|
|
331
|
+
"entry_id": self.entry_id,
|
|
332
|
+
"entry_version": self.entry_version,
|
|
333
|
+
"publisher": self.publisher,
|
|
334
|
+
"title": self.title,
|
|
335
|
+
"description": self.description,
|
|
336
|
+
"spatial_scope": list(self.spatial_scope),
|
|
337
|
+
"data_formats": list(self.data_formats),
|
|
338
|
+
"access_kind": self.access_kind,
|
|
339
|
+
"authentication_required": self.authentication_required,
|
|
340
|
+
"observation": self.observation.to_dict(),
|
|
341
|
+
"rights": self.rights.to_dict(),
|
|
342
|
+
"quirks": [item.to_dict() for item in self.quirks],
|
|
343
|
+
"declared_columns": list(self.declared_columns),
|
|
344
|
+
"declared_vocabulary": list(self.declared_vocabulary),
|
|
345
|
+
"declared_row_count": self.declared_row_count,
|
|
346
|
+
"evidence": [item.to_dict() for item in self.evidence],
|
|
347
|
+
"vectors": [item.to_dict() for item in self.vectors],
|
|
348
|
+
"previous_digest": self.previous_digest,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
_MANAGED_VERSION_FIELDS = ("entry_version", "previous_digest", "vectors")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def next_entry_version(previous: CatalogEntry, **changes: object) -> CatalogEntry:
|
|
356
|
+
"""Mint the successor of ``previous``, linked to its predecessor's facts digest.
|
|
357
|
+
|
|
358
|
+
The caller supplies the changed facts and nothing else. ``entry_version``,
|
|
359
|
+
``previous_digest`` and ``vectors`` are computed here and refused as arguments -- that refusal
|
|
360
|
+
is precisely what stops a version from drifting away from the evidence it claims to describe.
|
|
361
|
+
|
|
362
|
+
Two of those three deserve their reason stated.
|
|
363
|
+
|
|
364
|
+
``previous_digest`` is ``previous.facts_digest`` and not ``previous.digest``: sealing rewrites
|
|
365
|
+
an entry's ``vectors``, so a link taken over the whole entry would depend on whether the author
|
|
366
|
+
happened to hold the sealed or the unsealed spelling of the predecessor, and only one of the
|
|
367
|
+
two would ever verify. Linking on the facts makes the answer the same either way.
|
|
368
|
+
|
|
369
|
+
``vectors`` is cleared unconditionally. Carrying a predecessor's sealed vectors onto a
|
|
370
|
+
successor is the one way to manufacture an entry that ranks on facts it no longer states, and
|
|
371
|
+
it is exactly what a bare ``dataclasses.replace`` would do. A successor is re-sealed, never
|
|
372
|
+
inherited.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
if not isinstance(previous, CatalogEntry):
|
|
376
|
+
raise SourceContractError("TYPE", "previous", "must be a CatalogEntry")
|
|
377
|
+
managed = sorted(name for name in _MANAGED_VERSION_FIELDS if name in changes)
|
|
378
|
+
if managed:
|
|
379
|
+
raise SourceContractError(
|
|
380
|
+
"CATALOG_VERSION_MANAGED",
|
|
381
|
+
f"changes.{managed[0]}",
|
|
382
|
+
f"{sorted(_MANAGED_VERSION_FIELDS)} are derived from the predecessor and cannot be "
|
|
383
|
+
"supplied",
|
|
384
|
+
)
|
|
385
|
+
return replace(
|
|
386
|
+
previous,
|
|
387
|
+
entry_version=previous.entry_version + 1,
|
|
388
|
+
previous_digest=previous.facts_digest,
|
|
389
|
+
vectors=(),
|
|
390
|
+
**changes,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def verify_version_chain(chain: tuple[CatalogEntry, ...]) -> str:
|
|
395
|
+
"""Verify a whole entry history and return its head coordinate, e.g. ``faostat_qcl@v3``.
|
|
396
|
+
|
|
397
|
+
A chain verifies when every member names the same source, the versions run ``1..n`` with no
|
|
398
|
+
gap, the first member claims no predecessor, and every later member's ``previous_digest`` is
|
|
399
|
+
the exact ``facts_digest`` of the member before it. Changing any fact of any member breaks
|
|
400
|
+
every link after it, so a tampered history cannot re-link itself.
|
|
401
|
+
|
|
402
|
+
The link is over ``facts_digest`` rather than ``digest`` so that one history verifies in both
|
|
403
|
+
of its spellings: the authored entries an author holds and the sealed entries a catalog holds
|
|
404
|
+
differ only in their vectors, and a chain that verified in one and not the other would be a
|
|
405
|
+
chain nobody could actually check.
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
if not isinstance(chain, tuple):
|
|
409
|
+
raise SourceContractError("TYPE", "chain", "must be an immutable tuple")
|
|
410
|
+
if not chain:
|
|
411
|
+
raise SourceContractError(
|
|
412
|
+
"CATALOG_VERSION_CHAIN",
|
|
413
|
+
"chain",
|
|
414
|
+
"a version chain must contain at least one entry",
|
|
415
|
+
)
|
|
416
|
+
for index, entry in enumerate(chain):
|
|
417
|
+
if not isinstance(entry, CatalogEntry):
|
|
418
|
+
raise SourceContractError(
|
|
419
|
+
"CATALOG_VERSION_CHAIN",
|
|
420
|
+
f"chain[{index}]",
|
|
421
|
+
"must be a CatalogEntry",
|
|
422
|
+
)
|
|
423
|
+
if entry.entry_id != chain[0].entry_id:
|
|
424
|
+
raise SourceContractError(
|
|
425
|
+
"CATALOG_VERSION_CHAIN",
|
|
426
|
+
f"chain[{index}].entry_id",
|
|
427
|
+
f"every member must describe {chain[0].entry_id!r}, found {entry.entry_id!r}",
|
|
428
|
+
)
|
|
429
|
+
if entry.entry_version != index + 1:
|
|
430
|
+
raise SourceContractError(
|
|
431
|
+
"CATALOG_VERSION_CHAIN",
|
|
432
|
+
f"chain[{index}].entry_version",
|
|
433
|
+
f"versions must run 1..n in order with no gap; expected {index + 1}, "
|
|
434
|
+
f"found {entry.entry_version}",
|
|
435
|
+
)
|
|
436
|
+
if index == 0:
|
|
437
|
+
if entry.previous_digest is not None:
|
|
438
|
+
raise SourceContractError(
|
|
439
|
+
"CATALOG_VERSION_CHAIN",
|
|
440
|
+
"chain[0].previous_digest",
|
|
441
|
+
"the first version has no predecessor",
|
|
442
|
+
)
|
|
443
|
+
continue
|
|
444
|
+
expected = chain[index - 1].facts_digest
|
|
445
|
+
if entry.previous_digest != expected:
|
|
446
|
+
raise SourceContractError(
|
|
447
|
+
"CATALOG_VERSION_CHAIN",
|
|
448
|
+
f"chain[{index}].previous_digest",
|
|
449
|
+
"does not match the canonical facts digest of the preceding version",
|
|
450
|
+
)
|
|
451
|
+
return chain[-1].coordinate
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def catalog_entry_from_dict(data: Mapping[str, Any]) -> CatalogEntry:
|
|
455
|
+
"""Rebuild an entry from its exact ``to_dict()`` shape, refusing anything else.
|
|
456
|
+
|
|
457
|
+
Reconstruction is strict on both sides: a missing key and an unknown key are equally a typed
|
|
458
|
+
failure. A parser that silently drops what it does not recognise turns a tampered artifact
|
|
459
|
+
into a valid-looking one, which is the failure mode the digest chain exists to prevent.
|
|
460
|
+
"""
|
|
461
|
+
|
|
462
|
+
payload = _mapping(data, "catalog_entry")
|
|
463
|
+
_exact(
|
|
464
|
+
payload,
|
|
465
|
+
{
|
|
466
|
+
"schema_version",
|
|
467
|
+
"entry_id",
|
|
468
|
+
"entry_version",
|
|
469
|
+
"publisher",
|
|
470
|
+
"title",
|
|
471
|
+
"description",
|
|
472
|
+
"spatial_scope",
|
|
473
|
+
"data_formats",
|
|
474
|
+
"access_kind",
|
|
475
|
+
"authentication_required",
|
|
476
|
+
"observation",
|
|
477
|
+
"rights",
|
|
478
|
+
"quirks",
|
|
479
|
+
"declared_columns",
|
|
480
|
+
"declared_vocabulary",
|
|
481
|
+
"declared_row_count",
|
|
482
|
+
"evidence",
|
|
483
|
+
"vectors",
|
|
484
|
+
"previous_digest",
|
|
485
|
+
},
|
|
486
|
+
"catalog_entry",
|
|
487
|
+
)
|
|
488
|
+
return CatalogEntry(
|
|
489
|
+
schema_version=payload["schema_version"],
|
|
490
|
+
entry_id=payload["entry_id"],
|
|
491
|
+
entry_version=payload["entry_version"],
|
|
492
|
+
publisher=payload["publisher"],
|
|
493
|
+
title=payload["title"],
|
|
494
|
+
description=payload["description"],
|
|
495
|
+
spatial_scope=tuple(_sequence(payload["spatial_scope"], "catalog_entry.spatial_scope")),
|
|
496
|
+
data_formats=tuple(_sequence(payload["data_formats"], "catalog_entry.data_formats")),
|
|
497
|
+
access_kind=payload["access_kind"],
|
|
498
|
+
authentication_required=payload["authentication_required"],
|
|
499
|
+
observation=_observation_from_dict(payload["observation"]),
|
|
500
|
+
rights=_rights_from_dict(payload["rights"]),
|
|
501
|
+
quirks=tuple(
|
|
502
|
+
_quirk_from_dict(item) for item in _sequence(payload["quirks"], "catalog_entry.quirks")
|
|
503
|
+
),
|
|
504
|
+
declared_columns=tuple(
|
|
505
|
+
_sequence(payload["declared_columns"], "catalog_entry.declared_columns")
|
|
506
|
+
),
|
|
507
|
+
declared_vocabulary=tuple(
|
|
508
|
+
_sequence(payload["declared_vocabulary"], "catalog_entry.declared_vocabulary")
|
|
509
|
+
),
|
|
510
|
+
declared_row_count=payload["declared_row_count"],
|
|
511
|
+
evidence=tuple(
|
|
512
|
+
_evidence_from_dict(item, f"catalog_entry.evidence[{index}]")
|
|
513
|
+
for index, item in enumerate(_sequence(payload["evidence"], "catalog_entry.evidence"))
|
|
514
|
+
),
|
|
515
|
+
vectors=tuple(
|
|
516
|
+
_vector_from_dict(item)
|
|
517
|
+
for item in _sequence(payload["vectors"], "catalog_entry.vectors")
|
|
518
|
+
),
|
|
519
|
+
previous_digest=payload["previous_digest"],
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _quirk_from_dict(data: Any) -> SourceQuirk:
|
|
524
|
+
payload = _mapping(data, "catalog_entry.quirks[]")
|
|
525
|
+
_exact(payload, {"code", "detail"}, "catalog_entry.quirks[]")
|
|
526
|
+
return SourceQuirk(code=payload["code"], detail=payload["detail"])
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _vector_from_dict(data: Any) -> LayerVectorReference:
|
|
530
|
+
payload = _mapping(data, "catalog_entry.vectors[]")
|
|
531
|
+
_exact(
|
|
532
|
+
payload,
|
|
533
|
+
{
|
|
534
|
+
"layer",
|
|
535
|
+
"backend_id",
|
|
536
|
+
"backend_version",
|
|
537
|
+
"dimensions",
|
|
538
|
+
"quantization",
|
|
539
|
+
"vector_sha256",
|
|
540
|
+
"entry_facts_digest",
|
|
541
|
+
},
|
|
542
|
+
"catalog_entry.vectors[]",
|
|
543
|
+
)
|
|
544
|
+
return LayerVectorReference(
|
|
545
|
+
layer=payload["layer"],
|
|
546
|
+
backend_id=payload["backend_id"],
|
|
547
|
+
backend_version=payload["backend_version"],
|
|
548
|
+
dimensions=payload["dimensions"],
|
|
549
|
+
quantization=payload["quantization"],
|
|
550
|
+
vector_sha256=payload["vector_sha256"],
|
|
551
|
+
entry_facts_digest=payload["entry_facts_digest"],
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _evidence_from_dict(data: Any, path: str) -> EvidenceReference:
|
|
556
|
+
payload = _mapping(data, path)
|
|
557
|
+
_exact(payload, {"uri", "observed_at", "content_sha256", "media_type"}, path)
|
|
558
|
+
return EvidenceReference(
|
|
559
|
+
uri=payload["uri"],
|
|
560
|
+
observed_at=payload["observed_at"],
|
|
561
|
+
content_sha256=payload["content_sha256"],
|
|
562
|
+
media_type=payload["media_type"],
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _obligation_from_dict(data: Any) -> LicenseObligation:
|
|
567
|
+
path = "catalog_entry.rights.obligations[]"
|
|
568
|
+
payload = _mapping(data, path)
|
|
569
|
+
_exact(
|
|
570
|
+
payload,
|
|
571
|
+
{
|
|
572
|
+
"obligation_id",
|
|
573
|
+
"applies_at",
|
|
574
|
+
"permitted_uses",
|
|
575
|
+
"prohibited_uses",
|
|
576
|
+
"required_artifact_digests",
|
|
577
|
+
"escalation_when_ambiguous",
|
|
578
|
+
},
|
|
579
|
+
path,
|
|
580
|
+
)
|
|
581
|
+
return LicenseObligation(
|
|
582
|
+
obligation_id=payload["obligation_id"],
|
|
583
|
+
applies_at=tuple(_sequence(payload["applies_at"], f"{path}.applies_at")),
|
|
584
|
+
permitted_uses=tuple(_sequence(payload["permitted_uses"], f"{path}.permitted_uses")),
|
|
585
|
+
prohibited_uses=tuple(_sequence(payload["prohibited_uses"], f"{path}.prohibited_uses")),
|
|
586
|
+
required_artifact_digests=tuple(
|
|
587
|
+
_sequence(payload["required_artifact_digests"], f"{path}.required_artifact_digests")
|
|
588
|
+
),
|
|
589
|
+
escalation_when_ambiguous=payload["escalation_when_ambiguous"],
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def rights_evidence_from_dict(data: Any, *, path: str = "catalog_entry.rights") -> RightsEvidence:
|
|
594
|
+
"""Rebuild one authored rights claim from its exact ``to_dict()`` shape.
|
|
595
|
+
|
|
596
|
+
The public spelling of the parser ``catalog_entry_from_dict`` already uses for the
|
|
597
|
+
nested ``rights`` field, exported so a rights decision document can carry the same
|
|
598
|
+
shape without a second parser drifting from this one. ``path`` names the document the
|
|
599
|
+
claim actually sits in, so a refusal sends the person to the right file.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
return _rights_from_dict(data, path=path)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _rights_from_dict(data: Any, *, path: str = "catalog_entry.rights") -> RightsEvidence:
|
|
606
|
+
payload = _mapping(data, path)
|
|
607
|
+
_exact(
|
|
608
|
+
payload,
|
|
609
|
+
{
|
|
610
|
+
"status",
|
|
611
|
+
"lawful_basis",
|
|
612
|
+
"allowed_uses",
|
|
613
|
+
"evidence",
|
|
614
|
+
"obligations",
|
|
615
|
+
"human_decision_reference",
|
|
616
|
+
},
|
|
617
|
+
path,
|
|
618
|
+
)
|
|
619
|
+
return RightsEvidence(
|
|
620
|
+
status=payload["status"],
|
|
621
|
+
lawful_basis=payload["lawful_basis"],
|
|
622
|
+
allowed_uses=tuple(_sequence(payload["allowed_uses"], f"{path}.allowed_uses")),
|
|
623
|
+
evidence=tuple(
|
|
624
|
+
_evidence_from_dict(item, f"{path}.evidence[{index}]")
|
|
625
|
+
for index, item in enumerate(_sequence(payload["evidence"], f"{path}.evidence"))
|
|
626
|
+
),
|
|
627
|
+
obligations=tuple(
|
|
628
|
+
_obligation_from_dict(item)
|
|
629
|
+
for item in _sequence(payload["obligations"], f"{path}.obligations")
|
|
630
|
+
),
|
|
631
|
+
human_decision_reference=payload["human_decision_reference"],
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _observation_from_dict(data: Any) -> SourceObservation:
|
|
636
|
+
path = "catalog_entry.observation"
|
|
637
|
+
payload = _mapping(data, path)
|
|
638
|
+
_exact(
|
|
639
|
+
payload,
|
|
640
|
+
{
|
|
641
|
+
"schema_version",
|
|
642
|
+
"source_id",
|
|
643
|
+
"observed_at",
|
|
644
|
+
"event_time_field",
|
|
645
|
+
"available_at_field",
|
|
646
|
+
"historical_start",
|
|
647
|
+
"historical_end",
|
|
648
|
+
"live_status",
|
|
649
|
+
"publication_delay_seconds",
|
|
650
|
+
"update_frequency_seconds",
|
|
651
|
+
"response_status",
|
|
652
|
+
"evidence",
|
|
653
|
+
},
|
|
654
|
+
path,
|
|
655
|
+
)
|
|
656
|
+
return SourceObservation(
|
|
657
|
+
schema_version=payload["schema_version"],
|
|
658
|
+
source_id=payload["source_id"],
|
|
659
|
+
observed_at=payload["observed_at"],
|
|
660
|
+
event_time_field=payload["event_time_field"],
|
|
661
|
+
available_at_field=payload["available_at_field"],
|
|
662
|
+
historical_start=payload["historical_start"],
|
|
663
|
+
historical_end=payload["historical_end"],
|
|
664
|
+
live_status=payload["live_status"],
|
|
665
|
+
publication_delay_seconds=payload["publication_delay_seconds"],
|
|
666
|
+
update_frequency_seconds=payload["update_frequency_seconds"],
|
|
667
|
+
response_status=payload["response_status"],
|
|
668
|
+
evidence=tuple(
|
|
669
|
+
_evidence_from_dict(item, f"{path}.evidence[{index}]")
|
|
670
|
+
for index, item in enumerate(_sequence(payload["evidence"], f"{path}.evidence"))
|
|
671
|
+
),
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _mapping(value: Any, path: str) -> Mapping[str, Any]:
|
|
676
|
+
if not isinstance(value, Mapping):
|
|
677
|
+
raise SourceContractError("TYPE", path, "must be an object")
|
|
678
|
+
for key in value:
|
|
679
|
+
if not isinstance(key, str):
|
|
680
|
+
raise SourceContractError("TYPE", path, "object keys must be strings")
|
|
681
|
+
return value
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _sequence(value: Any, path: str) -> list[Any]:
|
|
685
|
+
if not isinstance(value, list):
|
|
686
|
+
raise SourceContractError("TYPE", path, "must be an array")
|
|
687
|
+
return value
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _exact(data: Mapping[str, Any], expected: set[str], path: str) -> None:
|
|
691
|
+
actual = set(data)
|
|
692
|
+
if actual != expected:
|
|
693
|
+
raise SourceContractError(
|
|
694
|
+
"FIELDS",
|
|
695
|
+
path,
|
|
696
|
+
f"must contain exactly {sorted(expected)}; missing={sorted(expected - actual)}, "
|
|
697
|
+
f"extra={sorted(actual - expected)}",
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _text(value: Any, path: str, *, maximum: int) -> str:
|
|
702
|
+
if not isinstance(value, str) or not value or len(value) > maximum:
|
|
703
|
+
raise SourceContractError(
|
|
704
|
+
"TEXT",
|
|
705
|
+
path,
|
|
706
|
+
f"must be a non-empty string no longer than {maximum} characters",
|
|
707
|
+
)
|
|
708
|
+
for index, char in enumerate(value):
|
|
709
|
+
if 0xD800 <= ord(char) <= 0xDFFF:
|
|
710
|
+
raise SourceContractError(
|
|
711
|
+
"UNICODE",
|
|
712
|
+
path,
|
|
713
|
+
f"contains an unpaired surrogate at character {index}",
|
|
714
|
+
)
|
|
715
|
+
return value
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _texts(
|
|
719
|
+
values: Any,
|
|
720
|
+
path: str,
|
|
721
|
+
*,
|
|
722
|
+
nonempty: bool,
|
|
723
|
+
maximum: int,
|
|
724
|
+
item_maximum: int,
|
|
725
|
+
) -> None:
|
|
726
|
+
if not isinstance(values, tuple):
|
|
727
|
+
raise SourceContractError("TYPE", path, "must be an immutable tuple")
|
|
728
|
+
if nonempty and not values:
|
|
729
|
+
raise SourceContractError("EMPTY", path, "must not be empty")
|
|
730
|
+
if len(values) > maximum:
|
|
731
|
+
raise SourceContractError("LIMIT", path, f"must contain at most {maximum} values")
|
|
732
|
+
for index, value in enumerate(values):
|
|
733
|
+
_text(value, f"{path}[{index}]", maximum=item_maximum)
|
|
734
|
+
if len(set(values)) != len(values):
|
|
735
|
+
raise SourceContractError("DUPLICATE", path, "values must be unique")
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _identifier(value: Any, path: str) -> str:
|
|
739
|
+
text = _text(value, path, maximum=MAX_DECLARED_NAME)
|
|
740
|
+
if not _IDENTIFIER.fullmatch(text):
|
|
741
|
+
raise SourceContractError(
|
|
742
|
+
"IDENTIFIER",
|
|
743
|
+
path,
|
|
744
|
+
"must be a lowercase dotted/dashed/underscored identifier",
|
|
745
|
+
)
|
|
746
|
+
return text
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _semver(value: Any, path: str) -> str:
|
|
750
|
+
text = _text(value, path, maximum=64)
|
|
751
|
+
if not _SEMVER.fullmatch(text):
|
|
752
|
+
raise SourceContractError("SEMVER", path, "must be a positive-major semantic version")
|
|
753
|
+
return text
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _digest(value: Any, path: str) -> str:
|
|
757
|
+
text = _text(value, path, maximum=64)
|
|
758
|
+
if not _SHA256.fullmatch(text):
|
|
759
|
+
raise SourceContractError("SHA256", path, "must be a lowercase 64-character SHA-256")
|
|
760
|
+
return text
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _choice(value: Any, allowed: frozenset[str], path: str) -> str:
|
|
764
|
+
text = _text(value, path, maximum=MAX_DECLARED_NAME)
|
|
765
|
+
if text not in allowed:
|
|
766
|
+
raise SourceContractError("ENUM", path, f"must be one of {sorted(allowed)}")
|
|
767
|
+
return text
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def _choices(
|
|
771
|
+
values: Any,
|
|
772
|
+
allowed: frozenset[str],
|
|
773
|
+
path: str,
|
|
774
|
+
*,
|
|
775
|
+
nonempty: bool,
|
|
776
|
+
maximum: int,
|
|
777
|
+
) -> None:
|
|
778
|
+
if not isinstance(values, tuple):
|
|
779
|
+
raise SourceContractError("TYPE", path, "must be an immutable tuple")
|
|
780
|
+
if nonempty and not values:
|
|
781
|
+
raise SourceContractError("EMPTY", path, "must not be empty")
|
|
782
|
+
if len(values) > maximum:
|
|
783
|
+
raise SourceContractError("LIMIT", path, f"must contain at most {maximum} values")
|
|
784
|
+
for index, value in enumerate(values):
|
|
785
|
+
_choice(value, allowed, f"{path}[{index}]")
|
|
786
|
+
if len(set(values)) != len(values):
|
|
787
|
+
raise SourceContractError("DUPLICATE", path, "values must be unique")
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _typed_tuple(values: Any, expected_type: type[Any], path: str, *, maximum: int) -> None:
|
|
791
|
+
if not isinstance(values, tuple):
|
|
792
|
+
raise SourceContractError("TYPE", path, "must be an immutable tuple")
|
|
793
|
+
if len(values) > maximum:
|
|
794
|
+
raise SourceContractError("LIMIT", path, f"must contain at most {maximum} values")
|
|
795
|
+
for index, value in enumerate(values):
|
|
796
|
+
if not isinstance(value, expected_type):
|
|
797
|
+
raise SourceContractError(
|
|
798
|
+
"TYPE",
|
|
799
|
+
f"{path}[{index}]",
|
|
800
|
+
f"must be a {expected_type.__name__}",
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _integer(value: Any, path: str, *, minimum: int, maximum: int) -> int:
|
|
805
|
+
# ``type(value) is not int`` rather than ``isinstance``: it refuses both bool and float, which
|
|
806
|
+
# is what keeps a float out of the digest before ``canonical_json_bytes`` ever sees it.
|
|
807
|
+
if type(value) is not int or not minimum <= value <= maximum:
|
|
808
|
+
raise SourceContractError(
|
|
809
|
+
"INTEGER",
|
|
810
|
+
path,
|
|
811
|
+
f"must be an integer in [{minimum}, {maximum}]",
|
|
812
|
+
)
|
|
813
|
+
if abs(value) > MAX_SAFE_INTEGER:
|
|
814
|
+
raise SourceContractError(
|
|
815
|
+
"SAFE_INTEGER",
|
|
816
|
+
path,
|
|
817
|
+
"integer exceeds the exact Python/JavaScript JSON domain",
|
|
818
|
+
)
|
|
819
|
+
return value
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _boolean(value: Any, path: str) -> bool:
|
|
823
|
+
if type(value) is not bool:
|
|
824
|
+
raise SourceContractError("BOOLEAN", path, "must be a boolean")
|
|
825
|
+
return value
|