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,218 @@
|
|
|
1
|
+
"""Strict source-independent integration seam for acquired tabular snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mostlyright.data_harness.acquisition.parsing import ParsedTable
|
|
9
|
+
from mostlyright.data_harness.canonical import canonical_sha256, sha256_bytes
|
|
10
|
+
from mostlyright.data_harness.sources.contracts import SourceContractError
|
|
11
|
+
from mostlyright.data_harness.sources.registry import AcquisitionOutput
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class OneToOneJoinEvidence:
|
|
16
|
+
left_input_sha256: str
|
|
17
|
+
right_input_sha256: str
|
|
18
|
+
join_key: str
|
|
19
|
+
left_rows: int
|
|
20
|
+
right_rows: int
|
|
21
|
+
output_rows: int
|
|
22
|
+
unmatched_left_rows: int
|
|
23
|
+
row_multiplier: float
|
|
24
|
+
output_columns: tuple[str, ...]
|
|
25
|
+
output_rows_value: tuple[tuple[Any, ...], ...]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def digest(self) -> str:
|
|
29
|
+
return canonical_sha256(
|
|
30
|
+
{
|
|
31
|
+
"left_input_sha256": self.left_input_sha256,
|
|
32
|
+
"right_input_sha256": self.right_input_sha256,
|
|
33
|
+
"join_key": self.join_key,
|
|
34
|
+
"left_rows": self.left_rows,
|
|
35
|
+
"right_rows": self.right_rows,
|
|
36
|
+
"output_rows": self.output_rows,
|
|
37
|
+
"unmatched_left_rows": self.unmatched_left_rows,
|
|
38
|
+
"row_multiplier": {
|
|
39
|
+
"numerator": self.output_rows,
|
|
40
|
+
"denominator": self.left_rows,
|
|
41
|
+
},
|
|
42
|
+
"output_columns": list(self.output_columns),
|
|
43
|
+
"output_rows_value": [list(row) for row in self.output_rows_value],
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class AcquiredOneToOneJoinEvidence:
|
|
50
|
+
"""A one-to-one join bound to both exact acquisition receipts and observations."""
|
|
51
|
+
|
|
52
|
+
left_receipt_digest: str
|
|
53
|
+
right_receipt_digest: str
|
|
54
|
+
left_observation_digest: str
|
|
55
|
+
right_observation_digest: str
|
|
56
|
+
join: OneToOneJoinEvidence
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def digest(self) -> str:
|
|
60
|
+
return canonical_sha256(
|
|
61
|
+
{
|
|
62
|
+
"left_receipt_digest": self.left_receipt_digest,
|
|
63
|
+
"right_receipt_digest": self.right_receipt_digest,
|
|
64
|
+
"left_observation_digest": self.left_observation_digest,
|
|
65
|
+
"right_observation_digest": self.right_observation_digest,
|
|
66
|
+
"join_digest": self.join.digest,
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def join_acquired_sources_one_to_one(
|
|
72
|
+
left_output: AcquisitionOutput,
|
|
73
|
+
left_table: ParsedTable,
|
|
74
|
+
right_output: AcquisitionOutput,
|
|
75
|
+
right_table: ParsedTable,
|
|
76
|
+
*,
|
|
77
|
+
join_key: str,
|
|
78
|
+
) -> AcquiredOneToOneJoinEvidence:
|
|
79
|
+
"""Join only tables proven to be parsed from their exact admitted snapshots."""
|
|
80
|
+
|
|
81
|
+
_require_acquired_table_binding(left_output, left_table, "left")
|
|
82
|
+
_require_acquired_table_binding(right_output, right_table, "right")
|
|
83
|
+
return AcquiredOneToOneJoinEvidence(
|
|
84
|
+
left_receipt_digest=left_output.receipt.digest,
|
|
85
|
+
right_receipt_digest=right_output.receipt.digest,
|
|
86
|
+
left_observation_digest=left_output.observation.digest,
|
|
87
|
+
right_observation_digest=right_output.observation.digest,
|
|
88
|
+
join=join_source_tables_one_to_one(left_table, right_table, join_key=join_key),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def join_source_tables_one_to_one(
|
|
93
|
+
left: ParsedTable,
|
|
94
|
+
right: ParsedTable,
|
|
95
|
+
*,
|
|
96
|
+
join_key: str,
|
|
97
|
+
) -> OneToOneJoinEvidence:
|
|
98
|
+
"""Join two acquired tables only when the exact key relation is one-to-one and complete."""
|
|
99
|
+
|
|
100
|
+
if not isinstance(join_key, str) or not join_key:
|
|
101
|
+
raise SourceContractError("JOIN_KEY", "join_key", "must be non-empty text")
|
|
102
|
+
try:
|
|
103
|
+
left_key_index = left.columns.index(join_key)
|
|
104
|
+
right_key_index = right.columns.index(join_key)
|
|
105
|
+
except ValueError:
|
|
106
|
+
raise SourceContractError(
|
|
107
|
+
"JOIN_KEY",
|
|
108
|
+
"join_key",
|
|
109
|
+
"must exist in both acquired tables",
|
|
110
|
+
) from None
|
|
111
|
+
right_extra_columns = tuple(column for column in right.columns if column != join_key)
|
|
112
|
+
overlaps = sorted(set(left.columns) & set(right_extra_columns))
|
|
113
|
+
if overlaps:
|
|
114
|
+
raise SourceContractError(
|
|
115
|
+
"JOIN_COLUMN_COLLISION",
|
|
116
|
+
"right.columns",
|
|
117
|
+
f"non-key columns collide with the left table: {overlaps}",
|
|
118
|
+
)
|
|
119
|
+
right_extra_indexes = tuple(
|
|
120
|
+
index for index, column in enumerate(right.columns) if column != join_key
|
|
121
|
+
)
|
|
122
|
+
left_keys = _unique_non_null_keys(left.rows, left_key_index, "left")
|
|
123
|
+
right_keys = _unique_non_null_keys(right.rows, right_key_index, "right")
|
|
124
|
+
right_by_key = {
|
|
125
|
+
row[right_key_index]: tuple(row[index] for index in right_extra_indexes)
|
|
126
|
+
for row in right.rows
|
|
127
|
+
}
|
|
128
|
+
unmatched = tuple(key for key in left_keys if key not in right_keys)
|
|
129
|
+
if unmatched:
|
|
130
|
+
raise SourceContractError(
|
|
131
|
+
"JOIN_UNMATCHED_LEFT",
|
|
132
|
+
"right.rows",
|
|
133
|
+
f"right table is missing {len(unmatched)} left keys",
|
|
134
|
+
)
|
|
135
|
+
output_rows = tuple((*row, *right_by_key[row[left_key_index]]) for row in left.rows)
|
|
136
|
+
if len(output_rows) != len(left.rows):
|
|
137
|
+
raise SourceContractError(
|
|
138
|
+
"JOIN_ROW_MULTIPLICATION",
|
|
139
|
+
"output_rows",
|
|
140
|
+
"one-to-one join changed the left row count",
|
|
141
|
+
)
|
|
142
|
+
return OneToOneJoinEvidence(
|
|
143
|
+
left_input_sha256=left.input_sha256,
|
|
144
|
+
right_input_sha256=right.input_sha256,
|
|
145
|
+
join_key=join_key,
|
|
146
|
+
left_rows=len(left.rows),
|
|
147
|
+
right_rows=len(right.rows),
|
|
148
|
+
output_rows=len(output_rows),
|
|
149
|
+
unmatched_left_rows=0,
|
|
150
|
+
row_multiplier=1.0,
|
|
151
|
+
output_columns=(*left.columns, *right_extra_columns),
|
|
152
|
+
output_rows_value=output_rows,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _require_acquired_table_binding(
|
|
157
|
+
output: AcquisitionOutput,
|
|
158
|
+
table: ParsedTable,
|
|
159
|
+
side: str,
|
|
160
|
+
) -> None:
|
|
161
|
+
if (
|
|
162
|
+
output.parsed_schema_digest != table.schema_digest
|
|
163
|
+
or output.receipt.snapshot.content_sha256 != table.input_sha256
|
|
164
|
+
or output.receipt.observation_digest != output.observation.digest
|
|
165
|
+
):
|
|
166
|
+
raise SourceContractError(
|
|
167
|
+
"ACQUIRED_TABLE_BINDING",
|
|
168
|
+
f"{side}.acquisition",
|
|
169
|
+
"parsed table is not bound to the exact receipt and observation",
|
|
170
|
+
)
|
|
171
|
+
content = output.snapshot_content
|
|
172
|
+
if content is None:
|
|
173
|
+
raise SourceContractError(
|
|
174
|
+
"ACQUIRED_SNAPSHOT",
|
|
175
|
+
f"{side}.snapshot_content",
|
|
176
|
+
"registry-authenticated snapshot bytes are unavailable",
|
|
177
|
+
)
|
|
178
|
+
if (
|
|
179
|
+
sha256_bytes(content) != output.receipt.snapshot.content_sha256
|
|
180
|
+
or len(content) != output.receipt.snapshot.size_bytes
|
|
181
|
+
):
|
|
182
|
+
raise SourceContractError(
|
|
183
|
+
"ACQUIRED_SNAPSHOT",
|
|
184
|
+
f"{side}.snapshot_content",
|
|
185
|
+
"authenticated snapshot bytes do not match the exact acquisition receipt",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _unique_non_null_keys(
|
|
190
|
+
rows: tuple[tuple[Any, ...], ...],
|
|
191
|
+
key_index: int,
|
|
192
|
+
side: str,
|
|
193
|
+
) -> frozenset[Any]:
|
|
194
|
+
keys: list[Any] = []
|
|
195
|
+
for row in rows:
|
|
196
|
+
key = row[key_index]
|
|
197
|
+
if key is None or key == "":
|
|
198
|
+
raise SourceContractError(
|
|
199
|
+
"JOIN_NULL_KEY",
|
|
200
|
+
f"{side}.rows",
|
|
201
|
+
"join keys must be non-null and non-empty",
|
|
202
|
+
)
|
|
203
|
+
keys.append(key)
|
|
204
|
+
try:
|
|
205
|
+
unique_keys = frozenset(keys)
|
|
206
|
+
except TypeError:
|
|
207
|
+
raise SourceContractError(
|
|
208
|
+
"JOIN_KEY_TYPE",
|
|
209
|
+
f"{side}.rows",
|
|
210
|
+
"join keys must be scalar hashable values",
|
|
211
|
+
) from None
|
|
212
|
+
if len(unique_keys) != len(keys):
|
|
213
|
+
raise SourceContractError(
|
|
214
|
+
"JOIN_CARDINALITY",
|
|
215
|
+
f"{side}.rows",
|
|
216
|
+
"join keys must be unique for a one-to-one integration",
|
|
217
|
+
)
|
|
218
|
+
return unique_keys
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Compose a range acquisition with a pinned Reader and immutable revision evidence.
|
|
2
|
+
|
|
3
|
+
This module owns the public seam that neither the range crawler nor a Reader can own alone.
|
|
4
|
+
The crawler returns exact selected messages and source-relative accounting; the Reader turns one
|
|
5
|
+
message into canonical table bytes. The seam binds those two outcomes before returning anything
|
|
6
|
+
a recipe may seal.
|
|
7
|
+
|
|
8
|
+
It is intentionally source-general. HRRR supplies the first task, but no point, variable, host,
|
|
9
|
+
cycle cadence, or expected value is embedded here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, Protocol
|
|
17
|
+
|
|
18
|
+
from mostlyright.data_harness.acquisition.http import RetrievalLimits
|
|
19
|
+
from mostlyright.data_harness.acquisition.parsing import ParseLimits
|
|
20
|
+
from mostlyright.data_harness.acquisition.sandbox import (
|
|
21
|
+
CrawlerSandbox,
|
|
22
|
+
FetchedMember,
|
|
23
|
+
SandboxResult,
|
|
24
|
+
)
|
|
25
|
+
from mostlyright.data_harness.acquisition.url_policy import AcquisitionSecurityError
|
|
26
|
+
from mostlyright.data_harness.canonical import canonical_sha256, sha256_bytes
|
|
27
|
+
from mostlyright.data_harness.readers.contracts import ReaderBudgets, ReaderPin
|
|
28
|
+
|
|
29
|
+
MAX_SOURCE_OBJECT_BYTES = 1 << 44
|
|
30
|
+
_DIGEST = re.compile(r"^[0-9a-f]{64}$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _RangeReaderSandbox(Protocol):
|
|
34
|
+
def fetch_ranges(self, **kwargs: Any) -> SandboxResult: ...
|
|
35
|
+
|
|
36
|
+
def decode_and_parse(self, **kwargs: Any) -> SandboxResult: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _digest(value: str, subject: str) -> str:
|
|
40
|
+
if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
|
|
41
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", f"{subject} is not a SHA-256")
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _positive(value: int, subject: str, *, maximum: int = MAX_SOURCE_OBJECT_BYTES) -> int:
|
|
46
|
+
if type(value) is not int or not 1 <= value <= maximum:
|
|
47
|
+
raise AcquisitionSecurityError(
|
|
48
|
+
"RANGE_READER_LOCK",
|
|
49
|
+
f"{subject} must be a positive bounded integer",
|
|
50
|
+
)
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class LockedSlice:
|
|
56
|
+
"""One selected message in the operator-frozen source revision."""
|
|
57
|
+
|
|
58
|
+
descriptor: str
|
|
59
|
+
index: int
|
|
60
|
+
first_byte: int
|
|
61
|
+
last_byte: int
|
|
62
|
+
sha256: str
|
|
63
|
+
|
|
64
|
+
def __post_init__(self) -> None:
|
|
65
|
+
if not isinstance(self.descriptor, str) or not self.descriptor:
|
|
66
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", "slice descriptor is empty")
|
|
67
|
+
_positive(self.index, "slice index", maximum=1_000_000)
|
|
68
|
+
if (
|
|
69
|
+
type(self.first_byte) is not int
|
|
70
|
+
or type(self.last_byte) is not int
|
|
71
|
+
or self.first_byte < 0
|
|
72
|
+
or self.last_byte < self.first_byte
|
|
73
|
+
or self.last_byte >= MAX_SOURCE_OBJECT_BYTES
|
|
74
|
+
):
|
|
75
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", "slice byte range is invalid")
|
|
76
|
+
_digest(self.sha256, "slice digest")
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def size_bytes(self) -> int:
|
|
80
|
+
return self.last_byte - self.first_byte + 1
|
|
81
|
+
|
|
82
|
+
def to_dict(self) -> dict[str, Any]:
|
|
83
|
+
return {
|
|
84
|
+
"descriptor": self.descriptor,
|
|
85
|
+
"index": self.index,
|
|
86
|
+
"first_byte": self.first_byte,
|
|
87
|
+
"last_byte": self.last_byte,
|
|
88
|
+
"size_bytes": self.size_bytes,
|
|
89
|
+
"sha256": self.sha256,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class RangeRevisionLock:
|
|
95
|
+
"""The source revision the coordinator selected before contestant execution."""
|
|
96
|
+
|
|
97
|
+
cycle: str
|
|
98
|
+
sidecar_url: str
|
|
99
|
+
sidecar_final_url: str
|
|
100
|
+
object_url: str
|
|
101
|
+
sidecar_sha256: str
|
|
102
|
+
sidecar_size_bytes: int
|
|
103
|
+
sidecar_etag: str | None
|
|
104
|
+
sidecar_last_modified: str | None
|
|
105
|
+
object_etag: str | None
|
|
106
|
+
object_last_modified: str | None
|
|
107
|
+
full_object_size_bytes: int
|
|
108
|
+
slices: tuple[LockedSlice, ...]
|
|
109
|
+
|
|
110
|
+
def __post_init__(self) -> None:
|
|
111
|
+
if not isinstance(self.cycle, str) or not self.cycle:
|
|
112
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", "cycle is empty")
|
|
113
|
+
for subject, url in (
|
|
114
|
+
("sidecar URL", self.sidecar_url),
|
|
115
|
+
("sidecar final URL", self.sidecar_final_url),
|
|
116
|
+
("object URL", self.object_url),
|
|
117
|
+
):
|
|
118
|
+
if not isinstance(url, str) or not url.startswith("https://") or len(url) > 2_048:
|
|
119
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", f"{subject} is invalid")
|
|
120
|
+
_digest(self.sidecar_sha256, "sidecar digest")
|
|
121
|
+
_positive(self.sidecar_size_bytes, "sidecar size", maximum=1 << 30)
|
|
122
|
+
_positive(self.full_object_size_bytes, "full object size")
|
|
123
|
+
for subject, pair in (
|
|
124
|
+
("sidecar", (self.sidecar_etag, self.sidecar_last_modified)),
|
|
125
|
+
("object", (self.object_etag, self.object_last_modified)),
|
|
126
|
+
):
|
|
127
|
+
if pair == (None, None):
|
|
128
|
+
raise AcquisitionSecurityError(
|
|
129
|
+
"RANGE_READER_LOCK", f"{subject} revision has no HTTP validator"
|
|
130
|
+
)
|
|
131
|
+
for value in pair:
|
|
132
|
+
if value is not None and (
|
|
133
|
+
not isinstance(value, str) or not 1 <= len(value) <= 1_024
|
|
134
|
+
):
|
|
135
|
+
raise AcquisitionSecurityError(
|
|
136
|
+
"RANGE_READER_LOCK", f"{subject} validator is invalid"
|
|
137
|
+
)
|
|
138
|
+
if not isinstance(self.slices, tuple) or len(self.slices) != 1:
|
|
139
|
+
raise AcquisitionSecurityError(
|
|
140
|
+
"RANGE_READER_LOCK",
|
|
141
|
+
"this contract version requires exactly one selected message",
|
|
142
|
+
)
|
|
143
|
+
if self.slices[0].last_byte >= self.full_object_size_bytes:
|
|
144
|
+
raise AcquisitionSecurityError(
|
|
145
|
+
"RANGE_READER_LOCK",
|
|
146
|
+
"selected message runs past the locked object",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def digest(self) -> str:
|
|
151
|
+
return canonical_sha256(self.to_dict())
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def source_revision_digest(self) -> str:
|
|
155
|
+
return canonical_sha256(
|
|
156
|
+
{
|
|
157
|
+
"cycle": self.cycle,
|
|
158
|
+
"sidecar_final_url": self.sidecar_final_url,
|
|
159
|
+
"sidecar_sha256": self.sidecar_sha256,
|
|
160
|
+
"sidecar_size_bytes": self.sidecar_size_bytes,
|
|
161
|
+
"sidecar_etag": self.sidecar_etag,
|
|
162
|
+
"sidecar_last_modified": self.sidecar_last_modified,
|
|
163
|
+
"object_url": self.object_url,
|
|
164
|
+
"object_etag": self.object_etag,
|
|
165
|
+
"object_last_modified": self.object_last_modified,
|
|
166
|
+
"full_object_size_bytes": self.full_object_size_bytes,
|
|
167
|
+
"slices": [item.to_dict() for item in self.slices],
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def to_dict(self) -> dict[str, Any]:
|
|
172
|
+
return {
|
|
173
|
+
"cycle": self.cycle,
|
|
174
|
+
"sidecar_url": self.sidecar_url,
|
|
175
|
+
"sidecar_final_url": self.sidecar_final_url,
|
|
176
|
+
"object_url": self.object_url,
|
|
177
|
+
"sidecar_sha256": self.sidecar_sha256,
|
|
178
|
+
"sidecar_size_bytes": self.sidecar_size_bytes,
|
|
179
|
+
"sidecar_etag": self.sidecar_etag,
|
|
180
|
+
"sidecar_last_modified": self.sidecar_last_modified,
|
|
181
|
+
"object_etag": self.object_etag,
|
|
182
|
+
"object_last_modified": self.object_last_modified,
|
|
183
|
+
"full_object_size_bytes": self.full_object_size_bytes,
|
|
184
|
+
"slices": [item.to_dict() for item in self.slices],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def from_dict(cls, value: Any) -> RangeRevisionLock:
|
|
189
|
+
if not isinstance(value, dict) or set(value) != {
|
|
190
|
+
"cycle",
|
|
191
|
+
"sidecar_url",
|
|
192
|
+
"sidecar_final_url",
|
|
193
|
+
"object_url",
|
|
194
|
+
"sidecar_sha256",
|
|
195
|
+
"sidecar_size_bytes",
|
|
196
|
+
"sidecar_etag",
|
|
197
|
+
"sidecar_last_modified",
|
|
198
|
+
"object_etag",
|
|
199
|
+
"object_last_modified",
|
|
200
|
+
"full_object_size_bytes",
|
|
201
|
+
"slices",
|
|
202
|
+
}:
|
|
203
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", "revision lock shape is invalid")
|
|
204
|
+
raw_slices = value["slices"]
|
|
205
|
+
if not isinstance(raw_slices, list):
|
|
206
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", "lock slices are not an array")
|
|
207
|
+
slices: list[LockedSlice] = []
|
|
208
|
+
for raw in raw_slices:
|
|
209
|
+
if not isinstance(raw, dict) or set(raw) != {
|
|
210
|
+
"descriptor",
|
|
211
|
+
"index",
|
|
212
|
+
"first_byte",
|
|
213
|
+
"last_byte",
|
|
214
|
+
"size_bytes",
|
|
215
|
+
"sha256",
|
|
216
|
+
}:
|
|
217
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", "locked slice shape is invalid")
|
|
218
|
+
stated_size = raw["size_bytes"]
|
|
219
|
+
item = LockedSlice(
|
|
220
|
+
descriptor=raw["descriptor"],
|
|
221
|
+
index=raw["index"],
|
|
222
|
+
first_byte=raw["first_byte"],
|
|
223
|
+
last_byte=raw["last_byte"],
|
|
224
|
+
sha256=raw["sha256"],
|
|
225
|
+
)
|
|
226
|
+
if stated_size != item.size_bytes:
|
|
227
|
+
raise AcquisitionSecurityError("RANGE_READER_LOCK", "locked slice size is invalid")
|
|
228
|
+
slices.append(item)
|
|
229
|
+
kwargs = dict(value)
|
|
230
|
+
kwargs.pop("slices")
|
|
231
|
+
return cls(**kwargs, slices=tuple(slices))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass(frozen=True)
|
|
235
|
+
class RangeReaderOutput:
|
|
236
|
+
"""Canonical Reader bytes plus the exact recipe-consumable evidence document."""
|
|
237
|
+
|
|
238
|
+
content: bytes
|
|
239
|
+
media_type: str
|
|
240
|
+
parsed_schema_digest: str
|
|
241
|
+
decode_flags: tuple[str, ...]
|
|
242
|
+
fetched_members: tuple[FetchedMember, ...]
|
|
243
|
+
transport_evidence_digest: str
|
|
244
|
+
efficiency_numerator: int
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def acquire_range_reader(
|
|
248
|
+
sandbox: _RangeReaderSandbox,
|
|
249
|
+
*,
|
|
250
|
+
request_id: str,
|
|
251
|
+
lock: RangeRevisionLock,
|
|
252
|
+
allowed_hostnames: tuple[str, ...],
|
|
253
|
+
retrieval_limits: RetrievalLimits,
|
|
254
|
+
reader_pin: ReaderPin,
|
|
255
|
+
reader_budgets: ReaderBudgets,
|
|
256
|
+
parse_limits: ParseLimits,
|
|
257
|
+
expected_fetch_policy_digest: str,
|
|
258
|
+
expected_decode_policy_digest: str,
|
|
259
|
+
) -> RangeReaderOutput:
|
|
260
|
+
"""Fetch, revision-check, decode and bind one selected message without a glue-script gap."""
|
|
261
|
+
|
|
262
|
+
_digest(expected_fetch_policy_digest, "fetch policy digest")
|
|
263
|
+
_digest(expected_decode_policy_digest, "decode policy digest")
|
|
264
|
+
fetched = sandbox.fetch_ranges(
|
|
265
|
+
request_id=request_id,
|
|
266
|
+
sidecar_url=lock.sidecar_url,
|
|
267
|
+
object_url=lock.object_url,
|
|
268
|
+
selectors=tuple(item.descriptor for item in lock.slices),
|
|
269
|
+
allowed_hostnames=allowed_hostnames,
|
|
270
|
+
retrieval_limits=retrieval_limits,
|
|
271
|
+
expected_reference_time=lock.cycle,
|
|
272
|
+
)
|
|
273
|
+
if fetched.policy_digest != expected_fetch_policy_digest:
|
|
274
|
+
raise AcquisitionSecurityError(
|
|
275
|
+
"RANGE_READER_POLICY",
|
|
276
|
+
"range result does not bind the coordinator-pinned fetch policy",
|
|
277
|
+
)
|
|
278
|
+
if (
|
|
279
|
+
fetched.ranges is None
|
|
280
|
+
or len(fetched.ranges) != len(lock.slices)
|
|
281
|
+
or fetched.sidecar_content_sha256 != lock.sidecar_sha256
|
|
282
|
+
or fetched.sidecar_size_bytes != lock.sidecar_size_bytes
|
|
283
|
+
or fetched.full_object_size_bytes != lock.full_object_size_bytes
|
|
284
|
+
or fetched.fetched_members is None
|
|
285
|
+
or fetched.transport_evidence_digest is None
|
|
286
|
+
or fetched.final_url is None
|
|
287
|
+
):
|
|
288
|
+
raise AcquisitionSecurityError(
|
|
289
|
+
"RANGE_READER_REVISION",
|
|
290
|
+
"range result does not match the locked sidecar and object revision",
|
|
291
|
+
)
|
|
292
|
+
for delivered, expected in zip(fetched.ranges, lock.slices, strict=True):
|
|
293
|
+
if (
|
|
294
|
+
delivered.descriptor != expected.descriptor
|
|
295
|
+
or delivered.index != expected.index
|
|
296
|
+
or delivered.first_byte != expected.first_byte
|
|
297
|
+
or delivered.last_byte != expected.last_byte
|
|
298
|
+
or delivered.sha256 != expected.sha256
|
|
299
|
+
or len(delivered.content) != expected.size_bytes
|
|
300
|
+
):
|
|
301
|
+
raise AcquisitionSecurityError(
|
|
302
|
+
"RANGE_READER_REVISION",
|
|
303
|
+
"delivered message differs from the locked selection",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
members = fetched.fetched_members
|
|
307
|
+
roles = tuple(member.role for member in members)
|
|
308
|
+
terminal = {
|
|
309
|
+
role: members[max(index for index, value in enumerate(roles) if value == role)]
|
|
310
|
+
for role in ("cycle_probe", "sidecar_initial", "selected_slice", "sidecar_final")
|
|
311
|
+
}
|
|
312
|
+
sidecar_initial = terminal["sidecar_initial"]
|
|
313
|
+
sidecar_final = terminal["sidecar_final"]
|
|
314
|
+
object_probe = terminal["cycle_probe"]
|
|
315
|
+
object_slice = terminal["selected_slice"]
|
|
316
|
+
if (
|
|
317
|
+
fetched.final_url != lock.sidecar_final_url
|
|
318
|
+
or sidecar_initial.url != lock.sidecar_final_url
|
|
319
|
+
or sidecar_final.url != lock.sidecar_final_url
|
|
320
|
+
or sidecar_initial.response_body_sha256 != lock.sidecar_sha256
|
|
321
|
+
or sidecar_final.response_body_sha256 != lock.sidecar_sha256
|
|
322
|
+
or sidecar_initial.response_body_size_bytes != lock.sidecar_size_bytes
|
|
323
|
+
or sidecar_final.response_body_size_bytes != lock.sidecar_size_bytes
|
|
324
|
+
or (sidecar_initial.etag, sidecar_initial.last_modified)
|
|
325
|
+
!= (lock.sidecar_etag, lock.sidecar_last_modified)
|
|
326
|
+
or (sidecar_final.etag, sidecar_final.last_modified)
|
|
327
|
+
!= (lock.sidecar_etag, lock.sidecar_last_modified)
|
|
328
|
+
or object_probe.url != lock.object_url
|
|
329
|
+
or object_slice.url != lock.object_url
|
|
330
|
+
or (object_probe.etag, object_probe.last_modified)
|
|
331
|
+
!= (lock.object_etag, lock.object_last_modified)
|
|
332
|
+
or (object_slice.etag, object_slice.last_modified)
|
|
333
|
+
!= (lock.object_etag, lock.object_last_modified)
|
|
334
|
+
):
|
|
335
|
+
raise AcquisitionSecurityError(
|
|
336
|
+
"RANGE_READER_REVISION",
|
|
337
|
+
"response validators or final member identities differ from the locked revision",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
selected = fetched.ranges[0]
|
|
341
|
+
decoded = sandbox.decode_and_parse(
|
|
342
|
+
request_id=f"{request_id}.decode",
|
|
343
|
+
content=selected.content,
|
|
344
|
+
reader_pin={
|
|
345
|
+
"family_id": reader_pin.family_id,
|
|
346
|
+
"family_version": reader_pin.family_version,
|
|
347
|
+
"decode_options": dict(reader_pin.decode_options),
|
|
348
|
+
},
|
|
349
|
+
output_format="csv",
|
|
350
|
+
limits=parse_limits,
|
|
351
|
+
reader_budgets={
|
|
352
|
+
name: getattr(reader_budgets, name) for name in reader_budgets.__dataclass_fields__
|
|
353
|
+
},
|
|
354
|
+
)
|
|
355
|
+
if decoded.policy_digest != expected_decode_policy_digest:
|
|
356
|
+
raise AcquisitionSecurityError(
|
|
357
|
+
"RANGE_READER_POLICY",
|
|
358
|
+
"decode result does not bind the coordinator-pinned decode policy",
|
|
359
|
+
)
|
|
360
|
+
if (
|
|
361
|
+
decoded.content is None
|
|
362
|
+
or decoded.media_type is None
|
|
363
|
+
or decoded.parsed is None
|
|
364
|
+
or decoded.parsed.input_sha256 != sha256_bytes(decoded.content)
|
|
365
|
+
or decoded.decode_family_id != reader_pin.family_id
|
|
366
|
+
or decoded.decode_family_version != reader_pin.family_version
|
|
367
|
+
or decoded.decode_options_digest != reader_pin.options_digest
|
|
368
|
+
or decoded.decode_flags is None
|
|
369
|
+
):
|
|
370
|
+
raise AcquisitionSecurityError(
|
|
371
|
+
"RANGE_READER_DECODE",
|
|
372
|
+
"decode result is incomplete or does not match the pinned Reader",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Receipt v2 historically measures retained response bodies only. The HTTP and sandbox
|
|
376
|
+
# runtime enforce the stronger cumulative budget (including failed-peer partial bodies), but
|
|
377
|
+
# changing this sealed formula would invalidate already published v2 receipts.
|
|
378
|
+
numerator = sum(member.response_body_size_bytes for member in members)
|
|
379
|
+
if numerator * 100 > lock.full_object_size_bytes:
|
|
380
|
+
raise AcquisitionSecurityError(
|
|
381
|
+
"RANGE_READER_EFFICIENCY",
|
|
382
|
+
"source response bodies exceed one percent of the locked object",
|
|
383
|
+
)
|
|
384
|
+
return RangeReaderOutput(
|
|
385
|
+
content=decoded.content,
|
|
386
|
+
media_type=decoded.media_type,
|
|
387
|
+
parsed_schema_digest=decoded.parsed.schema_digest,
|
|
388
|
+
decode_flags=decoded.decode_flags,
|
|
389
|
+
fetched_members=members,
|
|
390
|
+
transport_evidence_digest=fetched.transport_evidence_digest,
|
|
391
|
+
efficiency_numerator=numerator,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def default_policy_digests() -> tuple[str, str]:
|
|
396
|
+
"""Expose the exact two sandbox authorities an operator must pin for this composition."""
|
|
397
|
+
|
|
398
|
+
from mostlyright.data_harness.acquisition.sandbox import sandbox_policy_digest
|
|
399
|
+
|
|
400
|
+
return sandbox_policy_digest("fetch_ranges"), sandbox_policy_digest("decode_and_parse")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def range_reader_policy_digest() -> str:
|
|
404
|
+
"""Coordinator pin for the exact two sandbox policies used by the composed adapter."""
|
|
405
|
+
|
|
406
|
+
fetch, decode = default_policy_digests()
|
|
407
|
+
return canonical_sha256({"fetch_ranges": fetch, "decode_and_parse": decode})
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def require_crawler_sandbox(value: Any) -> CrawlerSandbox:
|
|
411
|
+
"""A narrow runtime guard for callers constructing the production path."""
|
|
412
|
+
|
|
413
|
+
if not isinstance(value, CrawlerSandbox):
|
|
414
|
+
raise AcquisitionSecurityError(
|
|
415
|
+
"RANGE_READER_SANDBOX",
|
|
416
|
+
"range Reader acquisition needs the shipped clean-room runner",
|
|
417
|
+
)
|
|
418
|
+
return value
|