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,545 @@
|
|
|
1
|
+
"""``mr-data catalog-harvest``: stage one keyed provider sweep, or just prove the key boundary.
|
|
2
|
+
|
|
3
|
+
This is the first of the three explicit provider-scale mutations. It reaches exactly one
|
|
4
|
+
endpoint, authorises with a key it reads out of a file the caller names and never keeps, and
|
|
5
|
+
writes a private staging root plus one secret-free receipt. It publishes no catalog: authoring
|
|
6
|
+
and publication are separate commands with separate receipts, so a sweep that stopped part-way
|
|
7
|
+
cannot be mistaken for a generation.
|
|
8
|
+
|
|
9
|
+
``--preflight-only`` is the whole of the key boundary and nothing else. It opens the named file
|
|
10
|
+
through a retained no-follow parent chain, checks ownership, link count, mode, size and
|
|
11
|
+
before/after identity, and answers whether the file was accepted. It never prints the key, never
|
|
12
|
+
prints the path, and never makes a request.
|
|
13
|
+
|
|
14
|
+
The receipt is the resume journal. An invocation stopped by ``--max-pages-this-invocation``
|
|
15
|
+
installs a typed-incomplete receipt carrying the observation timestamp, the retained checkpoint
|
|
16
|
+
digest and this invocation's page count; the exact same argv with ``--resume`` inserted reads that
|
|
17
|
+
receipt back, continues from the retained cursor, and replaces it. The invocation cap itself is
|
|
18
|
+
recorded as invocation evidence and is deliberately not part of the durable semantic configuration
|
|
19
|
+
digest, so a resume may drop it while every API parameter and cumulative bound stays exact.
|
|
20
|
+
|
|
21
|
+
``--pace-in-process`` changes who waits out the provider's minimum request interval, never how
|
|
22
|
+
long that wait is. Without it, an invocation that has committed a page hands the remaining wait
|
|
23
|
+
back and stops, and something outside has to resume it; a paced sweep of any size is therefore a
|
|
24
|
+
sweep of one-page invocations, and each one re-authenticates the whole evidence tree it is
|
|
25
|
+
resuming into, so the cost of the sweep grows with the square of the records already staged.
|
|
26
|
+
With it, the same interval is waited out here and the walk continues, so that authentication is
|
|
27
|
+
paid once for the invocation. The page commit and the resume journal are untouched either way:
|
|
28
|
+
a paced sweep killed mid-wait resumes from exactly the checkpoint an unpaced one would leave.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import time
|
|
35
|
+
from collections.abc import Mapping
|
|
36
|
+
from datetime import UTC, datetime
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any
|
|
39
|
+
from urllib.parse import urlsplit
|
|
40
|
+
|
|
41
|
+
from mostlyright.data_harness.acquisition.http import (
|
|
42
|
+
DATAGOV_V4_AUTHENTICATION_KIND,
|
|
43
|
+
DatagovV4Authorization,
|
|
44
|
+
PinnedHttpsRetriever,
|
|
45
|
+
RetrievalLimits,
|
|
46
|
+
StdlibPinnedTransport,
|
|
47
|
+
)
|
|
48
|
+
from mostlyright.data_harness.acquisition.url_policy import EgressPolicy, SystemResolver
|
|
49
|
+
from mostlyright.data_harness.sources.catalog.bounded_io import (
|
|
50
|
+
BoundedReadFailure,
|
|
51
|
+
measure_bounded_tree,
|
|
52
|
+
)
|
|
53
|
+
from mostlyright.data_harness.sources.catalog.fill import (
|
|
54
|
+
DATAGOV_V4_SORT_ORDERS,
|
|
55
|
+
CatalogFillConfig,
|
|
56
|
+
CatalogFillLimits,
|
|
57
|
+
run_catalog_fill,
|
|
58
|
+
)
|
|
59
|
+
from mostlyright.data_harness.sources.catalog.generation_receipt import (
|
|
60
|
+
HARVEST_PREFLIGHT_SCHEMA,
|
|
61
|
+
HARVEST_RECEIPT_SCHEMA,
|
|
62
|
+
MAX_STAGE_RECEIPT_BYTES,
|
|
63
|
+
ReceiptInstallation,
|
|
64
|
+
install_receipt,
|
|
65
|
+
load_execution_coordinate,
|
|
66
|
+
publication_output_lock,
|
|
67
|
+
receipt_installation_lock,
|
|
68
|
+
require_output_root_identity,
|
|
69
|
+
retained_incomplete_receipt,
|
|
70
|
+
root_coordinate,
|
|
71
|
+
)
|
|
72
|
+
from mostlyright.data_harness.sources.catalog.harvest.datagov_v4 import DATAGOV_V4_ENDPOINT
|
|
73
|
+
from mostlyright.data_harness.sources.contracts import SourceContractError
|
|
74
|
+
from mostlyright.data_harness.ux.commands import CommandSpec
|
|
75
|
+
|
|
76
|
+
#: The one provider this command harvests, and the sort orders its cursor contract admits.
|
|
77
|
+
PROVIDERS = ("datagov_v4",)
|
|
78
|
+
SORT_ORDERS = DATAGOV_V4_SORT_ORDERS
|
|
79
|
+
|
|
80
|
+
HARVEST_INCOMPLETE_STATUS = "harvest_incomplete"
|
|
81
|
+
HARVEST_COMPLETE_STATUS = "harvest_complete"
|
|
82
|
+
|
|
83
|
+
#: Bounds this command owns rather than passes through, so a caller cannot state one twice.
|
|
84
|
+
MAX_DISK_SCAN_MEMBERS = 5_000_000
|
|
85
|
+
|
|
86
|
+
#: The flags the preflight form of this command forbids: it produces nothing and spends nothing.
|
|
87
|
+
_PREFLIGHT_FORBIDDEN = (
|
|
88
|
+
"output_root",
|
|
89
|
+
"receipt_file",
|
|
90
|
+
"execution_manifest",
|
|
91
|
+
"execution_commit",
|
|
92
|
+
"per_page",
|
|
93
|
+
"sort",
|
|
94
|
+
"max_requests",
|
|
95
|
+
"max_responses",
|
|
96
|
+
"max_pages",
|
|
97
|
+
"max_records",
|
|
98
|
+
"max_response_bytes",
|
|
99
|
+
"max_network_bytes",
|
|
100
|
+
"max_disk_bytes",
|
|
101
|
+
"max_wall_seconds",
|
|
102
|
+
"min_request_interval_ms",
|
|
103
|
+
"max_convergence_passes",
|
|
104
|
+
"max_conflicting_identifiers",
|
|
105
|
+
"max_pages_this_invocation",
|
|
106
|
+
"pace_in_process",
|
|
107
|
+
"resume",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
#: Everything a real harvest must state before it makes one request.
|
|
111
|
+
_REQUIRED = (
|
|
112
|
+
"output_root",
|
|
113
|
+
"receipt_file",
|
|
114
|
+
"execution_manifest",
|
|
115
|
+
"execution_commit",
|
|
116
|
+
"per_page",
|
|
117
|
+
"sort",
|
|
118
|
+
"max_requests",
|
|
119
|
+
"max_responses",
|
|
120
|
+
"max_pages",
|
|
121
|
+
"max_records",
|
|
122
|
+
"max_response_bytes",
|
|
123
|
+
"max_network_bytes",
|
|
124
|
+
"max_disk_bytes",
|
|
125
|
+
"max_wall_seconds",
|
|
126
|
+
"min_request_interval_ms",
|
|
127
|
+
"max_convergence_passes",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--provider",
|
|
134
|
+
choices=PROVIDERS,
|
|
135
|
+
required=True,
|
|
136
|
+
help="the closed keyed public catalog provider",
|
|
137
|
+
)
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--api-key-file",
|
|
140
|
+
type=Path,
|
|
141
|
+
required=True,
|
|
142
|
+
help="the path of a file you own holding one key line; it is read, never kept or printed",
|
|
143
|
+
)
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--preflight-only",
|
|
146
|
+
action="store_true",
|
|
147
|
+
help="check the key file boundary and answer; make no request and write nothing",
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--output-root", type=Path, help="the private staging root this sweep is written into"
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument("--receipt-file", type=Path, help="where this stage's receipt is installed")
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--execution-manifest", type=Path, help="the committed execution coordinate document"
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument("--execution-commit", help="the exact commit that coordinate was cut at")
|
|
157
|
+
parser.add_argument("--per-page", type=int, help="records requested per provider page")
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--sort", choices=SORT_ORDERS, help="the closed provider sort the cursor contract requires"
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--max-conflicting-identifiers",
|
|
163
|
+
type=int,
|
|
164
|
+
help=(
|
|
165
|
+
"how many identifiers the provider may publish twice with different content before "
|
|
166
|
+
"the sweep refuses to converge (default 0). Each one is still resolved "
|
|
167
|
+
"deterministically and the count is recorded in the receipt, so a catalogue can "
|
|
168
|
+
"never hide them"
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
parser.add_argument("--max-requests", type=int, help="maximum aggregate network requests")
|
|
172
|
+
parser.add_argument("--max-responses", type=int, help="maximum aggregate network responses")
|
|
173
|
+
parser.add_argument("--max-pages", type=int, help="maximum aggregate provider pages")
|
|
174
|
+
parser.add_argument("--max-records", type=int, help="maximum aggregate provider records")
|
|
175
|
+
parser.add_argument("--max-response-bytes", type=int, help="maximum bytes in one response")
|
|
176
|
+
parser.add_argument("--max-network-bytes", type=int, help="maximum bytes across every response")
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
"--max-disk-bytes", type=int, help="maximum bytes this sweep may leave in its staging root"
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument("--max-wall-seconds", type=int, help="maximum cumulative retrieval seconds")
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"--min-request-interval-ms",
|
|
183
|
+
type=int,
|
|
184
|
+
help="smallest gap between two requests, in milliseconds",
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--max-convergence-passes",
|
|
188
|
+
type=int,
|
|
189
|
+
help="maximum complete passes allowed to reach convergence",
|
|
190
|
+
)
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--max-pages-this-invocation",
|
|
193
|
+
type=int,
|
|
194
|
+
help="stop this process after this many newly committed pages and leave a resumable stop",
|
|
195
|
+
)
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"--pace-in-process",
|
|
198
|
+
action="store_true",
|
|
199
|
+
help="wait the request interval out here and keep going, instead of stopping at each page",
|
|
200
|
+
)
|
|
201
|
+
parser.add_argument(
|
|
202
|
+
"--resume",
|
|
203
|
+
action="store_true",
|
|
204
|
+
help="continue the sweep this receipt file was left incomplete at",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
209
|
+
if args.preflight_only:
|
|
210
|
+
return _preflight(args)
|
|
211
|
+
_require_full_form(args)
|
|
212
|
+
output_root = Path(args.output_root)
|
|
213
|
+
with publication_output_lock(
|
|
214
|
+
output_root,
|
|
215
|
+
label="output_root",
|
|
216
|
+
code_prefix="HARVEST",
|
|
217
|
+
refusal=SourceContractError,
|
|
218
|
+
) as output_descriptor:
|
|
219
|
+
with receipt_installation_lock(
|
|
220
|
+
args.receipt_file,
|
|
221
|
+
label="receipt_file",
|
|
222
|
+
code_prefix="HARVEST",
|
|
223
|
+
refusal=SourceContractError,
|
|
224
|
+
) as receipt_installation:
|
|
225
|
+
return _run_locked(
|
|
226
|
+
args,
|
|
227
|
+
output_root=output_root,
|
|
228
|
+
output_descriptor=output_descriptor,
|
|
229
|
+
receipt_installation=receipt_installation,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _run_locked(
|
|
234
|
+
args: argparse.Namespace,
|
|
235
|
+
*,
|
|
236
|
+
output_root: Path,
|
|
237
|
+
output_descriptor: int,
|
|
238
|
+
receipt_installation: ReceiptInstallation,
|
|
239
|
+
) -> dict[str, Any]:
|
|
240
|
+
execution = load_execution_coordinate(args.execution_manifest, args.execution_commit)
|
|
241
|
+
retained = None
|
|
242
|
+
if args.resume:
|
|
243
|
+
retained = retained_incomplete_receipt(
|
|
244
|
+
args.receipt_file,
|
|
245
|
+
maximum=MAX_STAGE_RECEIPT_BYTES,
|
|
246
|
+
label="receipt_file",
|
|
247
|
+
schema=HARVEST_RECEIPT_SCHEMA,
|
|
248
|
+
incomplete_status=HARVEST_INCOMPLETE_STATUS,
|
|
249
|
+
output_root=output_root,
|
|
250
|
+
code_prefix="HARVEST",
|
|
251
|
+
refusal=SourceContractError,
|
|
252
|
+
)
|
|
253
|
+
if retained is None:
|
|
254
|
+
raise SourceContractError(
|
|
255
|
+
"HARVEST_RESUME_RECEIPT",
|
|
256
|
+
"receipt_file",
|
|
257
|
+
"a resume continues the typed-incomplete receipt its interruption left behind",
|
|
258
|
+
)
|
|
259
|
+
if retained["execution"]["root_sha256"] != execution["root_sha256"]:
|
|
260
|
+
raise SourceContractError(
|
|
261
|
+
"HARVEST_RESUME_RECEIPT",
|
|
262
|
+
"execution_manifest",
|
|
263
|
+
"the retained sweep was harvested from another execution coordinate",
|
|
264
|
+
)
|
|
265
|
+
observed_at = _observed_at(retained)
|
|
266
|
+
config = _config(args, observed_at)
|
|
267
|
+
authorization = DatagovV4Authorization.from_key_file(args.api_key_file)
|
|
268
|
+
result = run_catalog_fill(
|
|
269
|
+
config=config,
|
|
270
|
+
staging_root=output_root,
|
|
271
|
+
retriever=_retriever(args),
|
|
272
|
+
expected_checkpoint_digest=None if retained is None else retained["checkpoint_sha256"],
|
|
273
|
+
max_pages_this_invocation=args.max_pages_this_invocation,
|
|
274
|
+
authorization=authorization,
|
|
275
|
+
staging_descriptor=output_descriptor,
|
|
276
|
+
pacing_sleep=time.sleep if args.pace_in_process else None,
|
|
277
|
+
)
|
|
278
|
+
del authorization
|
|
279
|
+
disk_bytes = _disk_bytes(output_descriptor, maximum_bytes=args.max_disk_bytes)
|
|
280
|
+
receipt = result.receipt.to_dict()
|
|
281
|
+
complete = result.status == "complete"
|
|
282
|
+
previous_pages = 0 if retained is None else retained["invocation"]["cumulative_pages"]
|
|
283
|
+
payload: dict[str, Any] = {
|
|
284
|
+
"schema_version": HARVEST_RECEIPT_SCHEMA,
|
|
285
|
+
"status": HARVEST_COMPLETE_STATUS if complete else HARVEST_INCOMPLETE_STATUS,
|
|
286
|
+
"reason_code": result.reason_code,
|
|
287
|
+
"provider": args.provider,
|
|
288
|
+
"endpoint": config.endpoint,
|
|
289
|
+
"harvester_coordinate": config.coordinate,
|
|
290
|
+
"authentication_kind": DATAGOV_V4_AUTHENTICATION_KIND,
|
|
291
|
+
"observed_at": observed_at,
|
|
292
|
+
"checkpoint_sha256": result.checkpoint_sha256,
|
|
293
|
+
"config_sha256": receipt["config_sha256"],
|
|
294
|
+
"staging_sha256": result.eligible_staging_sha256,
|
|
295
|
+
"output_root_sha256": root_coordinate(output_root),
|
|
296
|
+
"output_bytes": disk_bytes,
|
|
297
|
+
"fill_receipt": receipt,
|
|
298
|
+
"fill_receipt_sha256": result.receipt.digest,
|
|
299
|
+
"measures": {
|
|
300
|
+
"requests": receipt["requests"],
|
|
301
|
+
"responses": receipt["responses"],
|
|
302
|
+
"pages": receipt["pages"],
|
|
303
|
+
"provider_records": receipt["unique_records"],
|
|
304
|
+
},
|
|
305
|
+
"sort": config.sort,
|
|
306
|
+
"conflicting_identifiers": receipt.get("conflicting_identifiers"),
|
|
307
|
+
"invocation": {
|
|
308
|
+
"max_pages_this_invocation": args.max_pages_this_invocation,
|
|
309
|
+
"paced_in_process": bool(args.pace_in_process),
|
|
310
|
+
"pages_this_invocation": receipt["pages"] - previous_pages,
|
|
311
|
+
"cumulative_pages": receipt["pages"],
|
|
312
|
+
"resumed": bool(args.resume),
|
|
313
|
+
},
|
|
314
|
+
"execution": execution,
|
|
315
|
+
"limits": _limits_evidence(args),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
def commit_guard() -> None:
|
|
319
|
+
require_output_root_identity(
|
|
320
|
+
output_root,
|
|
321
|
+
output_descriptor,
|
|
322
|
+
label="output_root",
|
|
323
|
+
code_prefix="HARVEST",
|
|
324
|
+
refusal=SourceContractError,
|
|
325
|
+
)
|
|
326
|
+
if _disk_bytes(output_descriptor, maximum_bytes=args.max_disk_bytes) != disk_bytes:
|
|
327
|
+
raise SourceContractError(
|
|
328
|
+
"HARVEST_DISK_LIMIT",
|
|
329
|
+
"output_root",
|
|
330
|
+
"the retained staging tree changed while its receipt was installed",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
install_receipt(
|
|
334
|
+
args.receipt_file,
|
|
335
|
+
payload,
|
|
336
|
+
maximum=MAX_STAGE_RECEIPT_BYTES,
|
|
337
|
+
label="receipt_file",
|
|
338
|
+
code_prefix="HARVEST",
|
|
339
|
+
refusal=SourceContractError,
|
|
340
|
+
replaces=retained,
|
|
341
|
+
installation=receipt_installation,
|
|
342
|
+
commit_guard=commit_guard,
|
|
343
|
+
)
|
|
344
|
+
return payload
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------------------------------------------
|
|
348
|
+
# The key-file boundary
|
|
349
|
+
# ------------------------------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _require_preflight_form(args: argparse.Namespace) -> None:
|
|
353
|
+
"""Refuse a preflight that states an argument the preflight form does not take.
|
|
354
|
+
|
|
355
|
+
Separated from :func:`_preflight` so the refusal is reachable without reading the key file
|
|
356
|
+
or reaching a provider: a spec that cannot run must be refusable by a deployment check.
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
stated = [name for name in _PREFLIGHT_FORBIDDEN if _stated(args, name)]
|
|
360
|
+
if stated:
|
|
361
|
+
raise SourceContractError(
|
|
362
|
+
"HARVEST_ARGUMENTS",
|
|
363
|
+
stated[0],
|
|
364
|
+
"the preflight form checks the key boundary and takes no output, execution or budget "
|
|
365
|
+
f"argument, and this one states {', '.join(sorted(stated))}",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def validate(args: argparse.Namespace) -> None:
|
|
370
|
+
"""Refuse an argument set this command would refuse, without running it.
|
|
371
|
+
|
|
372
|
+
``run`` calls the same two refusals on the same two branches, so a namespace this accepts is
|
|
373
|
+
one ``run`` reaches its first side effect with, and a namespace this refuses is one ``run``
|
|
374
|
+
refuses identically. That equivalence is the whole point: it lets ``mr-data-catalog-job
|
|
375
|
+
--dry-run`` answer "would this spec run?" rather than only "does argparse accept it?".
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
if args.preflight_only:
|
|
379
|
+
_require_preflight_form(args)
|
|
380
|
+
return
|
|
381
|
+
_require_full_form(args)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _preflight(args: argparse.Namespace) -> dict[str, Any]:
|
|
385
|
+
_require_preflight_form(args)
|
|
386
|
+
authorization = DatagovV4Authorization.from_key_file(args.api_key_file)
|
|
387
|
+
del authorization
|
|
388
|
+
return {
|
|
389
|
+
"schema_version": HARVEST_PREFLIGHT_SCHEMA,
|
|
390
|
+
"status": "harvest_preflight_accepted",
|
|
391
|
+
"provider": args.provider,
|
|
392
|
+
"endpoint": DATAGOV_V4_ENDPOINT,
|
|
393
|
+
"authentication_kind": DATAGOV_V4_AUTHENTICATION_KIND,
|
|
394
|
+
"key_file_boundary": "accepted",
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _stated(args: argparse.Namespace, name: str) -> bool:
|
|
399
|
+
# Identity, not equality. `0 == False` in Python, so `value not in (None, False)` reported
|
|
400
|
+
# `--per-page 0` and `--min-request-interval-ms 0` as unstated and let them through the
|
|
401
|
+
# preflight refusal they are forbidden by.
|
|
402
|
+
value = getattr(args, name, None)
|
|
403
|
+
return value is not None and value is not False
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _require_full_form(args: argparse.Namespace) -> None:
|
|
407
|
+
missing = [name for name in _REQUIRED if getattr(args, name, None) is None]
|
|
408
|
+
if missing:
|
|
409
|
+
raise SourceContractError(
|
|
410
|
+
"HARVEST_ARGUMENTS",
|
|
411
|
+
missing[0],
|
|
412
|
+
"a harvest states its output, its execution coordinate and every budget before it "
|
|
413
|
+
"makes one request",
|
|
414
|
+
)
|
|
415
|
+
for name in (
|
|
416
|
+
"per_page",
|
|
417
|
+
"max_requests",
|
|
418
|
+
"max_responses",
|
|
419
|
+
"max_pages",
|
|
420
|
+
"max_records",
|
|
421
|
+
"max_response_bytes",
|
|
422
|
+
"max_network_bytes",
|
|
423
|
+
"max_disk_bytes",
|
|
424
|
+
"max_wall_seconds",
|
|
425
|
+
"max_convergence_passes",
|
|
426
|
+
):
|
|
427
|
+
value = getattr(args, name)
|
|
428
|
+
if type(value) is not int or value < 1:
|
|
429
|
+
raise SourceContractError(
|
|
430
|
+
"HARVEST_ARGUMENTS", name, "must be a positive canonical decimal integer"
|
|
431
|
+
)
|
|
432
|
+
if type(args.min_request_interval_ms) is not int or args.min_request_interval_ms < 0:
|
|
433
|
+
raise SourceContractError(
|
|
434
|
+
"HARVEST_ARGUMENTS", "min_request_interval_ms", "must be a non-negative integer"
|
|
435
|
+
)
|
|
436
|
+
if args.max_pages_this_invocation is not None and (
|
|
437
|
+
type(args.max_pages_this_invocation) is not int or args.max_pages_this_invocation < 1
|
|
438
|
+
):
|
|
439
|
+
raise SourceContractError(
|
|
440
|
+
"HARVEST_ARGUMENTS", "max_pages_this_invocation", "must be a positive integer"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ------------------------------------------------------------------------------------------------
|
|
445
|
+
# Configuration, transport, and the two things this command measures for itself
|
|
446
|
+
# ------------------------------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _config(args: argparse.Namespace, observed_at: str) -> CatalogFillConfig:
|
|
450
|
+
return CatalogFillConfig(
|
|
451
|
+
protocol=args.provider,
|
|
452
|
+
endpoint=DATAGOV_V4_ENDPOINT,
|
|
453
|
+
observed_at=observed_at,
|
|
454
|
+
page_size=args.per_page,
|
|
455
|
+
max_convergence_passes=args.max_convergence_passes,
|
|
456
|
+
sort=args.sort,
|
|
457
|
+
max_conflicting_identifiers=args.max_conflicting_identifiers or 0,
|
|
458
|
+
authentication_kind=DATAGOV_V4_AUTHENTICATION_KIND,
|
|
459
|
+
minimum_request_interval_ns=args.min_request_interval_ms * 1_000_000,
|
|
460
|
+
limits=CatalogFillLimits(
|
|
461
|
+
max_requests=args.max_requests,
|
|
462
|
+
max_pages=args.max_pages,
|
|
463
|
+
max_records=args.max_records,
|
|
464
|
+
max_response_bytes=args.max_response_bytes,
|
|
465
|
+
max_aggregate_bytes=args.max_network_bytes,
|
|
466
|
+
max_responses=args.max_responses,
|
|
467
|
+
max_wall_time_ns=args.max_wall_seconds * 1_000_000_000,
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _retriever(args: argparse.Namespace) -> Any:
|
|
473
|
+
"""The one pinned transport this command reaches the provider through."""
|
|
474
|
+
|
|
475
|
+
hostname = urlsplit(DATAGOV_V4_ENDPOINT).hostname
|
|
476
|
+
assert hostname is not None
|
|
477
|
+
return PinnedHttpsRetriever(
|
|
478
|
+
resolver=SystemResolver(),
|
|
479
|
+
egress_policy=EgressPolicy(allowed_hostnames=(hostname,)),
|
|
480
|
+
transport=StdlibPinnedTransport(),
|
|
481
|
+
limits=RetrievalLimits(
|
|
482
|
+
max_response_bytes=args.max_response_bytes,
|
|
483
|
+
max_aggregate_response_bytes=min(args.max_network_bytes, 1 << 40),
|
|
484
|
+
max_redirects=0,
|
|
485
|
+
max_requests=16,
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _observed_at(retained: Mapping[str, Any] | None) -> str:
|
|
491
|
+
"""The sweep's observation timestamp: minted once, then carried by the receipt it left."""
|
|
492
|
+
|
|
493
|
+
if retained is not None:
|
|
494
|
+
return str(retained["observed_at"])
|
|
495
|
+
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _disk_bytes(root_descriptor: int, *, maximum_bytes: int) -> int:
|
|
499
|
+
"""Bytes held by one retained tree, measured incrementally and fail-closed."""
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
total, _members = measure_bounded_tree(
|
|
503
|
+
root_descriptor,
|
|
504
|
+
maximum_members=MAX_DISK_SCAN_MEMBERS,
|
|
505
|
+
maximum_bytes=maximum_bytes,
|
|
506
|
+
)
|
|
507
|
+
return total
|
|
508
|
+
except BoundedReadFailure as error:
|
|
509
|
+
raise SourceContractError(
|
|
510
|
+
"HARVEST_DISK_LIMIT",
|
|
511
|
+
"output_root",
|
|
512
|
+
"the retained staging tree is unsafe, changed, or exceeds its stated bounds",
|
|
513
|
+
) from error
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _limits_evidence(args: argparse.Namespace) -> dict[str, int]:
|
|
517
|
+
return {
|
|
518
|
+
"per_page": args.per_page,
|
|
519
|
+
"max_requests": args.max_requests,
|
|
520
|
+
"max_responses": args.max_responses,
|
|
521
|
+
"max_pages": args.max_pages,
|
|
522
|
+
"max_records": args.max_records,
|
|
523
|
+
"max_response_bytes": args.max_response_bytes,
|
|
524
|
+
"max_network_bytes": args.max_network_bytes,
|
|
525
|
+
"max_disk_bytes": args.max_disk_bytes,
|
|
526
|
+
"max_wall_seconds": args.max_wall_seconds,
|
|
527
|
+
"min_request_interval_ms": args.min_request_interval_ms,
|
|
528
|
+
"max_convergence_passes": args.max_convergence_passes,
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _exit_code(payload: Mapping[str, Any]) -> int:
|
|
533
|
+
return (
|
|
534
|
+
0 if payload.get("status") in {HARVEST_COMPLETE_STATUS, "harvest_preflight_accepted"} else 1
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
SPEC = CommandSpec(
|
|
539
|
+
name="catalog-harvest",
|
|
540
|
+
help="stage one keyed public catalog sweep, or just check the key file boundary",
|
|
541
|
+
add_arguments=add_arguments,
|
|
542
|
+
run=run,
|
|
543
|
+
exit_code=_exit_code,
|
|
544
|
+
validate=validate,
|
|
545
|
+
)
|