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,474 @@
|
|
|
1
|
+
"""Descriptor-rooted, bounded capture of one private Build-index snapshot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from mostlyright.data_harness.canonical import (
|
|
13
|
+
CanonicalJSONError,
|
|
14
|
+
parse_canonical_json,
|
|
15
|
+
sha256_bytes,
|
|
16
|
+
)
|
|
17
|
+
from mostlyright.data_harness.local_search.contracts import (
|
|
18
|
+
MAX_ENTRIES,
|
|
19
|
+
BuildDiagnosticDocument,
|
|
20
|
+
BuildIndexEntry,
|
|
21
|
+
BuildIndexManifest,
|
|
22
|
+
BuildLocatorDocument,
|
|
23
|
+
LocalSearchContractError,
|
|
24
|
+
SnapshotMember,
|
|
25
|
+
build_diagnostic_document_from_dict,
|
|
26
|
+
build_index_entry_from_dict,
|
|
27
|
+
build_index_manifest_from_dict,
|
|
28
|
+
build_locator_document_from_dict,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_OPEN_DIRECTORY = os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_CLOEXEC", 0)
|
|
32
|
+
_OPEN_MEMBER = (
|
|
33
|
+
os.O_RDONLY
|
|
34
|
+
| getattr(os, "O_CLOEXEC", 0)
|
|
35
|
+
| getattr(os, "O_NOFOLLOW", 0)
|
|
36
|
+
| getattr(os, "O_NONBLOCK", 0)
|
|
37
|
+
)
|
|
38
|
+
_NOFOLLOW = getattr(os, "O_NOFOLLOW", 0)
|
|
39
|
+
_READ_CHUNK = 1024 * 1024
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SnapshotLimits:
|
|
44
|
+
"""Closed capture budgets applied before unbounded allocation."""
|
|
45
|
+
|
|
46
|
+
max_member_bytes: int = 16 * 1024 * 1024
|
|
47
|
+
max_total_bytes: int = 256 * 1024 * 1024
|
|
48
|
+
max_members: int = MAX_ENTRIES + 2
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
for name, value, maximum in (
|
|
52
|
+
("max_member_bytes", self.max_member_bytes, 256 * 1024 * 1024),
|
|
53
|
+
("max_total_bytes", self.max_total_bytes, 1024 * 1024 * 1024),
|
|
54
|
+
("max_members", self.max_members, MAX_ENTRIES + 2),
|
|
55
|
+
):
|
|
56
|
+
if type(value) is not int or not 1 <= value <= maximum:
|
|
57
|
+
raise LocalSearchContractError(
|
|
58
|
+
"LOCAL_SEARCH_SNAPSHOT_LIMIT",
|
|
59
|
+
f"limits.{name}",
|
|
60
|
+
f"must be an integer in [1, {maximum}]",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class CapturedBuildIndexSnapshot:
|
|
66
|
+
"""A fully verified private index plus the exact bytes that produced it."""
|
|
67
|
+
|
|
68
|
+
manifest: BuildIndexManifest
|
|
69
|
+
manifest_sha256: str
|
|
70
|
+
entries: tuple[BuildIndexEntry, ...]
|
|
71
|
+
locators: BuildLocatorDocument
|
|
72
|
+
diagnostics: BuildDiagnosticDocument
|
|
73
|
+
manifest_bytes: bytes
|
|
74
|
+
member_bytes: tuple[tuple[str, bytes], ...]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def capture_build_index_snapshot(
|
|
78
|
+
directory: Path,
|
|
79
|
+
*,
|
|
80
|
+
expected_manifest_sha256: str,
|
|
81
|
+
limits: SnapshotLimits | None = None,
|
|
82
|
+
) -> CapturedBuildIndexSnapshot:
|
|
83
|
+
"""Capture and verify a private index from one retained root identity.
|
|
84
|
+
|
|
85
|
+
``expected_manifest_sha256`` is deliberately mandatory: the live directory cannot nominate
|
|
86
|
+
its own authority coordinate merely by replacing all of its files consistently.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
_expected_digest(expected_manifest_sha256)
|
|
90
|
+
if not isinstance(directory, Path):
|
|
91
|
+
raise LocalSearchContractError("LOCAL_SEARCH_TYPE", "directory", "must be a pathlib.Path")
|
|
92
|
+
selected_limits = limits or SnapshotLimits()
|
|
93
|
+
if not isinstance(selected_limits, SnapshotLimits):
|
|
94
|
+
raise LocalSearchContractError("LOCAL_SEARCH_TYPE", "limits", "must be SnapshotLimits")
|
|
95
|
+
|
|
96
|
+
root_fd = _open_root(directory)
|
|
97
|
+
try:
|
|
98
|
+
root_identity = _directory_identity(root_fd, "root")
|
|
99
|
+
root_names_before = _directory_names(root_fd, "root", maximum=5)
|
|
100
|
+
expected_root_names = {"manifest.json", "entries", "locators.json", "diagnostics.json"}
|
|
101
|
+
_exact_membership(root_names_before, expected_root_names, "root")
|
|
102
|
+
|
|
103
|
+
manifest_bytes = _read_plain_file(
|
|
104
|
+
root_fd,
|
|
105
|
+
"manifest.json",
|
|
106
|
+
maximum=min(selected_limits.max_member_bytes, selected_limits.max_total_bytes),
|
|
107
|
+
)
|
|
108
|
+
manifest_sha256 = sha256_bytes(manifest_bytes)
|
|
109
|
+
if manifest_sha256 != expected_manifest_sha256:
|
|
110
|
+
raise LocalSearchContractError(
|
|
111
|
+
"LOCAL_SEARCH_MANIFEST_MISMATCH",
|
|
112
|
+
"manifest.json",
|
|
113
|
+
"does not match the caller's expected manifest digest",
|
|
114
|
+
)
|
|
115
|
+
manifest = build_index_manifest_from_dict(_canonical_mapping(manifest_bytes, "manifest"))
|
|
116
|
+
if len(manifest.members) > selected_limits.max_members:
|
|
117
|
+
raise LocalSearchContractError(
|
|
118
|
+
"LOCAL_SEARCH_SNAPSHOT_LIMIT",
|
|
119
|
+
"manifest.entries",
|
|
120
|
+
"snapshot exceeds the member-count limit",
|
|
121
|
+
)
|
|
122
|
+
if manifest.total_bytes + len(manifest_bytes) > selected_limits.max_total_bytes:
|
|
123
|
+
raise LocalSearchContractError(
|
|
124
|
+
"LOCAL_SEARCH_SNAPSHOT_LIMIT",
|
|
125
|
+
"manifest.total_bytes",
|
|
126
|
+
"snapshot exceeds the aggregate byte limit",
|
|
127
|
+
)
|
|
128
|
+
for member in manifest.members:
|
|
129
|
+
if member.bytes > selected_limits.max_member_bytes:
|
|
130
|
+
raise LocalSearchContractError(
|
|
131
|
+
"LOCAL_SEARCH_SNAPSHOT_LIMIT",
|
|
132
|
+
member.path,
|
|
133
|
+
"member exceeds the byte limit",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
entries_fd = _open_child_directory(root_fd, "entries")
|
|
137
|
+
try:
|
|
138
|
+
entries_identity = _directory_identity(entries_fd, "entries")
|
|
139
|
+
expected_entry_names = {
|
|
140
|
+
member.path.removeprefix("entries/") for member in manifest.entries
|
|
141
|
+
}
|
|
142
|
+
_exact_membership(
|
|
143
|
+
_directory_names(
|
|
144
|
+
entries_fd,
|
|
145
|
+
"entries",
|
|
146
|
+
maximum=selected_limits.max_members + 1,
|
|
147
|
+
),
|
|
148
|
+
expected_entry_names,
|
|
149
|
+
"entries",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
captured_members: list[tuple[str, bytes]] = []
|
|
153
|
+
parsed_entries: list[BuildIndexEntry] = []
|
|
154
|
+
for member in manifest.entries:
|
|
155
|
+
name = member.path.removeprefix("entries/")
|
|
156
|
+
raw = _capture_bound_member(entries_fd, name, member, selected_limits)
|
|
157
|
+
entry = build_index_entry_from_dict(_canonical_mapping(raw, member.path))
|
|
158
|
+
if entry.facts_sha256 != member.sha256 or name != f"{entry.facts_sha256}.json":
|
|
159
|
+
raise LocalSearchContractError(
|
|
160
|
+
"LOCAL_SEARCH_ENTRY_BINDING",
|
|
161
|
+
member.path,
|
|
162
|
+
"filename and member digest must equal the entry facts digest",
|
|
163
|
+
)
|
|
164
|
+
parsed_entries.append(entry)
|
|
165
|
+
captured_members.append((member.path, raw))
|
|
166
|
+
|
|
167
|
+
_validate_directory(entries_fd, entries_identity, "entries")
|
|
168
|
+
_validate_named_directory(root_fd, "entries", entries_identity)
|
|
169
|
+
_exact_membership(
|
|
170
|
+
_directory_names(
|
|
171
|
+
entries_fd,
|
|
172
|
+
"entries",
|
|
173
|
+
maximum=selected_limits.max_members + 1,
|
|
174
|
+
),
|
|
175
|
+
expected_entry_names,
|
|
176
|
+
"entries",
|
|
177
|
+
)
|
|
178
|
+
finally:
|
|
179
|
+
os.close(entries_fd)
|
|
180
|
+
|
|
181
|
+
locator_bytes = _capture_bound_member(
|
|
182
|
+
root_fd, "locators.json", manifest.locators, selected_limits
|
|
183
|
+
)
|
|
184
|
+
diagnostic_bytes = _capture_bound_member(
|
|
185
|
+
root_fd, "diagnostics.json", manifest.diagnostics, selected_limits
|
|
186
|
+
)
|
|
187
|
+
locators = build_locator_document_from_dict(
|
|
188
|
+
_canonical_mapping(locator_bytes, "locators.json")
|
|
189
|
+
)
|
|
190
|
+
diagnostics = build_diagnostic_document_from_dict(
|
|
191
|
+
_canonical_mapping(diagnostic_bytes, "diagnostics.json")
|
|
192
|
+
)
|
|
193
|
+
if len(locators.locators) != manifest.locator_count:
|
|
194
|
+
raise LocalSearchContractError(
|
|
195
|
+
"LOCAL_SEARCH_MANIFEST",
|
|
196
|
+
"manifest.locator_count",
|
|
197
|
+
"does not match locators.json",
|
|
198
|
+
)
|
|
199
|
+
if len(diagnostics.diagnostics) != manifest.diagnostic_count:
|
|
200
|
+
raise LocalSearchContractError(
|
|
201
|
+
"LOCAL_SEARCH_MANIFEST",
|
|
202
|
+
"manifest.diagnostic_count",
|
|
203
|
+
"does not match diagnostics.json",
|
|
204
|
+
)
|
|
205
|
+
entry_digests = {entry.facts_sha256 for entry in parsed_entries}
|
|
206
|
+
dangling = sorted(
|
|
207
|
+
locator.facts_sha256
|
|
208
|
+
for locator in locators.locators
|
|
209
|
+
if locator.facts_sha256 not in entry_digests
|
|
210
|
+
)
|
|
211
|
+
if dangling:
|
|
212
|
+
raise LocalSearchContractError(
|
|
213
|
+
"LOCAL_SEARCH_LOCATOR_BINDING",
|
|
214
|
+
"locators.json",
|
|
215
|
+
"a locator names facts absent from this snapshot",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
captured_members.extend(
|
|
219
|
+
(("locators.json", locator_bytes), ("diagnostics.json", diagnostic_bytes))
|
|
220
|
+
)
|
|
221
|
+
_validate_directory(root_fd, root_identity, "root")
|
|
222
|
+
_exact_membership(_directory_names(root_fd, "root", maximum=5), expected_root_names, "root")
|
|
223
|
+
return CapturedBuildIndexSnapshot(
|
|
224
|
+
manifest=manifest,
|
|
225
|
+
manifest_sha256=manifest_sha256,
|
|
226
|
+
entries=tuple(parsed_entries),
|
|
227
|
+
locators=locators,
|
|
228
|
+
diagnostics=diagnostics,
|
|
229
|
+
manifest_bytes=manifest_bytes,
|
|
230
|
+
member_bytes=tuple(captured_members),
|
|
231
|
+
)
|
|
232
|
+
finally:
|
|
233
|
+
os.close(root_fd)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _open_root(directory: Path) -> int:
|
|
237
|
+
try:
|
|
238
|
+
descriptor = os.open(directory, _OPEN_DIRECTORY | _NOFOLLOW)
|
|
239
|
+
except OSError as error:
|
|
240
|
+
raise LocalSearchContractError(
|
|
241
|
+
"LOCAL_SEARCH_SNAPSHOT_ROOT",
|
|
242
|
+
"root",
|
|
243
|
+
"cannot open a retained no-follow directory",
|
|
244
|
+
) from error
|
|
245
|
+
try:
|
|
246
|
+
_directory_identity(descriptor, "root")
|
|
247
|
+
except BaseException:
|
|
248
|
+
os.close(descriptor)
|
|
249
|
+
raise
|
|
250
|
+
return descriptor
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _open_child_directory(parent_fd: int, name: str) -> int:
|
|
254
|
+
try:
|
|
255
|
+
named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
256
|
+
descriptor = os.open(name, _OPEN_DIRECTORY | _NOFOLLOW, dir_fd=parent_fd)
|
|
257
|
+
except OSError as error:
|
|
258
|
+
raise LocalSearchContractError(
|
|
259
|
+
"LOCAL_SEARCH_SNAPSHOT_MEMBER",
|
|
260
|
+
name,
|
|
261
|
+
"cannot open a retained no-follow directory",
|
|
262
|
+
) from error
|
|
263
|
+
try:
|
|
264
|
+
opened = os.fstat(descriptor)
|
|
265
|
+
if not stat.S_ISDIR(named.st_mode) or not stat.S_ISDIR(opened.st_mode):
|
|
266
|
+
raise LocalSearchContractError(
|
|
267
|
+
"LOCAL_SEARCH_SNAPSHOT_MEMBER", name, "must be a directory"
|
|
268
|
+
)
|
|
269
|
+
if _identity(named) != _identity(opened):
|
|
270
|
+
raise LocalSearchContractError(
|
|
271
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", name, "named and opened identities differ"
|
|
272
|
+
)
|
|
273
|
+
except BaseException:
|
|
274
|
+
os.close(descriptor)
|
|
275
|
+
raise
|
|
276
|
+
return descriptor
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _capture_bound_member(
|
|
280
|
+
parent_fd: int,
|
|
281
|
+
name: str,
|
|
282
|
+
expected: SnapshotMember,
|
|
283
|
+
limits: SnapshotLimits,
|
|
284
|
+
) -> bytes:
|
|
285
|
+
raw = _read_plain_file(parent_fd, name, maximum=limits.max_member_bytes)
|
|
286
|
+
if len(raw) != expected.bytes or sha256_bytes(raw) != expected.sha256:
|
|
287
|
+
raise LocalSearchContractError(
|
|
288
|
+
"LOCAL_SEARCH_SNAPSHOT_MEMBER",
|
|
289
|
+
expected.path,
|
|
290
|
+
"size or digest differs from the manifest",
|
|
291
|
+
)
|
|
292
|
+
return raw
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _read_plain_file(parent_fd: int, name: str, *, maximum: int) -> bytes:
|
|
296
|
+
"""Read one named file once, checking opened and named identity on both sides."""
|
|
297
|
+
|
|
298
|
+
descriptor = -1
|
|
299
|
+
try:
|
|
300
|
+
named_before = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
301
|
+
descriptor = os.open(name, _OPEN_MEMBER, dir_fd=parent_fd)
|
|
302
|
+
opened_before = os.fstat(descriptor)
|
|
303
|
+
except OSError as error:
|
|
304
|
+
if descriptor >= 0:
|
|
305
|
+
os.close(descriptor)
|
|
306
|
+
raise LocalSearchContractError(
|
|
307
|
+
"LOCAL_SEARCH_SNAPSHOT_MEMBER", name, "cannot open a plain no-follow member"
|
|
308
|
+
) from error
|
|
309
|
+
try:
|
|
310
|
+
if (
|
|
311
|
+
not stat.S_ISREG(named_before.st_mode)
|
|
312
|
+
or not stat.S_ISREG(opened_before.st_mode)
|
|
313
|
+
or named_before.st_nlink != 1
|
|
314
|
+
or opened_before.st_nlink != 1
|
|
315
|
+
):
|
|
316
|
+
raise LocalSearchContractError(
|
|
317
|
+
"LOCAL_SEARCH_SNAPSHOT_MEMBER",
|
|
318
|
+
name,
|
|
319
|
+
"must be a single-link regular file",
|
|
320
|
+
)
|
|
321
|
+
if _identity(named_before) != _identity(opened_before):
|
|
322
|
+
raise LocalSearchContractError(
|
|
323
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", name, "named and opened identities differ"
|
|
324
|
+
)
|
|
325
|
+
if opened_before.st_size > maximum:
|
|
326
|
+
raise LocalSearchContractError(
|
|
327
|
+
"LOCAL_SEARCH_SNAPSHOT_LIMIT", name, "member exceeds the byte limit"
|
|
328
|
+
)
|
|
329
|
+
chunks: list[bytes] = []
|
|
330
|
+
observed = 0
|
|
331
|
+
while True:
|
|
332
|
+
chunk = os.read(descriptor, min(_READ_CHUNK, maximum - observed + 1))
|
|
333
|
+
if not chunk:
|
|
334
|
+
break
|
|
335
|
+
observed += len(chunk)
|
|
336
|
+
if observed > maximum:
|
|
337
|
+
raise LocalSearchContractError(
|
|
338
|
+
"LOCAL_SEARCH_SNAPSHOT_LIMIT", name, "member exceeds the byte limit"
|
|
339
|
+
)
|
|
340
|
+
chunks.append(chunk)
|
|
341
|
+
opened_after = os.fstat(descriptor)
|
|
342
|
+
try:
|
|
343
|
+
named_after = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
344
|
+
except OSError as error:
|
|
345
|
+
raise LocalSearchContractError(
|
|
346
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", name, "named member disappeared during capture"
|
|
347
|
+
) from error
|
|
348
|
+
raw = b"".join(chunks)
|
|
349
|
+
if (
|
|
350
|
+
_identity(opened_before) != _identity(opened_after)
|
|
351
|
+
or _identity(opened_after) != _identity(named_after)
|
|
352
|
+
or opened_after.st_nlink != 1
|
|
353
|
+
or len(raw) != opened_after.st_size
|
|
354
|
+
):
|
|
355
|
+
raise LocalSearchContractError(
|
|
356
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", name, "member changed during capture"
|
|
357
|
+
)
|
|
358
|
+
return raw
|
|
359
|
+
finally:
|
|
360
|
+
os.close(descriptor)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _canonical_mapping(raw: bytes, path: str) -> Mapping[str, Any]:
|
|
364
|
+
try:
|
|
365
|
+
value = parse_canonical_json(raw)
|
|
366
|
+
except CanonicalJSONError as error:
|
|
367
|
+
raise LocalSearchContractError(
|
|
368
|
+
"LOCAL_SEARCH_CANONICAL", path, "member is not exact canonical JSON"
|
|
369
|
+
) from error
|
|
370
|
+
if not isinstance(value, Mapping):
|
|
371
|
+
raise LocalSearchContractError("LOCAL_SEARCH_TYPE", path, "must contain an object")
|
|
372
|
+
return value
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _expected_digest(value: Any) -> str:
|
|
376
|
+
if (
|
|
377
|
+
not isinstance(value, str)
|
|
378
|
+
or len(value) != 64
|
|
379
|
+
or any(char not in "0123456789abcdef" for char in value)
|
|
380
|
+
):
|
|
381
|
+
raise LocalSearchContractError(
|
|
382
|
+
"LOCAL_SEARCH_SHA256",
|
|
383
|
+
"expected_manifest_sha256",
|
|
384
|
+
"must be a lowercase 64-character SHA-256",
|
|
385
|
+
)
|
|
386
|
+
return value
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _directory_names(descriptor: int, path: str, *, maximum: int) -> set[str]:
|
|
390
|
+
"""Read at most ``maximum`` names, refusing before an attacker-sized allocation."""
|
|
391
|
+
|
|
392
|
+
names: set[str] = set()
|
|
393
|
+
try:
|
|
394
|
+
with os.scandir(descriptor) as entries:
|
|
395
|
+
for entry in entries:
|
|
396
|
+
if len(names) >= maximum:
|
|
397
|
+
raise LocalSearchContractError(
|
|
398
|
+
"LOCAL_SEARCH_SNAPSHOT_MEMBERSHIP",
|
|
399
|
+
path,
|
|
400
|
+
"directory contains more names than the bounded snapshot permits",
|
|
401
|
+
)
|
|
402
|
+
if entry.name in names:
|
|
403
|
+
raise LocalSearchContractError(
|
|
404
|
+
"LOCAL_SEARCH_SNAPSHOT_MEMBERSHIP",
|
|
405
|
+
path,
|
|
406
|
+
"directory contains a duplicate name",
|
|
407
|
+
)
|
|
408
|
+
names.add(entry.name)
|
|
409
|
+
except LocalSearchContractError:
|
|
410
|
+
raise
|
|
411
|
+
except OSError as error:
|
|
412
|
+
raise LocalSearchContractError(
|
|
413
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", path, "directory cannot be listed"
|
|
414
|
+
) from error
|
|
415
|
+
return names
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _exact_membership(actual: set[str], expected: set[str], path: str) -> None:
|
|
419
|
+
if actual != expected:
|
|
420
|
+
raise LocalSearchContractError(
|
|
421
|
+
"LOCAL_SEARCH_SNAPSHOT_MEMBERSHIP",
|
|
422
|
+
path,
|
|
423
|
+
"membership is not exact; "
|
|
424
|
+
f"missing_count={len(expected - actual)}, extra_count={len(actual - expected)}",
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _directory_identity(descriptor: int, path: str) -> tuple[int, int, int, int, int, int]:
|
|
429
|
+
try:
|
|
430
|
+
observed = os.fstat(descriptor)
|
|
431
|
+
except OSError as error:
|
|
432
|
+
raise LocalSearchContractError(
|
|
433
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", path, "directory identity cannot be read"
|
|
434
|
+
) from error
|
|
435
|
+
if not stat.S_ISDIR(observed.st_mode):
|
|
436
|
+
raise LocalSearchContractError("LOCAL_SEARCH_SNAPSHOT_MEMBER", path, "must be a directory")
|
|
437
|
+
return _identity(observed)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _validate_directory(
|
|
441
|
+
descriptor: int, expected: tuple[int, int, int, int, int, int], path: str
|
|
442
|
+
) -> None:
|
|
443
|
+
if _directory_identity(descriptor, path) != expected:
|
|
444
|
+
raise LocalSearchContractError(
|
|
445
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", path, "directory changed during capture"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _validate_named_directory(
|
|
450
|
+
parent_fd: int,
|
|
451
|
+
name: str,
|
|
452
|
+
expected: tuple[int, int, int, int, int, int],
|
|
453
|
+
) -> None:
|
|
454
|
+
try:
|
|
455
|
+
named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
|
|
456
|
+
except OSError as error:
|
|
457
|
+
raise LocalSearchContractError(
|
|
458
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", name, "directory disappeared during capture"
|
|
459
|
+
) from error
|
|
460
|
+
if not stat.S_ISDIR(named.st_mode) or _identity(named) != expected:
|
|
461
|
+
raise LocalSearchContractError(
|
|
462
|
+
"LOCAL_SEARCH_SNAPSHOT_RACE", name, "named directory changed during capture"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _identity(value: os.stat_result) -> tuple[int, int, int, int, int, int]:
|
|
467
|
+
return (
|
|
468
|
+
value.st_dev,
|
|
469
|
+
value.st_ino,
|
|
470
|
+
value.st_mode,
|
|
471
|
+
value.st_size,
|
|
472
|
+
value.st_mtime_ns,
|
|
473
|
+
value.st_ctime_ns,
|
|
474
|
+
)
|