mostlyright-data 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""Allowlisted source-adapter registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import stat
|
|
9
|
+
from dataclasses import dataclass, field, replace
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
from mostlyright.data_harness.acquisition.http import ProbeTransport
|
|
14
|
+
from mostlyright.data_harness.sources.contracts import (
|
|
15
|
+
AcquisitionReceipt,
|
|
16
|
+
AcquisitionReceiptV2,
|
|
17
|
+
AcquisitionReceiptV3,
|
|
18
|
+
AcquisitionRequest,
|
|
19
|
+
AdapterDescriptor,
|
|
20
|
+
SourceContractError,
|
|
21
|
+
SourceObservation,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class AcquisitionContext:
|
|
27
|
+
"""Trusted coordinator inputs that source content cannot override."""
|
|
28
|
+
|
|
29
|
+
snapshot_root: Path
|
|
30
|
+
trusted_clock: str
|
|
31
|
+
attempt_id: str
|
|
32
|
+
max_source_bytes: int
|
|
33
|
+
max_rows: int
|
|
34
|
+
max_columns: int
|
|
35
|
+
crawler_sandbox_attestation_digest: str | None = None
|
|
36
|
+
# The decode/normalize output budget, distinct from the fetched-byte budget above. It
|
|
37
|
+
# narrows Reader decode output and bounds the sealed snapshot; ``None`` means "no separate
|
|
38
|
+
# budget was stated" and every consumer falls back to ``max_source_bytes``, which is the
|
|
39
|
+
# historical behaviour.
|
|
40
|
+
max_normalized_bytes: int | None = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
if not self.snapshot_root.is_absolute():
|
|
44
|
+
raise SourceContractError(
|
|
45
|
+
"TRUSTED_ROOT",
|
|
46
|
+
"context.snapshot_root",
|
|
47
|
+
"trusted snapshot root must be absolute",
|
|
48
|
+
)
|
|
49
|
+
if type(self.max_source_bytes) is not int or not 1 <= self.max_source_bytes <= 1 << 34:
|
|
50
|
+
raise SourceContractError(
|
|
51
|
+
"LIMIT",
|
|
52
|
+
"context.max_source_bytes",
|
|
53
|
+
"must be a positive bounded integer",
|
|
54
|
+
)
|
|
55
|
+
if self.max_normalized_bytes is not None and (
|
|
56
|
+
type(self.max_normalized_bytes) is not int
|
|
57
|
+
or not 1 <= self.max_normalized_bytes <= 1 << 34
|
|
58
|
+
):
|
|
59
|
+
raise SourceContractError(
|
|
60
|
+
"LIMIT",
|
|
61
|
+
"context.max_normalized_bytes",
|
|
62
|
+
"must be a positive bounded integer",
|
|
63
|
+
)
|
|
64
|
+
if type(self.max_rows) is not int or not 1 <= self.max_rows <= 10_000_000:
|
|
65
|
+
raise SourceContractError(
|
|
66
|
+
"LIMIT",
|
|
67
|
+
"context.max_rows",
|
|
68
|
+
"must be a positive bounded integer",
|
|
69
|
+
)
|
|
70
|
+
if type(self.max_columns) is not int or not 1 <= self.max_columns <= 10_000:
|
|
71
|
+
raise SourceContractError(
|
|
72
|
+
"LIMIT",
|
|
73
|
+
"context.max_columns",
|
|
74
|
+
"must be a positive bounded integer",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class AcquisitionOutput:
|
|
80
|
+
"""Immutable adapter result binding one receipt, its observation, and the sealed snapshot."""
|
|
81
|
+
|
|
82
|
+
receipt: AcquisitionReceipt | AcquisitionReceiptV2 | AcquisitionReceiptV3
|
|
83
|
+
observation: SourceObservation
|
|
84
|
+
parsed_schema_digest: str | None = None
|
|
85
|
+
snapshot_path: Path | None = None
|
|
86
|
+
next_watermark: str | None = None
|
|
87
|
+
snapshot_content: bytes | None = field(default=None, repr=False)
|
|
88
|
+
# What the terminal response stated, sealed inside the Clean room and carried out hashed.
|
|
89
|
+
# It is not on the receipt: a receipt is sealed evidence whose digest is bound across the
|
|
90
|
+
# repository, and this fact belongs to the empirical observation the probe produces rather
|
|
91
|
+
# than to the artifact it acquired. ``None`` for every adapter that settles on no single
|
|
92
|
+
# response, which is every adapter but the whole-document public HTTPS one.
|
|
93
|
+
probe_transport: ProbeTransport | None = None
|
|
94
|
+
# The parsed geometry an empirical observation states beside the schema digest. Both are
|
|
95
|
+
# facts about the same parse ``parsed_schema_digest`` already comes from, carried rather
|
|
96
|
+
# than recomputed so a consumer cannot derive a second answer from a second parse.
|
|
97
|
+
parsed_row_count: int | None = None
|
|
98
|
+
parsed_row_digest: str | None = None
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
if self.receipt.source_id != self.observation.source_id:
|
|
102
|
+
raise SourceContractError(
|
|
103
|
+
"SOURCE_BINDING",
|
|
104
|
+
"output",
|
|
105
|
+
"receipt and observation source IDs must match",
|
|
106
|
+
)
|
|
107
|
+
if self.receipt.observation_digest != self.observation.digest:
|
|
108
|
+
raise SourceContractError(
|
|
109
|
+
"OBSERVATION_BINDING",
|
|
110
|
+
"output.receipt.observation_digest",
|
|
111
|
+
"receipt does not bind the exact source observation",
|
|
112
|
+
)
|
|
113
|
+
if self.receipt.observed_at != self.observation.observed_at:
|
|
114
|
+
raise SourceContractError(
|
|
115
|
+
"OBSERVATION_TIME_BINDING",
|
|
116
|
+
"output.receipt.observed_at",
|
|
117
|
+
"receipt and observation times differ",
|
|
118
|
+
)
|
|
119
|
+
if self.receipt.event_time_field != self.observation.event_time_field:
|
|
120
|
+
raise SourceContractError(
|
|
121
|
+
"EVENT_TIME_BINDING",
|
|
122
|
+
"output.receipt.event_time_field",
|
|
123
|
+
"receipt and observation event-time fields differ",
|
|
124
|
+
)
|
|
125
|
+
if self.parsed_schema_digest is not None and (
|
|
126
|
+
len(self.parsed_schema_digest) != 64
|
|
127
|
+
or any(char not in "0123456789abcdef" for char in self.parsed_schema_digest)
|
|
128
|
+
):
|
|
129
|
+
raise SourceContractError(
|
|
130
|
+
"PARSED_SCHEMA_DIGEST",
|
|
131
|
+
"output.parsed_schema_digest",
|
|
132
|
+
"must be a lowercase SHA-256",
|
|
133
|
+
)
|
|
134
|
+
if self.snapshot_path is not None and not self.snapshot_path.is_absolute():
|
|
135
|
+
raise SourceContractError(
|
|
136
|
+
"SNAPSHOT_PATH",
|
|
137
|
+
"output.snapshot_path",
|
|
138
|
+
"must be an absolute coordinator-owned path",
|
|
139
|
+
)
|
|
140
|
+
if self.snapshot_content is not None:
|
|
141
|
+
if not isinstance(self.snapshot_content, bytes):
|
|
142
|
+
raise SourceContractError(
|
|
143
|
+
"SNAPSHOT_CONTENT",
|
|
144
|
+
"output.snapshot_content",
|
|
145
|
+
"must be immutable bytes",
|
|
146
|
+
)
|
|
147
|
+
if (
|
|
148
|
+
len(self.snapshot_content) != self.receipt.snapshot.size_bytes
|
|
149
|
+
or hashlib.sha256(self.snapshot_content).hexdigest()
|
|
150
|
+
!= self.receipt.snapshot.content_sha256
|
|
151
|
+
):
|
|
152
|
+
raise SourceContractError(
|
|
153
|
+
"SNAPSHOT_CONTENT",
|
|
154
|
+
"output.snapshot_content",
|
|
155
|
+
"must match the exact acquisition receipt",
|
|
156
|
+
)
|
|
157
|
+
if self.probe_transport is not None and not isinstance(
|
|
158
|
+
self.probe_transport, ProbeTransport
|
|
159
|
+
):
|
|
160
|
+
raise SourceContractError(
|
|
161
|
+
"PROBE_TRANSPORT",
|
|
162
|
+
"output.probe_transport",
|
|
163
|
+
"must be sealed transport evidence",
|
|
164
|
+
)
|
|
165
|
+
row_facts = (self.parsed_row_count, self.parsed_row_digest)
|
|
166
|
+
if any(item is None for item in row_facts) and not all(item is None for item in row_facts):
|
|
167
|
+
raise SourceContractError(
|
|
168
|
+
"PARSED_ROW_FACTS",
|
|
169
|
+
"output.parsed_row_count",
|
|
170
|
+
"row count and row digest are both present or both absent",
|
|
171
|
+
)
|
|
172
|
+
if self.parsed_row_digest is not None and (
|
|
173
|
+
len(self.parsed_row_digest) != 64
|
|
174
|
+
or any(char not in "0123456789abcdef" for char in self.parsed_row_digest)
|
|
175
|
+
):
|
|
176
|
+
raise SourceContractError(
|
|
177
|
+
"PARSED_ROW_FACTS",
|
|
178
|
+
"output.parsed_row_digest",
|
|
179
|
+
"must be a lowercase SHA-256",
|
|
180
|
+
)
|
|
181
|
+
if self.next_watermark is not None and (
|
|
182
|
+
not isinstance(self.next_watermark, str)
|
|
183
|
+
or not 1 <= len(self.next_watermark.encode("utf-8")) <= 512
|
|
184
|
+
):
|
|
185
|
+
raise SourceContractError(
|
|
186
|
+
"WATERMARK",
|
|
187
|
+
"output.next_watermark",
|
|
188
|
+
"must be bounded nonempty text",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SourceAdapter(Protocol):
|
|
193
|
+
@property
|
|
194
|
+
def descriptor(self) -> AdapterDescriptor:
|
|
195
|
+
"""Return the immutable adapter identity."""
|
|
196
|
+
|
|
197
|
+
def acquire(
|
|
198
|
+
self,
|
|
199
|
+
request: AcquisitionRequest,
|
|
200
|
+
context: AcquisitionContext,
|
|
201
|
+
) -> AcquisitionOutput:
|
|
202
|
+
"""Acquire through the adapter's deterministic boundary."""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class AdapterRegistry:
|
|
206
|
+
"""Coordinator-owned adapter allowlist."""
|
|
207
|
+
|
|
208
|
+
def __init__(self, adapters: tuple[SourceAdapter, ...] = ()) -> None:
|
|
209
|
+
self._adapters: dict[tuple[str, str], SourceAdapter] = {}
|
|
210
|
+
for adapter in adapters:
|
|
211
|
+
self.register(adapter)
|
|
212
|
+
|
|
213
|
+
def register(self, adapter: SourceAdapter) -> None:
|
|
214
|
+
descriptor = adapter.descriptor
|
|
215
|
+
if not isinstance(descriptor, AdapterDescriptor):
|
|
216
|
+
raise SourceContractError(
|
|
217
|
+
"ADAPTER_DESCRIPTOR",
|
|
218
|
+
"adapter.descriptor",
|
|
219
|
+
"adapter descriptor must use the strict contract",
|
|
220
|
+
)
|
|
221
|
+
key = (descriptor.adapter_id, descriptor.adapter_version)
|
|
222
|
+
if key in self._adapters:
|
|
223
|
+
coordinate = f"{descriptor.adapter_id}@{descriptor.adapter_version}"
|
|
224
|
+
raise SourceContractError(
|
|
225
|
+
"ADAPTER_DUPLICATE",
|
|
226
|
+
"registry",
|
|
227
|
+
f"adapter {coordinate} is already registered",
|
|
228
|
+
)
|
|
229
|
+
self._adapters[key] = adapter
|
|
230
|
+
|
|
231
|
+
def resolve(self, adapter_id: str, adapter_version: str) -> SourceAdapter:
|
|
232
|
+
try:
|
|
233
|
+
return self._adapters[(adapter_id, adapter_version)]
|
|
234
|
+
except KeyError:
|
|
235
|
+
raise SourceContractError(
|
|
236
|
+
"ADAPTER_UNAVAILABLE",
|
|
237
|
+
"request.adapter_id",
|
|
238
|
+
f"adapter {adapter_id}@{adapter_version} is not allowlisted",
|
|
239
|
+
) from None
|
|
240
|
+
|
|
241
|
+
def acquire(
|
|
242
|
+
self,
|
|
243
|
+
request: AcquisitionRequest,
|
|
244
|
+
context: AcquisitionContext,
|
|
245
|
+
) -> AcquisitionOutput:
|
|
246
|
+
adapter = self.resolve(request.adapter_id, request.adapter_version)
|
|
247
|
+
output = adapter.acquire(request, context)
|
|
248
|
+
descriptor = adapter.descriptor
|
|
249
|
+
if output.receipt.request_digest != request.digest:
|
|
250
|
+
raise SourceContractError(
|
|
251
|
+
"REQUEST_BINDING",
|
|
252
|
+
"output.receipt.request_digest",
|
|
253
|
+
"adapter receipt does not bind the exact request",
|
|
254
|
+
)
|
|
255
|
+
if output.receipt.query_digest != request.query_digest:
|
|
256
|
+
raise SourceContractError(
|
|
257
|
+
"QUERY_BINDING",
|
|
258
|
+
"output.receipt.query_digest",
|
|
259
|
+
"adapter receipt does not bind the exact query",
|
|
260
|
+
)
|
|
261
|
+
if output.receipt.adapter != descriptor:
|
|
262
|
+
raise SourceContractError(
|
|
263
|
+
"ADAPTER_BINDING",
|
|
264
|
+
"output.receipt.adapter",
|
|
265
|
+
"adapter receipt identity differs from the registered adapter",
|
|
266
|
+
)
|
|
267
|
+
trusted_path, trusted_content = _authenticate_acquisition_snapshot(context, output)
|
|
268
|
+
return replace(
|
|
269
|
+
output,
|
|
270
|
+
snapshot_path=trusted_path,
|
|
271
|
+
snapshot_content=trusted_content,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def inventory(self) -> tuple[AdapterDescriptor, ...]:
|
|
275
|
+
return tuple(self._adapters[key].descriptor for key in sorted(self._adapters))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _authenticate_acquisition_snapshot(
|
|
279
|
+
context: AcquisitionContext,
|
|
280
|
+
output: AcquisitionOutput,
|
|
281
|
+
) -> tuple[Path, bytes]:
|
|
282
|
+
"""Return receipt-addressed bytes authenticated beneath the exact trusted root."""
|
|
283
|
+
|
|
284
|
+
reference = output.receipt.snapshot
|
|
285
|
+
expected_name = f"{reference.content_sha256}.{reference.data_format}"
|
|
286
|
+
if reference.storage_name != expected_name:
|
|
287
|
+
raise SourceContractError(
|
|
288
|
+
"ACQUISITION_SNAPSHOT",
|
|
289
|
+
"output.receipt.snapshot.storage_name",
|
|
290
|
+
"must be the exact receipt-addressed snapshot name",
|
|
291
|
+
)
|
|
292
|
+
trusted_path = context.snapshot_root / expected_name
|
|
293
|
+
if output.snapshot_path is not None and output.snapshot_path != trusted_path:
|
|
294
|
+
raise SourceContractError(
|
|
295
|
+
"ACQUISITION_SNAPSHOT",
|
|
296
|
+
"output.snapshot_path",
|
|
297
|
+
"adapter path differs from the receipt-addressed trusted-root path",
|
|
298
|
+
)
|
|
299
|
+
# The sealed snapshot is the normalized acquisition output, so its bound is the
|
|
300
|
+
# normalized-output budget when one was stated, falling back to the fetched-byte
|
|
301
|
+
# budget exactly as the hosted query composition does.
|
|
302
|
+
snapshot_limit = (
|
|
303
|
+
context.max_source_bytes
|
|
304
|
+
if context.max_normalized_bytes is None
|
|
305
|
+
else context.max_normalized_bytes
|
|
306
|
+
)
|
|
307
|
+
if reference.size_bytes > snapshot_limit:
|
|
308
|
+
raise SourceContractError(
|
|
309
|
+
"ACQUISITION_SNAPSHOT",
|
|
310
|
+
"output.receipt.snapshot.size_bytes",
|
|
311
|
+
"snapshot exceeds the trusted acquisition byte limit",
|
|
312
|
+
)
|
|
313
|
+
directory_flags = (
|
|
314
|
+
os.O_RDONLY
|
|
315
|
+
| getattr(os, "O_DIRECTORY", 0)
|
|
316
|
+
| getattr(os, "O_NOFOLLOW", 0)
|
|
317
|
+
| getattr(os, "O_CLOEXEC", 0)
|
|
318
|
+
)
|
|
319
|
+
try:
|
|
320
|
+
root_fd = os.open(context.snapshot_root, directory_flags)
|
|
321
|
+
except OSError as error:
|
|
322
|
+
raise SourceContractError(
|
|
323
|
+
"ACQUISITION_SNAPSHOT",
|
|
324
|
+
"context.snapshot_root",
|
|
325
|
+
"trusted snapshot root is unavailable or unsafe"
|
|
326
|
+
f" (open refused: {_describe_os_error(error)})",
|
|
327
|
+
) from None
|
|
328
|
+
descriptor: int | None = None
|
|
329
|
+
try:
|
|
330
|
+
root_info = os.fstat(root_fd)
|
|
331
|
+
flags = (
|
|
332
|
+
os.O_RDONLY
|
|
333
|
+
| getattr(os, "O_NOFOLLOW", 0)
|
|
334
|
+
| getattr(os, "O_NONBLOCK", 0)
|
|
335
|
+
| getattr(os, "O_CLOEXEC", 0)
|
|
336
|
+
)
|
|
337
|
+
try:
|
|
338
|
+
descriptor = os.open(expected_name, flags, dir_fd=root_fd)
|
|
339
|
+
except OSError as error:
|
|
340
|
+
raise SourceContractError(
|
|
341
|
+
"ACQUISITION_SNAPSHOT",
|
|
342
|
+
"output.receipt.snapshot.storage_name",
|
|
343
|
+
"receipt-addressed snapshot is unavailable or unsafe"
|
|
344
|
+
f" (open refused: {_describe_os_error(error)})",
|
|
345
|
+
) from None
|
|
346
|
+
opened = os.fstat(descriptor)
|
|
347
|
+
_require_authenticated_snapshot_stat(
|
|
348
|
+
opened,
|
|
349
|
+
root_info=root_info,
|
|
350
|
+
expected_size=reference.size_bytes,
|
|
351
|
+
)
|
|
352
|
+
try:
|
|
353
|
+
named = os.stat(expected_name, dir_fd=root_fd, follow_symlinks=False)
|
|
354
|
+
except OSError as error:
|
|
355
|
+
raise SourceContractError(
|
|
356
|
+
"ACQUISITION_SNAPSHOT",
|
|
357
|
+
"output.receipt.snapshot.storage_name",
|
|
358
|
+
"receipt-addressed snapshot changed during authentication"
|
|
359
|
+
f" (before the read: {_describe_os_error(error)})",
|
|
360
|
+
) from None
|
|
361
|
+
_require_stable_snapshot_identity(opened, named, observed_between=_OPEN_VERSUS_NAME)
|
|
362
|
+
raw = bytearray()
|
|
363
|
+
while len(raw) <= reference.size_bytes:
|
|
364
|
+
chunk = os.read(
|
|
365
|
+
descriptor,
|
|
366
|
+
min(65_536, reference.size_bytes + 1 - len(raw)),
|
|
367
|
+
)
|
|
368
|
+
if not chunk:
|
|
369
|
+
break
|
|
370
|
+
raw.extend(chunk)
|
|
371
|
+
_require_stable_snapshot_identity(
|
|
372
|
+
opened,
|
|
373
|
+
os.fstat(descriptor),
|
|
374
|
+
observed_between=_OPEN_VERSUS_READ,
|
|
375
|
+
)
|
|
376
|
+
os.close(descriptor)
|
|
377
|
+
descriptor = None
|
|
378
|
+
try:
|
|
379
|
+
after = os.stat(expected_name, dir_fd=root_fd, follow_symlinks=False)
|
|
380
|
+
except OSError as error:
|
|
381
|
+
raise SourceContractError(
|
|
382
|
+
"ACQUISITION_SNAPSHOT",
|
|
383
|
+
"output.receipt.snapshot.storage_name",
|
|
384
|
+
"receipt-addressed snapshot changed during authentication"
|
|
385
|
+
f" (after the read: {_describe_os_error(error)})",
|
|
386
|
+
) from None
|
|
387
|
+
_require_stable_snapshot_identity(named, after, observed_between=_NAME_VERSUS_READ)
|
|
388
|
+
finally:
|
|
389
|
+
if descriptor is not None:
|
|
390
|
+
os.close(descriptor)
|
|
391
|
+
os.close(root_fd)
|
|
392
|
+
content = bytes(raw)
|
|
393
|
+
if (
|
|
394
|
+
len(content) != reference.size_bytes
|
|
395
|
+
or hashlib.sha256(content).hexdigest() != reference.content_sha256
|
|
396
|
+
):
|
|
397
|
+
raise SourceContractError(
|
|
398
|
+
"ACQUISITION_SNAPSHOT",
|
|
399
|
+
"output.receipt.snapshot",
|
|
400
|
+
"authenticated bytes do not match the exact acquisition receipt",
|
|
401
|
+
)
|
|
402
|
+
return trusted_path, content
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _describe_os_error(error: OSError) -> str:
|
|
406
|
+
"""Name an errno without putting a path or any other caller string in the message."""
|
|
407
|
+
|
|
408
|
+
return f"{errno.errorcode.get(error.errno or 0, 'errno')} {error.strerror or ''}".strip()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _require_authenticated_snapshot_stat(
|
|
412
|
+
info: os.stat_result,
|
|
413
|
+
*,
|
|
414
|
+
root_info: os.stat_result,
|
|
415
|
+
expected_size: int,
|
|
416
|
+
) -> None:
|
|
417
|
+
"""Refuse an entry that is not one immutable, singly-linked, root-owned regular file.
|
|
418
|
+
|
|
419
|
+
Every field is evaluated rather than short-circuited so the refusal can name each one that
|
|
420
|
+
failed with its observed and its required value. This is the reader's half of the check
|
|
421
|
+
``acquisition/http.py`` runs when it seals the snapshot, and it carried the same defect that
|
|
422
|
+
half did before #229: six distinct causes sharing one sentence, so an ``ACQUISITION_SNAPSHOT``
|
|
423
|
+
read off a machine nobody can attach a debugger to said only that the entry was unsafe.
|
|
424
|
+
``st_nlink`` is the one a concurrent same-digest sealer can move, which is exactly why #209's
|
|
425
|
+
first diagnosis named the wrong gate.
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
faults: list[str] = []
|
|
429
|
+
if not stat.S_ISREG(info.st_mode):
|
|
430
|
+
faults.append(f"kind {stat.S_IFMT(info.st_mode):#o} is not a regular file")
|
|
431
|
+
if stat.S_IMODE(info.st_mode) != 0o444:
|
|
432
|
+
faults.append(f"st_mode {stat.S_IMODE(info.st_mode):#o} != 0o444")
|
|
433
|
+
if info.st_nlink != 1:
|
|
434
|
+
faults.append(f"st_nlink {info.st_nlink} != 1")
|
|
435
|
+
if info.st_size != expected_size:
|
|
436
|
+
faults.append(f"st_size {info.st_size} != {expected_size}")
|
|
437
|
+
if info.st_uid != root_info.st_uid:
|
|
438
|
+
faults.append(f"st_uid {info.st_uid} != {root_info.st_uid}")
|
|
439
|
+
if info.st_gid != root_info.st_gid:
|
|
440
|
+
faults.append(f"st_gid {info.st_gid} != {root_info.st_gid}")
|
|
441
|
+
if faults:
|
|
442
|
+
raise SourceContractError(
|
|
443
|
+
"ACQUISITION_SNAPSHOT",
|
|
444
|
+
"output.receipt.snapshot.storage_name",
|
|
445
|
+
"snapshot is not a root-owned single-link mode-0444 regular file of the exact size"
|
|
446
|
+
f" ({', '.join(faults)})",
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# The three points at which the bound stat signature is re-observed. They are named in the
|
|
451
|
+
# refusal so a field report says which observation drifted rather than only that one did.
|
|
452
|
+
_OPEN_VERSUS_NAME = "the opened descriptor and the named entry"
|
|
453
|
+
_OPEN_VERSUS_READ = "the opened descriptor before and after the read"
|
|
454
|
+
_NAME_VERSUS_READ = "the named entry before and after the read"
|
|
455
|
+
|
|
456
|
+
# The one drift with a single cause, said in words rather than left to be read off a field. Kept
|
|
457
|
+
# byte-identical to its counterpart in ``acquisition/http.py``: two copies of one guard that say
|
|
458
|
+
# different things about one field is the drift #237 was about.
|
|
459
|
+
_LAST_NAME_REMOVED = "; the last name for it was removed while it was open"
|
|
460
|
+
|
|
461
|
+
#: The anti-TOCTOU binding, held field by field so a refusal can name what moved. It is the same
|
|
462
|
+
#: set ``acquisition/http.py`` binds at seal time; ``tests/h3/test_integration.py`` holds the two
|
|
463
|
+
#: to each other so neither can quietly narrow.
|
|
464
|
+
_SNAPSHOT_STAT_FIELDS = (
|
|
465
|
+
"st_dev",
|
|
466
|
+
"st_ino",
|
|
467
|
+
"st_uid",
|
|
468
|
+
"st_gid",
|
|
469
|
+
"st_mode",
|
|
470
|
+
"st_nlink",
|
|
471
|
+
"st_size",
|
|
472
|
+
"st_mtime_ns",
|
|
473
|
+
"st_ctime_ns",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _require_stable_snapshot_identity(
|
|
478
|
+
before: os.stat_result,
|
|
479
|
+
after: os.stat_result,
|
|
480
|
+
*,
|
|
481
|
+
observed_between: str,
|
|
482
|
+
) -> None:
|
|
483
|
+
"""Refuse a snapshot whose bound identity moved, naming where it moved and by what.
|
|
484
|
+
|
|
485
|
+
The signature is a deliberate anti-TOCTOU binding, so it stays exactly as wide as it was.
|
|
486
|
+
What changes is only what the refusal says: which of the three observation pairs disagreed,
|
|
487
|
+
and every field that differs with its old and its new value. ``st_nlink`` moving means
|
|
488
|
+
another name was attached to or detached from this inode -- an unlink of the published name
|
|
489
|
+
reads as ``st_nlink 1 -> 0`` -- ``st_ino`` or ``st_dev`` moving means the name was rebound to
|
|
490
|
+
a different file, and ``st_ctime_ns`` alone means metadata was touched. Those are different
|
|
491
|
+
incidents and a field recurrence has to be able to tell them apart.
|
|
492
|
+
|
|
493
|
+
``st_nlink`` reaching zero is the one of them with a single cause, so it is named rather than
|
|
494
|
+
left to be inferred: the last name for this inode was removed while this descriptor held it
|
|
495
|
+
open, which is a deletion of the snapshot rather than a change to it. The remover is not
|
|
496
|
+
thereby identified. Governed deletion takes the sealer's per-name lock, but this reader holds
|
|
497
|
+
no lock at all, so the harness's own retention sweep can be the remover here just as an
|
|
498
|
+
unowned process can -- the sealer's copy of this guard is the one whose refusal rules the
|
|
499
|
+
harness out, because the sealer reads inside the held name.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
moved = tuple(
|
|
503
|
+
f"{field} {getattr(before, field)} -> {getattr(after, field)}"
|
|
504
|
+
for field in _SNAPSHOT_STAT_FIELDS
|
|
505
|
+
if getattr(before, field) != getattr(after, field)
|
|
506
|
+
)
|
|
507
|
+
if moved:
|
|
508
|
+
raise SourceContractError(
|
|
509
|
+
"ACQUISITION_SNAPSHOT",
|
|
510
|
+
"output.receipt.snapshot.storage_name",
|
|
511
|
+
"snapshot changed during authentication"
|
|
512
|
+
f" (between {observed_between}: {', '.join(moved)})"
|
|
513
|
+
f"{_LAST_NAME_REMOVED if before.st_nlink and not after.st_nlink else ''}",
|
|
514
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""The single repository-wide rights-status disposition authority.
|
|
2
|
+
|
|
3
|
+
One rule decides what a rights status *means*: admit, escalate, or refuse. The fitness
|
|
4
|
+
adjudicator (:mod:`mostlyright.data_harness.sources.fitness`) and the source-catalog gate both
|
|
5
|
+
read this module, so the two cannot drift into disagreeing adjudicators.
|
|
6
|
+
|
|
7
|
+
This module deliberately imports almost nothing. It depends on ``SourceContractError`` and
|
|
8
|
+
``RIGHTS_STATUSES`` from :mod:`mostlyright.data_harness.sources.contracts` and on nothing else --
|
|
9
|
+
not ``fitness``, not ``local_contracts``, not anything under ``catalog``. That import poverty is
|
|
10
|
+
what lets the catalog gate consume the rule without dragging the tenant proposal layer in.
|
|
11
|
+
|
|
12
|
+
The mapping is exhaustive by assertion at import time. A rights status added to the vocabulary
|
|
13
|
+
without a disposition is an import-time failure, never a silent admit.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from mostlyright.data_harness.sources.contracts import RIGHTS_STATUSES, SourceContractError
|
|
19
|
+
|
|
20
|
+
RIGHTS_DISPOSITIONS: frozenset[str] = frozenset({"admit", "escalate", "refuse"})
|
|
21
|
+
|
|
22
|
+
_DISPOSITION_BY_STATUS: dict[str, str] = {
|
|
23
|
+
"approved": "admit",
|
|
24
|
+
"conditional": "admit",
|
|
25
|
+
"unclear": "escalate",
|
|
26
|
+
"prohibited": "refuse",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if set(_DISPOSITION_BY_STATUS) != set(RIGHTS_STATUSES): # pragma: no cover - import-time guard
|
|
30
|
+
_missing = sorted(set(RIGHTS_STATUSES) - set(_DISPOSITION_BY_STATUS))
|
|
31
|
+
_extra = sorted(set(_DISPOSITION_BY_STATUS) - set(RIGHTS_STATUSES))
|
|
32
|
+
raise SourceContractError(
|
|
33
|
+
"RIGHTS_DISPOSITION_COVERAGE",
|
|
34
|
+
"rights_rule",
|
|
35
|
+
f"every rights status requires a disposition; missing={_missing}, extra={_extra}",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if not set(_DISPOSITION_BY_STATUS.values()) <= RIGHTS_DISPOSITIONS: # pragma: no cover
|
|
39
|
+
raise SourceContractError(
|
|
40
|
+
"RIGHTS_DISPOSITION_VALUE",
|
|
41
|
+
"rights_rule",
|
|
42
|
+
f"dispositions must be drawn from {sorted(RIGHTS_DISPOSITIONS)}",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rights_disposition(status: str) -> str:
|
|
47
|
+
"""Return ``admit``, ``escalate``, or ``refuse`` for one rights status.
|
|
48
|
+
|
|
49
|
+
Raises ``SourceContractError`` with code ``RIGHTS_STATUS`` for anything outside
|
|
50
|
+
``RIGHTS_STATUSES``; an unknown status is never admitted.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
if not isinstance(status, str) or status not in _DISPOSITION_BY_STATUS:
|
|
54
|
+
raise SourceContractError(
|
|
55
|
+
"RIGHTS_STATUS",
|
|
56
|
+
"rights.status",
|
|
57
|
+
f"must be one of {sorted(RIGHTS_STATUSES)}",
|
|
58
|
+
)
|
|
59
|
+
return _DISPOSITION_BY_STATUS[status]
|