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,1182 @@
|
|
|
1
|
+
"""Honest version-two public catalog entries.
|
|
2
|
+
|
|
3
|
+
The frozen :mod:`contracts` module remains the complete v1 contract. This module adds a
|
|
4
|
+
side-by-side successor in which absence is data: every nullable semantic fact says exactly
|
|
5
|
+
``known``, ``unknown``, or ``not_applicable``. A known fact is inseparable from the exact provider
|
|
6
|
+
record and canonical provider JSON path that stated it.
|
|
7
|
+
|
|
8
|
+
Semantic provenance and sweep provenance deliberately have different digests. The semantic
|
|
9
|
+
facts digest drives entry versioning and vector reuse; append-only observations record when and
|
|
10
|
+
from which response/page evidence those facts were seen without changing their meaning.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import dataclass, replace
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
|
+
from types import MappingProxyType
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from mostlyright.data_harness.canonical import (
|
|
23
|
+
MAX_CANONICAL_DEPTH,
|
|
24
|
+
CanonicalJSONError,
|
|
25
|
+
canonical_sha256,
|
|
26
|
+
parse_canonical_json,
|
|
27
|
+
)
|
|
28
|
+
from mostlyright.data_harness.sources.catalog.contracts import (
|
|
29
|
+
ACCESS_KINDS,
|
|
30
|
+
CATALOG_ENTRY_CONTRACT_VERSION,
|
|
31
|
+
EMBEDDING_LAYERS,
|
|
32
|
+
MAX_DECLARED_COLUMNS,
|
|
33
|
+
MAX_DECLARED_NAME,
|
|
34
|
+
MAX_DECLARED_VOCABULARY,
|
|
35
|
+
MAX_DESCRIPTION,
|
|
36
|
+
MAX_ENTRY_VERSION,
|
|
37
|
+
MAX_SPATIAL_SCOPE,
|
|
38
|
+
CatalogEntry,
|
|
39
|
+
LayerVectorReference,
|
|
40
|
+
catalog_entry_from_dict,
|
|
41
|
+
)
|
|
42
|
+
from mostlyright.data_harness.sources.catalog.harvest.protocol import (
|
|
43
|
+
DATAGOV_V4_HARVESTER_COORDINATE,
|
|
44
|
+
)
|
|
45
|
+
from mostlyright.data_harness.sources.contracts import (
|
|
46
|
+
DATA_FORMATS,
|
|
47
|
+
MAX_SAFE_INTEGER,
|
|
48
|
+
RIGHTS_STATUSES,
|
|
49
|
+
SourceContractError,
|
|
50
|
+
_CanonicalContract,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
CATALOG_ENTRY_V2_CONTRACT_VERSION = "harness-source-catalog-entry.v2"
|
|
54
|
+
|
|
55
|
+
# Keep a single entry below the existing four-MiB catalog-fact admission ceiling, and enforce it
|
|
56
|
+
# before ``json.loads`` can allocate a decoded object graph. Provider-scale collections use bounded
|
|
57
|
+
# shards/packs; no individual semantic record needs a wider spelling.
|
|
58
|
+
MAX_CATALOG_ENTRY_BYTES = 4 * 1024 * 1024
|
|
59
|
+
MAX_CATALOG_ENTRY_NESTING = MAX_CANONICAL_DEPTH
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ProviderContract:
|
|
64
|
+
"""Exact provider identity field and implementation coordinate; neither is inferred."""
|
|
65
|
+
|
|
66
|
+
harvester_coordinate: str
|
|
67
|
+
identifier_json_path: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# One immutable provider contract prevents the identity field and observation producer from
|
|
71
|
+
# drifting into independently maintained maps. Adding a provider requires one explicit reviewed
|
|
72
|
+
# registry entry; names, prefixes, and semantic-version proximity never imply authority.
|
|
73
|
+
PROVIDER_CONTRACTS: Mapping[str, ProviderContract] = MappingProxyType(
|
|
74
|
+
{
|
|
75
|
+
"datagov_v4": ProviderContract(
|
|
76
|
+
harvester_coordinate=DATAGOV_V4_HARVESTER_COORDINATE,
|
|
77
|
+
identifier_json_path="$.identifier",
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
FACT_STATES = frozenset({"known", "unknown", "not_applicable"})
|
|
83
|
+
MAX_PROVIDER_RECORD_ID = 512
|
|
84
|
+
MAX_PROVIDER_PATHS = 64
|
|
85
|
+
MAX_OBSERVATIONS = 10_000
|
|
86
|
+
MAX_PROFILES = 256
|
|
87
|
+
|
|
88
|
+
FILTERABLE_FACTS = frozenset(
|
|
89
|
+
{
|
|
90
|
+
"publisher",
|
|
91
|
+
"description",
|
|
92
|
+
"spatial_scope",
|
|
93
|
+
"data_formats",
|
|
94
|
+
"access_kind",
|
|
95
|
+
"authentication_required",
|
|
96
|
+
"declared_columns",
|
|
97
|
+
"declared_vocabulary",
|
|
98
|
+
"declared_row_count",
|
|
99
|
+
"profiles",
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$")
|
|
104
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
105
|
+
_HARVESTER_COORDINATE = re.compile(
|
|
106
|
+
r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*@[1-9][0-9]*\.[0-9]+\.[0-9]+$"
|
|
107
|
+
)
|
|
108
|
+
# Deliberately excludes numeric array positions. Provider records may reorder arrays between
|
|
109
|
+
# responses; a semantic author may cite the complete array (``[*]``), never an unstable local
|
|
110
|
+
# position. Keys are the closed portable spelling used by normalized provider records.
|
|
111
|
+
_PROVIDER_NAMED_SEGMENT = re.compile(r"\.[A-Za-z_][A-Za-z0-9_-]*")
|
|
112
|
+
_PROVIDER_QUOTED_KEYS = frozenset({"@type"})
|
|
113
|
+
_UTC_SECOND = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class ProviderRecordIdentity(_CanonicalContract):
|
|
118
|
+
"""Stable provider record identity, directly cited from one normalized provider field."""
|
|
119
|
+
|
|
120
|
+
provider_id: str
|
|
121
|
+
provider_record_id: str
|
|
122
|
+
provider_record_sha256: str
|
|
123
|
+
identifier_json_path: str
|
|
124
|
+
|
|
125
|
+
def __post_init__(self) -> None:
|
|
126
|
+
_identifier(self.provider_id, "provider_record.provider_id")
|
|
127
|
+
_text(
|
|
128
|
+
self.provider_record_id,
|
|
129
|
+
"provider_record.provider_record_id",
|
|
130
|
+
maximum=MAX_PROVIDER_RECORD_ID,
|
|
131
|
+
)
|
|
132
|
+
_digest(self.provider_record_sha256, "provider_record.provider_record_sha256")
|
|
133
|
+
_provider_identifier_path(
|
|
134
|
+
self.identifier_json_path,
|
|
135
|
+
"provider_record.identifier_json_path",
|
|
136
|
+
)
|
|
137
|
+
contract = PROVIDER_CONTRACTS.get(self.provider_id)
|
|
138
|
+
if contract is None or self.identifier_json_path != contract.identifier_json_path:
|
|
139
|
+
raise SourceContractError(
|
|
140
|
+
"CATALOG_PROVIDER_IDENTITY_PATH",
|
|
141
|
+
"provider_record.identifier_json_path",
|
|
142
|
+
"must equal the sole registered identity path for this provider",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> dict[str, str]:
|
|
146
|
+
return {
|
|
147
|
+
"provider_id": self.provider_id,
|
|
148
|
+
"provider_record_id": self.provider_record_id,
|
|
149
|
+
"provider_record_sha256": self.provider_record_sha256,
|
|
150
|
+
"identifier_json_path": self.identifier_json_path,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True)
|
|
155
|
+
class ProviderFactProvenance(_CanonicalContract):
|
|
156
|
+
"""Exact provider record and JSON paths supporting one known fact."""
|
|
157
|
+
|
|
158
|
+
provider_id: str
|
|
159
|
+
provider_record_id: str
|
|
160
|
+
provider_record_sha256: str
|
|
161
|
+
json_paths: tuple[str, ...]
|
|
162
|
+
|
|
163
|
+
def __post_init__(self) -> None:
|
|
164
|
+
_identifier(self.provider_id, "fact.provenance.provider_id")
|
|
165
|
+
_text(
|
|
166
|
+
self.provider_record_id,
|
|
167
|
+
"fact.provenance.provider_record_id",
|
|
168
|
+
maximum=MAX_PROVIDER_RECORD_ID,
|
|
169
|
+
)
|
|
170
|
+
_digest(self.provider_record_sha256, "fact.provenance.provider_record_sha256")
|
|
171
|
+
if not isinstance(self.json_paths, tuple):
|
|
172
|
+
raise SourceContractError(
|
|
173
|
+
"TYPE",
|
|
174
|
+
"fact.provenance.json_paths",
|
|
175
|
+
"must be an immutable tuple",
|
|
176
|
+
)
|
|
177
|
+
if not self.json_paths or len(self.json_paths) > MAX_PROVIDER_PATHS:
|
|
178
|
+
raise SourceContractError(
|
|
179
|
+
"LIMIT",
|
|
180
|
+
"fact.provenance.json_paths",
|
|
181
|
+
f"must contain between 1 and {MAX_PROVIDER_PATHS} paths",
|
|
182
|
+
)
|
|
183
|
+
for index, path in enumerate(self.json_paths):
|
|
184
|
+
_provider_path(path, f"fact.provenance.json_paths[{index}]")
|
|
185
|
+
if tuple(sorted(set(self.json_paths))) != self.json_paths:
|
|
186
|
+
raise SourceContractError(
|
|
187
|
+
"ORDER",
|
|
188
|
+
"fact.provenance.json_paths",
|
|
189
|
+
"paths must be unique and sorted",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def to_dict(self) -> dict[str, Any]:
|
|
193
|
+
return {
|
|
194
|
+
"provider_id": self.provider_id,
|
|
195
|
+
"provider_record_id": self.provider_record_id,
|
|
196
|
+
"provider_record_sha256": self.provider_record_sha256,
|
|
197
|
+
"json_paths": list(self.json_paths),
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(frozen=True)
|
|
202
|
+
class CatalogFact(_CanonicalContract):
|
|
203
|
+
"""One explicit nullable fact.
|
|
204
|
+
|
|
205
|
+
``None`` is not a fourth state. It is only the canonical empty slot carried by the two states
|
|
206
|
+
that have no value. Known values cannot be null and cannot exist without provenance.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
state: str
|
|
210
|
+
value: object | None = None
|
|
211
|
+
provenance: ProviderFactProvenance | None = None
|
|
212
|
+
|
|
213
|
+
def __post_init__(self) -> None:
|
|
214
|
+
_choice(self.state, FACT_STATES, "fact.state")
|
|
215
|
+
if self.state == "known":
|
|
216
|
+
if self.value is None:
|
|
217
|
+
raise SourceContractError("FACT_VALUE", "fact.value", "known requires a value")
|
|
218
|
+
if not isinstance(self.provenance, ProviderFactProvenance):
|
|
219
|
+
raise SourceContractError(
|
|
220
|
+
"FACT_PROVENANCE",
|
|
221
|
+
"fact.provenance",
|
|
222
|
+
"known requires exact provider provenance",
|
|
223
|
+
)
|
|
224
|
+
return
|
|
225
|
+
if self.value is not None:
|
|
226
|
+
raise SourceContractError(
|
|
227
|
+
"FACT_VALUE",
|
|
228
|
+
"fact.value",
|
|
229
|
+
f"{self.state} cannot carry a value",
|
|
230
|
+
)
|
|
231
|
+
if self.provenance is not None:
|
|
232
|
+
raise SourceContractError(
|
|
233
|
+
"FACT_PROVENANCE",
|
|
234
|
+
"fact.provenance",
|
|
235
|
+
f"{self.state} cannot carry provenance",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def to_dict(self) -> dict[str, Any]:
|
|
239
|
+
value = list(self.value) if isinstance(self.value, tuple) else self.value
|
|
240
|
+
return {
|
|
241
|
+
"state": self.state,
|
|
242
|
+
"value": value,
|
|
243
|
+
"provenance": self.provenance.to_dict() if self.provenance is not None else None,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@dataclass(frozen=True)
|
|
248
|
+
class CatalogObservation(_CanonicalContract):
|
|
249
|
+
"""Append-only sweep evidence, excluded from semantic versioning and vector identity."""
|
|
250
|
+
|
|
251
|
+
harvester_coordinate: str
|
|
252
|
+
observed_at: str
|
|
253
|
+
response_evidence_sha256: str
|
|
254
|
+
page_evidence_sha256: str
|
|
255
|
+
|
|
256
|
+
def __post_init__(self) -> None:
|
|
257
|
+
coordinate = _text(
|
|
258
|
+
self.harvester_coordinate,
|
|
259
|
+
"observation.harvester_coordinate",
|
|
260
|
+
maximum=192,
|
|
261
|
+
)
|
|
262
|
+
if not _HARVESTER_COORDINATE.fullmatch(coordinate):
|
|
263
|
+
raise SourceContractError(
|
|
264
|
+
"COORDINATE",
|
|
265
|
+
"observation.harvester_coordinate",
|
|
266
|
+
"must be a provider/harvester identifier plus positive-major semantic version",
|
|
267
|
+
)
|
|
268
|
+
_utc_second(self.observed_at, "observation.observed_at")
|
|
269
|
+
_digest(self.response_evidence_sha256, "observation.response_evidence_sha256")
|
|
270
|
+
_digest(self.page_evidence_sha256, "observation.page_evidence_sha256")
|
|
271
|
+
|
|
272
|
+
def to_dict(self) -> dict[str, str]:
|
|
273
|
+
return {
|
|
274
|
+
"harvester_coordinate": self.harvester_coordinate,
|
|
275
|
+
"observed_at": self.observed_at,
|
|
276
|
+
"response_evidence_sha256": self.response_evidence_sha256,
|
|
277
|
+
"page_evidence_sha256": self.page_evidence_sha256,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass(frozen=True)
|
|
282
|
+
class CatalogFactFilter(_CanonicalContract):
|
|
283
|
+
"""One closed exact filter over semantic facts, with no score-shaped input channel."""
|
|
284
|
+
|
|
285
|
+
field: str
|
|
286
|
+
expected: str | bool | int
|
|
287
|
+
|
|
288
|
+
def __post_init__(self) -> None:
|
|
289
|
+
_choice(self.field, FILTERABLE_FACTS, "filter.field")
|
|
290
|
+
path = "filter.expected"
|
|
291
|
+
if self.field == "authentication_required":
|
|
292
|
+
if type(self.expected) is not bool:
|
|
293
|
+
raise SourceContractError("BOOLEAN", path, "must be a boolean for this field")
|
|
294
|
+
elif self.field == "declared_row_count":
|
|
295
|
+
if type(self.expected) is not int or not 0 <= self.expected <= MAX_SAFE_INTEGER:
|
|
296
|
+
raise SourceContractError(
|
|
297
|
+
"INTEGER",
|
|
298
|
+
path,
|
|
299
|
+
f"must be an integer in [0, {MAX_SAFE_INTEGER}] for this field",
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
_text(self.expected, path, maximum=MAX_DESCRIPTION)
|
|
303
|
+
|
|
304
|
+
def to_dict(self) -> dict[str, Any]:
|
|
305
|
+
return {"field": self.field, "expected": self.expected}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@dataclass(frozen=True)
|
|
309
|
+
class CatalogEntryV2Decision(_CanonicalContract):
|
|
310
|
+
"""Score-blind retrieval/filter/admission projection for one already-safe v2 entry."""
|
|
311
|
+
|
|
312
|
+
entry_coordinate: str
|
|
313
|
+
searchable: bool
|
|
314
|
+
filters_match: bool
|
|
315
|
+
filter_failures: tuple[str, ...]
|
|
316
|
+
rights_disposition: str
|
|
317
|
+
recommendation: str
|
|
318
|
+
semantic_facts_digest: str
|
|
319
|
+
entry_digest: str
|
|
320
|
+
|
|
321
|
+
def __post_init__(self) -> None:
|
|
322
|
+
_text(self.entry_coordinate, "decision.entry_coordinate", maximum=256)
|
|
323
|
+
_boolean(self.searchable, "decision.searchable")
|
|
324
|
+
_boolean(self.filters_match, "decision.filters_match")
|
|
325
|
+
if not isinstance(self.filter_failures, tuple):
|
|
326
|
+
raise SourceContractError(
|
|
327
|
+
"TYPE",
|
|
328
|
+
"decision.filter_failures",
|
|
329
|
+
"must be an immutable tuple",
|
|
330
|
+
)
|
|
331
|
+
for index, failure in enumerate(self.filter_failures):
|
|
332
|
+
_text(failure, f"decision.filter_failures[{index}]", maximum=256)
|
|
333
|
+
_choice(
|
|
334
|
+
self.rights_disposition,
|
|
335
|
+
frozenset({"admitted", "flagged", "refused"}),
|
|
336
|
+
"decision.rights_disposition",
|
|
337
|
+
)
|
|
338
|
+
_choice(
|
|
339
|
+
self.recommendation,
|
|
340
|
+
frozenset({"use_source", "no_match"}),
|
|
341
|
+
"decision.recommendation",
|
|
342
|
+
)
|
|
343
|
+
_digest(self.semantic_facts_digest, "decision.semantic_facts_digest")
|
|
344
|
+
_digest(self.entry_digest, "decision.entry_digest")
|
|
345
|
+
if self.recommendation == "use_source" and (
|
|
346
|
+
not self.searchable or not self.filters_match or self.rights_disposition != "admitted"
|
|
347
|
+
):
|
|
348
|
+
raise SourceContractError(
|
|
349
|
+
"CATALOG_AUTHORITY",
|
|
350
|
+
"decision.recommendation",
|
|
351
|
+
"use_source requires searchable admitted facts satisfying every filter",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def to_dict(self) -> dict[str, Any]:
|
|
355
|
+
return {
|
|
356
|
+
"entry_coordinate": self.entry_coordinate,
|
|
357
|
+
"searchable": self.searchable,
|
|
358
|
+
"filters_match": self.filters_match,
|
|
359
|
+
"filter_failures": list(self.filter_failures),
|
|
360
|
+
"rights_disposition": self.rights_disposition,
|
|
361
|
+
"recommendation": self.recommendation,
|
|
362
|
+
"semantic_facts_digest": self.semantic_facts_digest,
|
|
363
|
+
"entry_digest": self.entry_digest,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@dataclass(frozen=True)
|
|
368
|
+
class CatalogRetrievalRead:
|
|
369
|
+
"""Durable parse disposition used by packed readers before a record can reach retrieval."""
|
|
370
|
+
|
|
371
|
+
entry: CatalogEntry | CatalogEntryV2 | None
|
|
372
|
+
schema_version: str | None
|
|
373
|
+
retrieval_disposition: str
|
|
374
|
+
searchable: bool
|
|
375
|
+
reason_code: str
|
|
376
|
+
|
|
377
|
+
def __post_init__(self) -> None:
|
|
378
|
+
if self.retrieval_disposition not in {"searchable", "skipped"}:
|
|
379
|
+
raise SourceContractError(
|
|
380
|
+
"ENUM",
|
|
381
|
+
"retrieval_read.retrieval_disposition",
|
|
382
|
+
"must be searchable or skipped",
|
|
383
|
+
)
|
|
384
|
+
if type(self.searchable) is not bool:
|
|
385
|
+
raise SourceContractError(
|
|
386
|
+
"BOOLEAN",
|
|
387
|
+
"retrieval_read.searchable",
|
|
388
|
+
"must be a boolean",
|
|
389
|
+
)
|
|
390
|
+
if (self.retrieval_disposition == "searchable") != self.searchable:
|
|
391
|
+
raise SourceContractError(
|
|
392
|
+
"CATALOG_RETRIEVAL_DISPOSITION",
|
|
393
|
+
"retrieval_read.searchable",
|
|
394
|
+
"must agree with retrieval_disposition",
|
|
395
|
+
)
|
|
396
|
+
if self.searchable != (self.entry is not None):
|
|
397
|
+
raise SourceContractError(
|
|
398
|
+
"CATALOG_RETRIEVAL_DISPOSITION",
|
|
399
|
+
"retrieval_read.entry",
|
|
400
|
+
"skipped reads carry no entry and searchable reads carry one",
|
|
401
|
+
)
|
|
402
|
+
_text(self.reason_code, "retrieval_read.reason_code", maximum=128)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
_FACT_FIELDS = (
|
|
406
|
+
"title",
|
|
407
|
+
"publisher",
|
|
408
|
+
"description",
|
|
409
|
+
"spatial_scope",
|
|
410
|
+
"data_formats",
|
|
411
|
+
"access_kind",
|
|
412
|
+
"authentication_required",
|
|
413
|
+
"rights",
|
|
414
|
+
"declared_columns",
|
|
415
|
+
"declared_vocabulary",
|
|
416
|
+
"declared_row_count",
|
|
417
|
+
"profiles",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@dataclass(frozen=True)
|
|
422
|
+
class CatalogEntryV2(_CanonicalContract):
|
|
423
|
+
"""One v2 public-source entry with explicit fact states and separate sweep history."""
|
|
424
|
+
|
|
425
|
+
entry_id: str
|
|
426
|
+
entry_version: int
|
|
427
|
+
provider_record: ProviderRecordIdentity
|
|
428
|
+
title: CatalogFact
|
|
429
|
+
publisher: CatalogFact
|
|
430
|
+
description: CatalogFact
|
|
431
|
+
spatial_scope: CatalogFact
|
|
432
|
+
data_formats: CatalogFact
|
|
433
|
+
access_kind: CatalogFact
|
|
434
|
+
authentication_required: CatalogFact
|
|
435
|
+
rights: CatalogFact
|
|
436
|
+
declared_columns: CatalogFact
|
|
437
|
+
declared_vocabulary: CatalogFact
|
|
438
|
+
declared_row_count: CatalogFact
|
|
439
|
+
profiles: CatalogFact
|
|
440
|
+
observations: tuple[CatalogObservation, ...]
|
|
441
|
+
vectors: tuple[LayerVectorReference, ...] = ()
|
|
442
|
+
previous_semantic_digest: str | None = None
|
|
443
|
+
schema_version: str = CATALOG_ENTRY_V2_CONTRACT_VERSION
|
|
444
|
+
|
|
445
|
+
def __post_init__(self) -> None:
|
|
446
|
+
if self.schema_version != CATALOG_ENTRY_V2_CONTRACT_VERSION:
|
|
447
|
+
raise SourceContractError(
|
|
448
|
+
"VERSION",
|
|
449
|
+
"catalog_entry_v2.schema_version",
|
|
450
|
+
f"must equal {CATALOG_ENTRY_V2_CONTRACT_VERSION!r}",
|
|
451
|
+
)
|
|
452
|
+
_identifier(self.entry_id, "catalog_entry_v2.entry_id")
|
|
453
|
+
if type(self.entry_version) is not int or not 1 <= self.entry_version <= MAX_ENTRY_VERSION:
|
|
454
|
+
raise SourceContractError(
|
|
455
|
+
"CATALOG_VERSION",
|
|
456
|
+
"catalog_entry_v2.entry_version",
|
|
457
|
+
f"must be an integer in [1, {MAX_ENTRY_VERSION}]",
|
|
458
|
+
)
|
|
459
|
+
if not isinstance(self.provider_record, ProviderRecordIdentity):
|
|
460
|
+
raise SourceContractError(
|
|
461
|
+
"TYPE",
|
|
462
|
+
"catalog_entry_v2.provider_record",
|
|
463
|
+
"must be a ProviderRecordIdentity",
|
|
464
|
+
)
|
|
465
|
+
for name in _FACT_FIELDS:
|
|
466
|
+
fact = getattr(self, name)
|
|
467
|
+
if not isinstance(fact, CatalogFact):
|
|
468
|
+
raise SourceContractError(
|
|
469
|
+
"TYPE",
|
|
470
|
+
f"catalog_entry_v2.{name}",
|
|
471
|
+
"must be a CatalogFact",
|
|
472
|
+
)
|
|
473
|
+
self._validate_fact_provenance(name, fact)
|
|
474
|
+
if self.title.state != "known":
|
|
475
|
+
raise SourceContractError(
|
|
476
|
+
"CATALOG_IDENTITY",
|
|
477
|
+
"catalog_entry_v2.title",
|
|
478
|
+
"safe searchable identity requires a directly evidenced known title",
|
|
479
|
+
)
|
|
480
|
+
self._validate_fact_types()
|
|
481
|
+
self._validate_observations()
|
|
482
|
+
self._validate_vectors()
|
|
483
|
+
if self.previous_semantic_digest is not None:
|
|
484
|
+
_digest(
|
|
485
|
+
self.previous_semantic_digest,
|
|
486
|
+
"catalog_entry_v2.previous_semantic_digest",
|
|
487
|
+
)
|
|
488
|
+
if self.entry_version == 1 and self.previous_semantic_digest is not None:
|
|
489
|
+
raise SourceContractError(
|
|
490
|
+
"CATALOG_VERSION_CHAIN",
|
|
491
|
+
"catalog_entry_v2.previous_semantic_digest",
|
|
492
|
+
"a first version has no semantic predecessor",
|
|
493
|
+
)
|
|
494
|
+
if self.entry_version > 1 and self.previous_semantic_digest is None:
|
|
495
|
+
raise SourceContractError(
|
|
496
|
+
"CATALOG_VERSION_CHAIN",
|
|
497
|
+
"catalog_entry_v2.previous_semantic_digest",
|
|
498
|
+
"a semantic successor must link to its predecessor",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def _validate_fact_provenance(self, name: str, fact: CatalogFact) -> None:
|
|
502
|
+
if fact.state != "known":
|
|
503
|
+
return
|
|
504
|
+
provenance = fact.provenance
|
|
505
|
+
assert provenance is not None
|
|
506
|
+
expected = (
|
|
507
|
+
self.provider_record.provider_id,
|
|
508
|
+
self.provider_record.provider_record_id,
|
|
509
|
+
self.provider_record.provider_record_sha256,
|
|
510
|
+
)
|
|
511
|
+
actual = (
|
|
512
|
+
provenance.provider_id,
|
|
513
|
+
provenance.provider_record_id,
|
|
514
|
+
provenance.provider_record_sha256,
|
|
515
|
+
)
|
|
516
|
+
if actual != expected:
|
|
517
|
+
raise SourceContractError(
|
|
518
|
+
"CATALOG_PROVIDER_BINDING",
|
|
519
|
+
f"catalog_entry_v2.{name}.provenance",
|
|
520
|
+
"known fact provenance must name this exact provider record and digest",
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def _validate_fact_types(self) -> None:
|
|
524
|
+
_known_text(self.title, "catalog_entry_v2.title", maximum=200)
|
|
525
|
+
_known_text(self.publisher, "catalog_entry_v2.publisher", maximum=200)
|
|
526
|
+
_known_text(self.description, "catalog_entry_v2.description", maximum=MAX_DESCRIPTION)
|
|
527
|
+
_known_text_tuple(
|
|
528
|
+
self.spatial_scope,
|
|
529
|
+
"catalog_entry_v2.spatial_scope",
|
|
530
|
+
maximum=MAX_SPATIAL_SCOPE,
|
|
531
|
+
)
|
|
532
|
+
_known_choices(
|
|
533
|
+
self.data_formats,
|
|
534
|
+
"catalog_entry_v2.data_formats",
|
|
535
|
+
allowed=DATA_FORMATS,
|
|
536
|
+
maximum=len(DATA_FORMATS),
|
|
537
|
+
)
|
|
538
|
+
_known_choice(
|
|
539
|
+
self.access_kind,
|
|
540
|
+
"catalog_entry_v2.access_kind",
|
|
541
|
+
allowed=ACCESS_KINDS,
|
|
542
|
+
)
|
|
543
|
+
_known_boolean(
|
|
544
|
+
self.authentication_required,
|
|
545
|
+
"catalog_entry_v2.authentication_required",
|
|
546
|
+
)
|
|
547
|
+
_known_choice(self.rights, "catalog_entry_v2.rights", allowed=RIGHTS_STATUSES)
|
|
548
|
+
_known_text_tuple(
|
|
549
|
+
self.declared_columns,
|
|
550
|
+
"catalog_entry_v2.declared_columns",
|
|
551
|
+
maximum=MAX_DECLARED_COLUMNS,
|
|
552
|
+
)
|
|
553
|
+
_known_text_tuple(
|
|
554
|
+
self.declared_vocabulary,
|
|
555
|
+
"catalog_entry_v2.declared_vocabulary",
|
|
556
|
+
maximum=MAX_DECLARED_VOCABULARY,
|
|
557
|
+
)
|
|
558
|
+
_known_integer(
|
|
559
|
+
self.declared_row_count,
|
|
560
|
+
"catalog_entry_v2.declared_row_count",
|
|
561
|
+
minimum=0,
|
|
562
|
+
maximum=MAX_SAFE_INTEGER,
|
|
563
|
+
)
|
|
564
|
+
_known_text_tuple(self.profiles, "catalog_entry_v2.profiles", maximum=MAX_PROFILES)
|
|
565
|
+
|
|
566
|
+
def _validate_observations(self) -> None:
|
|
567
|
+
if not isinstance(self.observations, tuple):
|
|
568
|
+
raise SourceContractError(
|
|
569
|
+
"TYPE",
|
|
570
|
+
"catalog_entry_v2.observations",
|
|
571
|
+
"must be an immutable tuple",
|
|
572
|
+
)
|
|
573
|
+
if not self.observations or len(self.observations) > MAX_OBSERVATIONS:
|
|
574
|
+
raise SourceContractError(
|
|
575
|
+
"LIMIT",
|
|
576
|
+
"catalog_entry_v2.observations",
|
|
577
|
+
f"must contain between 1 and {MAX_OBSERVATIONS} observations",
|
|
578
|
+
)
|
|
579
|
+
for index, item in enumerate(self.observations):
|
|
580
|
+
if not isinstance(item, CatalogObservation):
|
|
581
|
+
raise SourceContractError(
|
|
582
|
+
"TYPE",
|
|
583
|
+
f"catalog_entry_v2.observations[{index}]",
|
|
584
|
+
"must be a CatalogObservation",
|
|
585
|
+
)
|
|
586
|
+
contract = PROVIDER_CONTRACTS.get(self.provider_record.provider_id)
|
|
587
|
+
if contract is None or item.harvester_coordinate != contract.harvester_coordinate:
|
|
588
|
+
raise SourceContractError(
|
|
589
|
+
"CATALOG_PROVIDER_HARVESTER",
|
|
590
|
+
f"catalog_entry_v2.observations[{index}].harvester_coordinate",
|
|
591
|
+
"must be an exact registered harvester coordinate for this provider",
|
|
592
|
+
)
|
|
593
|
+
keys = tuple((item.observed_at, item.digest) for item in self.observations)
|
|
594
|
+
if tuple(sorted(set(keys))) != keys:
|
|
595
|
+
raise SourceContractError(
|
|
596
|
+
"CATALOG_OBSERVATION_HISTORY",
|
|
597
|
+
"catalog_entry_v2.observations",
|
|
598
|
+
"observations must be unique and append-ordered by timestamp and digest",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
def _validate_vectors(self) -> None:
|
|
602
|
+
if not isinstance(self.vectors, tuple):
|
|
603
|
+
raise SourceContractError(
|
|
604
|
+
"TYPE",
|
|
605
|
+
"catalog_entry_v2.vectors",
|
|
606
|
+
"must be an immutable tuple",
|
|
607
|
+
)
|
|
608
|
+
for index, item in enumerate(self.vectors):
|
|
609
|
+
if not isinstance(item, LayerVectorReference):
|
|
610
|
+
raise SourceContractError(
|
|
611
|
+
"TYPE",
|
|
612
|
+
f"catalog_entry_v2.vectors[{index}]",
|
|
613
|
+
"must be a LayerVectorReference",
|
|
614
|
+
)
|
|
615
|
+
if not self.vectors:
|
|
616
|
+
return
|
|
617
|
+
layers = tuple(item.layer for item in self.vectors)
|
|
618
|
+
if layers != EMBEDDING_LAYERS:
|
|
619
|
+
raise SourceContractError(
|
|
620
|
+
"CATALOG_VECTOR_LAYERS",
|
|
621
|
+
"catalog_entry_v2.vectors",
|
|
622
|
+
f"must carry exactly one vector for all layers in order {list(EMBEDDING_LAYERS)}",
|
|
623
|
+
)
|
|
624
|
+
for index, vector in enumerate(self.vectors):
|
|
625
|
+
if vector.entry_facts_digest != self.semantic_facts_digest:
|
|
626
|
+
raise SourceContractError(
|
|
627
|
+
"CATALOG_VECTOR_FACTS",
|
|
628
|
+
f"catalog_entry_v2.vectors[{index}].entry_facts_digest",
|
|
629
|
+
"must equal this entry's semantic facts digest",
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
@property
|
|
633
|
+
def coordinate(self) -> str:
|
|
634
|
+
return f"{self.entry_id}@v{self.entry_version}"
|
|
635
|
+
|
|
636
|
+
@property
|
|
637
|
+
def semantic_facts_digest(self) -> str:
|
|
638
|
+
"""Digest meaning and provider paths, excluding observations and vector instances."""
|
|
639
|
+
|
|
640
|
+
return canonical_sha256(self._semantic_payload())
|
|
641
|
+
|
|
642
|
+
@property
|
|
643
|
+
def provenance_history_digest(self) -> str:
|
|
644
|
+
"""Digest the stable meaning coordinate plus every append-only sweep observation."""
|
|
645
|
+
|
|
646
|
+
return canonical_sha256(
|
|
647
|
+
{
|
|
648
|
+
"schema_version": self.schema_version,
|
|
649
|
+
"semantic_facts_digest": self.semantic_facts_digest,
|
|
650
|
+
"observations": [item.to_dict() for item in self.observations],
|
|
651
|
+
}
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
def _semantic_payload(self) -> dict[str, Any]:
|
|
655
|
+
return {
|
|
656
|
+
"schema_version": self.schema_version,
|
|
657
|
+
"entry_id": self.entry_id,
|
|
658
|
+
"entry_version": self.entry_version,
|
|
659
|
+
"provider_record": self.provider_record.to_dict(),
|
|
660
|
+
**{name: getattr(self, name).to_dict() for name in _FACT_FIELDS},
|
|
661
|
+
"previous_semantic_digest": self.previous_semantic_digest,
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
def to_dict(self) -> dict[str, Any]:
|
|
665
|
+
return {
|
|
666
|
+
**self._semantic_payload(),
|
|
667
|
+
"observations": [item.to_dict() for item in self.observations],
|
|
668
|
+
"vectors": [item.to_dict() for item in self.vectors],
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def append_catalog_observation(
|
|
673
|
+
entry: CatalogEntryV2,
|
|
674
|
+
observation: CatalogObservation,
|
|
675
|
+
) -> CatalogEntryV2:
|
|
676
|
+
"""Append one evidence observation without minting a new semantic entry version."""
|
|
677
|
+
|
|
678
|
+
if not isinstance(entry, CatalogEntryV2):
|
|
679
|
+
raise SourceContractError("TYPE", "entry", "must be a CatalogEntryV2")
|
|
680
|
+
if not isinstance(observation, CatalogObservation):
|
|
681
|
+
raise SourceContractError("TYPE", "observation", "must be a CatalogObservation")
|
|
682
|
+
return replace(entry, observations=(*entry.observations, observation))
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def catalog_entry_from_canonical_bytes(data: bytes) -> CatalogEntry | CatalogEntryV2:
|
|
686
|
+
"""Dispatch exact canonical bytes to frozen v1 or explicit v2 without reinterpretation."""
|
|
687
|
+
|
|
688
|
+
if not isinstance(data, (bytes, bytearray, memoryview)):
|
|
689
|
+
raise SourceContractError(
|
|
690
|
+
"TYPE",
|
|
691
|
+
"catalog_entry_bytes",
|
|
692
|
+
"must be a bytes-like value",
|
|
693
|
+
)
|
|
694
|
+
if len(data) > MAX_CATALOG_ENTRY_BYTES:
|
|
695
|
+
raise SourceContractError(
|
|
696
|
+
"CATALOG_ENTRY_BYTES",
|
|
697
|
+
"catalog_entry_bytes",
|
|
698
|
+
f"must contain at most {MAX_CATALOG_ENTRY_BYTES} bytes",
|
|
699
|
+
)
|
|
700
|
+
try:
|
|
701
|
+
decoded = parse_canonical_json(data)
|
|
702
|
+
except RecursionError:
|
|
703
|
+
# CPython's JSON decoder may reach its interpreter recursion limit before the canonical
|
|
704
|
+
# normalizer can issue JSON_DEPTH. Normalize both routes to one stable contract refusal.
|
|
705
|
+
raise SourceContractError(
|
|
706
|
+
"JSON_DEPTH",
|
|
707
|
+
"catalog_entry_bytes",
|
|
708
|
+
f"nesting exceeds {MAX_CATALOG_ENTRY_NESTING} levels",
|
|
709
|
+
) from None
|
|
710
|
+
except CanonicalJSONError as error:
|
|
711
|
+
raise SourceContractError(
|
|
712
|
+
error.code,
|
|
713
|
+
"catalog_entry_bytes",
|
|
714
|
+
f"must be canonical JSON: {error.detail}",
|
|
715
|
+
) from None
|
|
716
|
+
payload = _mapping(decoded, "catalog_entry")
|
|
717
|
+
schema_version = payload.get("schema_version")
|
|
718
|
+
if schema_version == CATALOG_ENTRY_CONTRACT_VERSION:
|
|
719
|
+
return catalog_entry_from_dict(payload)
|
|
720
|
+
if schema_version == CATALOG_ENTRY_V2_CONTRACT_VERSION:
|
|
721
|
+
return catalog_entry_v2_from_dict(payload)
|
|
722
|
+
raise SourceContractError(
|
|
723
|
+
"VERSION",
|
|
724
|
+
"catalog_entry.schema_version",
|
|
725
|
+
"must name the exact frozen v1 or explicit v2 catalog entry contract",
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def read_catalog_entry_for_retrieval(data: bytes) -> CatalogRetrievalRead:
|
|
730
|
+
"""Parse a record for retrieval, durably skipping unsafe identity or provenance."""
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
entry = catalog_entry_from_canonical_bytes(data)
|
|
734
|
+
except RecursionError:
|
|
735
|
+
# Defense in depth: this adapter is the retrieval trust boundary, so a future parser
|
|
736
|
+
# branch must not accidentally restore an interpreter exception as a public outcome.
|
|
737
|
+
return CatalogRetrievalRead(
|
|
738
|
+
entry=None,
|
|
739
|
+
schema_version=None,
|
|
740
|
+
retrieval_disposition="skipped",
|
|
741
|
+
searchable=False,
|
|
742
|
+
reason_code="JSON_DEPTH",
|
|
743
|
+
)
|
|
744
|
+
except SourceContractError as error:
|
|
745
|
+
return CatalogRetrievalRead(
|
|
746
|
+
entry=None,
|
|
747
|
+
schema_version=None,
|
|
748
|
+
retrieval_disposition="skipped",
|
|
749
|
+
searchable=False,
|
|
750
|
+
reason_code=error.code,
|
|
751
|
+
)
|
|
752
|
+
return CatalogRetrievalRead(
|
|
753
|
+
entry=entry,
|
|
754
|
+
schema_version=entry.schema_version,
|
|
755
|
+
retrieval_disposition="searchable",
|
|
756
|
+
searchable=True,
|
|
757
|
+
reason_code="OK",
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def evaluate_catalog_entry_v2(
|
|
762
|
+
entry: CatalogEntryV2,
|
|
763
|
+
*,
|
|
764
|
+
filters: tuple[CatalogFactFilter, ...] = (),
|
|
765
|
+
) -> CatalogEntryV2Decision:
|
|
766
|
+
"""Project searchable metadata and fail-closed fact authority without retrieval scores."""
|
|
767
|
+
|
|
768
|
+
if not isinstance(entry, CatalogEntryV2):
|
|
769
|
+
raise SourceContractError("TYPE", "entry", "must be a CatalogEntryV2")
|
|
770
|
+
if not isinstance(filters, tuple):
|
|
771
|
+
raise SourceContractError("TYPE", "filters", "must be an immutable tuple")
|
|
772
|
+
failures: list[str] = []
|
|
773
|
+
seen: set[str] = set()
|
|
774
|
+
for index, required in enumerate(filters):
|
|
775
|
+
if not isinstance(required, CatalogFactFilter):
|
|
776
|
+
raise SourceContractError(
|
|
777
|
+
"TYPE",
|
|
778
|
+
f"filters[{index}]",
|
|
779
|
+
"must be a CatalogFactFilter",
|
|
780
|
+
)
|
|
781
|
+
if required.field in seen:
|
|
782
|
+
raise SourceContractError(
|
|
783
|
+
"DUPLICATE",
|
|
784
|
+
f"filters[{index}].field",
|
|
785
|
+
"each structured fact may be filtered at most once",
|
|
786
|
+
)
|
|
787
|
+
seen.add(required.field)
|
|
788
|
+
fact = getattr(entry, required.field)
|
|
789
|
+
if fact.state != "known":
|
|
790
|
+
failures.append(f"{required.field}:{fact.state}")
|
|
791
|
+
continue
|
|
792
|
+
value = fact.value
|
|
793
|
+
matched = (
|
|
794
|
+
required.expected in value if isinstance(value, tuple) else value == required.expected
|
|
795
|
+
)
|
|
796
|
+
if not matched:
|
|
797
|
+
failures.append(f"{required.field}:mismatch")
|
|
798
|
+
|
|
799
|
+
rights_disposition = _v2_rights_disposition(entry.rights)
|
|
800
|
+
explicitly_public = (
|
|
801
|
+
entry.authentication_required.state == "known"
|
|
802
|
+
and entry.authentication_required.value is False
|
|
803
|
+
and entry.access_kind.state == "known"
|
|
804
|
+
and entry.data_formats.state == "known"
|
|
805
|
+
)
|
|
806
|
+
recommendation = (
|
|
807
|
+
"use_source"
|
|
808
|
+
if rights_disposition == "admitted" and explicitly_public and not failures
|
|
809
|
+
else "no_match"
|
|
810
|
+
)
|
|
811
|
+
return CatalogEntryV2Decision(
|
|
812
|
+
entry_coordinate=entry.coordinate,
|
|
813
|
+
searchable=True,
|
|
814
|
+
filters_match=not failures,
|
|
815
|
+
filter_failures=tuple(failures),
|
|
816
|
+
rights_disposition=rights_disposition,
|
|
817
|
+
recommendation=recommendation,
|
|
818
|
+
semantic_facts_digest=entry.semantic_facts_digest,
|
|
819
|
+
entry_digest=entry.digest,
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _v2_rights_disposition(rights: CatalogFact) -> str:
|
|
824
|
+
if rights.state != "known":
|
|
825
|
+
return "flagged"
|
|
826
|
+
if rights.value == "approved":
|
|
827
|
+
return "admitted"
|
|
828
|
+
if rights.value == "prohibited":
|
|
829
|
+
return "refused"
|
|
830
|
+
# ``conditional`` lacks a v2 obligation fact, and ``unclear`` explicitly requires escalation.
|
|
831
|
+
return "flagged"
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def catalog_entry_v2_from_dict(data: Mapping[str, Any]) -> CatalogEntryV2:
|
|
835
|
+
"""Strictly rebuild the one exact canonical v2 object shape."""
|
|
836
|
+
|
|
837
|
+
payload = _mapping(data, "catalog_entry_v2")
|
|
838
|
+
_exact(
|
|
839
|
+
payload,
|
|
840
|
+
{
|
|
841
|
+
"schema_version",
|
|
842
|
+
"entry_id",
|
|
843
|
+
"entry_version",
|
|
844
|
+
"provider_record",
|
|
845
|
+
*_FACT_FIELDS,
|
|
846
|
+
"observations",
|
|
847
|
+
"vectors",
|
|
848
|
+
"previous_semantic_digest",
|
|
849
|
+
},
|
|
850
|
+
"catalog_entry_v2",
|
|
851
|
+
)
|
|
852
|
+
provider_record = _provider_record_from_dict(payload["provider_record"])
|
|
853
|
+
facts = {
|
|
854
|
+
name: _fact_from_dict(
|
|
855
|
+
payload[name],
|
|
856
|
+
f"catalog_entry_v2.{name}",
|
|
857
|
+
tuple_value=name
|
|
858
|
+
in {
|
|
859
|
+
"spatial_scope",
|
|
860
|
+
"data_formats",
|
|
861
|
+
"declared_columns",
|
|
862
|
+
"declared_vocabulary",
|
|
863
|
+
"profiles",
|
|
864
|
+
},
|
|
865
|
+
)
|
|
866
|
+
for name in _FACT_FIELDS
|
|
867
|
+
}
|
|
868
|
+
return CatalogEntryV2(
|
|
869
|
+
schema_version=payload["schema_version"],
|
|
870
|
+
entry_id=payload["entry_id"],
|
|
871
|
+
entry_version=payload["entry_version"],
|
|
872
|
+
provider_record=provider_record,
|
|
873
|
+
**facts,
|
|
874
|
+
observations=tuple(
|
|
875
|
+
_observation_from_dict(item, f"catalog_entry_v2.observations[{index}]")
|
|
876
|
+
for index, item in enumerate(
|
|
877
|
+
_sequence(payload["observations"], "catalog_entry_v2.observations")
|
|
878
|
+
)
|
|
879
|
+
),
|
|
880
|
+
vectors=tuple(
|
|
881
|
+
_vector_from_dict(item, f"catalog_entry_v2.vectors[{index}]")
|
|
882
|
+
for index, item in enumerate(_sequence(payload["vectors"], "catalog_entry_v2.vectors"))
|
|
883
|
+
),
|
|
884
|
+
previous_semantic_digest=payload["previous_semantic_digest"],
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _provider_record_from_dict(data: Any) -> ProviderRecordIdentity:
|
|
889
|
+
path = "catalog_entry_v2.provider_record"
|
|
890
|
+
payload = _mapping(data, path)
|
|
891
|
+
_exact(
|
|
892
|
+
payload,
|
|
893
|
+
{
|
|
894
|
+
"provider_id",
|
|
895
|
+
"provider_record_id",
|
|
896
|
+
"provider_record_sha256",
|
|
897
|
+
"identifier_json_path",
|
|
898
|
+
},
|
|
899
|
+
path,
|
|
900
|
+
)
|
|
901
|
+
return ProviderRecordIdentity(
|
|
902
|
+
provider_id=payload["provider_id"],
|
|
903
|
+
provider_record_id=payload["provider_record_id"],
|
|
904
|
+
provider_record_sha256=payload["provider_record_sha256"],
|
|
905
|
+
identifier_json_path=payload["identifier_json_path"],
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _fact_from_dict(data: Any, path: str, *, tuple_value: bool) -> CatalogFact:
|
|
910
|
+
payload = _mapping(data, path)
|
|
911
|
+
_exact(payload, {"state", "value", "provenance"}, path)
|
|
912
|
+
value = payload["value"]
|
|
913
|
+
if tuple_value and value is not None:
|
|
914
|
+
value = tuple(_sequence(value, f"{path}.value"))
|
|
915
|
+
provenance_payload = payload["provenance"]
|
|
916
|
+
provenance = None
|
|
917
|
+
if provenance_payload is not None:
|
|
918
|
+
provenance_data = _mapping(provenance_payload, f"{path}.provenance")
|
|
919
|
+
_exact(
|
|
920
|
+
provenance_data,
|
|
921
|
+
{"provider_id", "provider_record_id", "provider_record_sha256", "json_paths"},
|
|
922
|
+
f"{path}.provenance",
|
|
923
|
+
)
|
|
924
|
+
provenance = ProviderFactProvenance(
|
|
925
|
+
provider_id=provenance_data["provider_id"],
|
|
926
|
+
provider_record_id=provenance_data["provider_record_id"],
|
|
927
|
+
provider_record_sha256=provenance_data["provider_record_sha256"],
|
|
928
|
+
json_paths=tuple(
|
|
929
|
+
_sequence(provenance_data["json_paths"], f"{path}.provenance.json_paths")
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
return CatalogFact(state=payload["state"], value=value, provenance=provenance)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _observation_from_dict(data: Any, path: str) -> CatalogObservation:
|
|
936
|
+
payload = _mapping(data, path)
|
|
937
|
+
_exact(
|
|
938
|
+
payload,
|
|
939
|
+
{
|
|
940
|
+
"harvester_coordinate",
|
|
941
|
+
"observed_at",
|
|
942
|
+
"response_evidence_sha256",
|
|
943
|
+
"page_evidence_sha256",
|
|
944
|
+
},
|
|
945
|
+
path,
|
|
946
|
+
)
|
|
947
|
+
return CatalogObservation(
|
|
948
|
+
harvester_coordinate=payload["harvester_coordinate"],
|
|
949
|
+
observed_at=payload["observed_at"],
|
|
950
|
+
response_evidence_sha256=payload["response_evidence_sha256"],
|
|
951
|
+
page_evidence_sha256=payload["page_evidence_sha256"],
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _vector_from_dict(data: Any, path: str) -> LayerVectorReference:
|
|
956
|
+
payload = _mapping(data, path)
|
|
957
|
+
_exact(
|
|
958
|
+
payload,
|
|
959
|
+
{
|
|
960
|
+
"layer",
|
|
961
|
+
"backend_id",
|
|
962
|
+
"backend_version",
|
|
963
|
+
"dimensions",
|
|
964
|
+
"quantization",
|
|
965
|
+
"vector_sha256",
|
|
966
|
+
"entry_facts_digest",
|
|
967
|
+
},
|
|
968
|
+
path,
|
|
969
|
+
)
|
|
970
|
+
return LayerVectorReference(
|
|
971
|
+
layer=payload["layer"],
|
|
972
|
+
backend_id=payload["backend_id"],
|
|
973
|
+
backend_version=payload["backend_version"],
|
|
974
|
+
dimensions=payload["dimensions"],
|
|
975
|
+
quantization=payload["quantization"],
|
|
976
|
+
vector_sha256=payload["vector_sha256"],
|
|
977
|
+
entry_facts_digest=payload["entry_facts_digest"],
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _mapping(value: Any, path: str) -> Mapping[str, Any]:
|
|
982
|
+
if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
|
|
983
|
+
raise SourceContractError("TYPE", path, "must be an object with string keys")
|
|
984
|
+
return value
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _sequence(value: Any, path: str) -> list[Any]:
|
|
988
|
+
if not isinstance(value, list):
|
|
989
|
+
raise SourceContractError("TYPE", path, "must be an array")
|
|
990
|
+
return value
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _exact(data: Mapping[str, Any], expected: set[str], path: str) -> None:
|
|
994
|
+
actual = set(data)
|
|
995
|
+
if actual != expected:
|
|
996
|
+
raise SourceContractError(
|
|
997
|
+
"FIELDS",
|
|
998
|
+
path,
|
|
999
|
+
f"must contain exactly {sorted(expected)}; missing={sorted(expected - actual)}, "
|
|
1000
|
+
f"extra={sorted(actual - expected)}",
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _text(value: Any, path: str, *, maximum: int) -> str:
|
|
1005
|
+
if not isinstance(value, str) or not value or len(value) > maximum:
|
|
1006
|
+
raise SourceContractError(
|
|
1007
|
+
"TEXT",
|
|
1008
|
+
path,
|
|
1009
|
+
f"must be a non-empty string no longer than {maximum} characters",
|
|
1010
|
+
)
|
|
1011
|
+
for index, char in enumerate(value):
|
|
1012
|
+
if 0xD800 <= ord(char) <= 0xDFFF:
|
|
1013
|
+
raise SourceContractError(
|
|
1014
|
+
"UNICODE",
|
|
1015
|
+
path,
|
|
1016
|
+
f"contains an unpaired surrogate at character {index}",
|
|
1017
|
+
)
|
|
1018
|
+
if ord(char) < 0x20 or 0x7F <= ord(char) <= 0x9F:
|
|
1019
|
+
raise SourceContractError(
|
|
1020
|
+
"CONTROL_CHARACTER",
|
|
1021
|
+
path,
|
|
1022
|
+
f"contains a C0/C1 control character at character {index}",
|
|
1023
|
+
)
|
|
1024
|
+
if char.isspace() and char != " ":
|
|
1025
|
+
raise SourceContractError(
|
|
1026
|
+
"UNSAFE_WHITESPACE",
|
|
1027
|
+
path,
|
|
1028
|
+
f"contains non-ASCII-space whitespace at character {index}",
|
|
1029
|
+
)
|
|
1030
|
+
return value
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _identifier(value: Any, path: str) -> str:
|
|
1034
|
+
text = _text(value, path, maximum=MAX_DECLARED_NAME)
|
|
1035
|
+
if not _IDENTIFIER.fullmatch(text):
|
|
1036
|
+
raise SourceContractError(
|
|
1037
|
+
"IDENTIFIER",
|
|
1038
|
+
path,
|
|
1039
|
+
"must be a lowercase dotted/dashed/underscored identifier",
|
|
1040
|
+
)
|
|
1041
|
+
return text
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def _digest(value: Any, path: str) -> str:
|
|
1045
|
+
text = _text(value, path, maximum=64)
|
|
1046
|
+
if not _SHA256.fullmatch(text):
|
|
1047
|
+
raise SourceContractError("SHA256", path, "must be a lowercase 64-character SHA-256")
|
|
1048
|
+
return text
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _choice(value: Any, allowed: frozenset[str], path: str) -> str:
|
|
1052
|
+
text = _text(value, path, maximum=MAX_DECLARED_NAME)
|
|
1053
|
+
if text not in allowed:
|
|
1054
|
+
raise SourceContractError("ENUM", path, f"must be one of {sorted(allowed)}")
|
|
1055
|
+
return text
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _boolean(value: Any, path: str) -> bool:
|
|
1059
|
+
if type(value) is not bool:
|
|
1060
|
+
raise SourceContractError("BOOLEAN", path, "must be a boolean")
|
|
1061
|
+
return value
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _provider_path(value: Any, path: str) -> str:
|
|
1065
|
+
text = _text(value, path, maximum=512)
|
|
1066
|
+
valid_root = text.startswith("$")
|
|
1067
|
+
position = 1 if valid_root else len(text)
|
|
1068
|
+
segments = 0
|
|
1069
|
+
while position < len(text):
|
|
1070
|
+
named = _PROVIDER_NAMED_SEGMENT.match(text, position)
|
|
1071
|
+
if named is not None:
|
|
1072
|
+
position = named.end()
|
|
1073
|
+
segments += 1
|
|
1074
|
+
continue
|
|
1075
|
+
if text.startswith("[*]", position):
|
|
1076
|
+
position += 3
|
|
1077
|
+
segments += 1
|
|
1078
|
+
continue
|
|
1079
|
+
if text.startswith('["', position):
|
|
1080
|
+
end = text.find('"]', position + 2)
|
|
1081
|
+
if end != -1:
|
|
1082
|
+
key = text[position + 2 : end]
|
|
1083
|
+
if key in _PROVIDER_QUOTED_KEYS:
|
|
1084
|
+
position = end + 2
|
|
1085
|
+
segments += 1
|
|
1086
|
+
continue
|
|
1087
|
+
break
|
|
1088
|
+
if not valid_root or position != len(text) or segments == 0:
|
|
1089
|
+
raise SourceContractError(
|
|
1090
|
+
"PROVIDER_PATH",
|
|
1091
|
+
path,
|
|
1092
|
+
"must use canonical named keys, optional [*], and only closed bracket-quoted keys",
|
|
1093
|
+
)
|
|
1094
|
+
return text
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _provider_identifier_path(value: Any, path: str) -> str:
|
|
1098
|
+
text = _provider_path(value, path)
|
|
1099
|
+
if "[*]" in text:
|
|
1100
|
+
raise SourceContractError(
|
|
1101
|
+
"PROVIDER_IDENTIFIER_PATH",
|
|
1102
|
+
path,
|
|
1103
|
+
"must identify one singular named provider field without an array wildcard",
|
|
1104
|
+
)
|
|
1105
|
+
return text
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def _utc_second(value: Any, path: str) -> str:
|
|
1109
|
+
text = _text(value, path, maximum=20)
|
|
1110
|
+
if not _UTC_SECOND.fullmatch(text):
|
|
1111
|
+
raise SourceContractError("TIMESTAMP", path, "must be canonical UTC to whole seconds")
|
|
1112
|
+
try:
|
|
1113
|
+
parsed = datetime.strptime(text, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
|
|
1114
|
+
except ValueError:
|
|
1115
|
+
raise SourceContractError("TIMESTAMP", path, "must be a real UTC timestamp") from None
|
|
1116
|
+
if parsed.strftime("%Y-%m-%dT%H:%M:%SZ") != text:
|
|
1117
|
+
raise SourceContractError("TIMESTAMP", path, "must be canonical UTC to whole seconds")
|
|
1118
|
+
return text
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _known_text(fact: CatalogFact, path: str, *, maximum: int) -> None:
|
|
1122
|
+
if fact.state == "known":
|
|
1123
|
+
_text(fact.value, f"{path}.value", maximum=maximum)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _known_text_tuple(fact: CatalogFact, path: str, *, maximum: int) -> None:
|
|
1127
|
+
if fact.state != "known":
|
|
1128
|
+
return
|
|
1129
|
+
value = fact.value
|
|
1130
|
+
if not isinstance(value, tuple):
|
|
1131
|
+
raise SourceContractError("TYPE", f"{path}.value", "must be an immutable tuple")
|
|
1132
|
+
if len(value) > maximum:
|
|
1133
|
+
raise SourceContractError("LIMIT", f"{path}.value", f"must contain at most {maximum}")
|
|
1134
|
+
for index, item in enumerate(value):
|
|
1135
|
+
_text(item, f"{path}.value[{index}]", maximum=MAX_DECLARED_NAME)
|
|
1136
|
+
if len(set(value)) != len(value):
|
|
1137
|
+
raise SourceContractError("DUPLICATE", f"{path}.value", "values must be unique")
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _known_choice(fact: CatalogFact, path: str, *, allowed: frozenset[str]) -> None:
|
|
1141
|
+
if fact.state == "known":
|
|
1142
|
+
_choice(fact.value, allowed, f"{path}.value")
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _known_choices(
|
|
1146
|
+
fact: CatalogFact,
|
|
1147
|
+
path: str,
|
|
1148
|
+
*,
|
|
1149
|
+
allowed: frozenset[str],
|
|
1150
|
+
maximum: int,
|
|
1151
|
+
) -> None:
|
|
1152
|
+
if fact.state != "known":
|
|
1153
|
+
return
|
|
1154
|
+
value = fact.value
|
|
1155
|
+
if not isinstance(value, tuple):
|
|
1156
|
+
raise SourceContractError("TYPE", f"{path}.value", "must be an immutable tuple")
|
|
1157
|
+
if not value or len(value) > maximum:
|
|
1158
|
+
raise SourceContractError(
|
|
1159
|
+
"LIMIT",
|
|
1160
|
+
f"{path}.value",
|
|
1161
|
+
f"must contain between 1 and {maximum} values",
|
|
1162
|
+
)
|
|
1163
|
+
for index, item in enumerate(value):
|
|
1164
|
+
_choice(item, allowed, f"{path}.value[{index}]")
|
|
1165
|
+
if len(set(value)) != len(value):
|
|
1166
|
+
raise SourceContractError("DUPLICATE", f"{path}.value", "values must be unique")
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def _known_boolean(fact: CatalogFact, path: str) -> None:
|
|
1170
|
+
if fact.state == "known" and type(fact.value) is not bool:
|
|
1171
|
+
raise SourceContractError("BOOLEAN", f"{path}.value", "must be a boolean")
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _known_integer(fact: CatalogFact, path: str, *, minimum: int, maximum: int) -> None:
|
|
1175
|
+
if fact.state != "known":
|
|
1176
|
+
return
|
|
1177
|
+
if type(fact.value) is not int or not minimum <= fact.value <= maximum:
|
|
1178
|
+
raise SourceContractError(
|
|
1179
|
+
"INTEGER",
|
|
1180
|
+
f"{path}.value",
|
|
1181
|
+
f"must be an integer in [{minimum}, {maximum}]",
|
|
1182
|
+
)
|