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,423 @@
|
|
|
1
|
+
"""Trusted fixed public-source retrieval for the hosted crawler coordinator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
from mostlyright.data_harness.acquisition.http import (
|
|
10
|
+
AcquisitionLimiter,
|
|
11
|
+
ByteRange,
|
|
12
|
+
PinnedHttpsRetriever,
|
|
13
|
+
RetrievalHop,
|
|
14
|
+
RetrievalLimits,
|
|
15
|
+
RetrievedBytes,
|
|
16
|
+
StdlibPinnedTransport,
|
|
17
|
+
require_range_plan_within_budget,
|
|
18
|
+
)
|
|
19
|
+
from mostlyright.data_harness.acquisition.url_policy import (
|
|
20
|
+
AcquisitionSecurityError,
|
|
21
|
+
EgressPolicy,
|
|
22
|
+
SystemResolver,
|
|
23
|
+
canonical_public_hostname,
|
|
24
|
+
)
|
|
25
|
+
from mostlyright.data_harness.canonical import canonical_sha256, sha256_bytes
|
|
26
|
+
from mostlyright.data_harness.hosted_crawler import (
|
|
27
|
+
HostedCrawlerError,
|
|
28
|
+
_public_expected_media_types,
|
|
29
|
+
validate_crawler_request,
|
|
30
|
+
)
|
|
31
|
+
from mostlyright.data_harness.hosted_crawler_protocol import HostedCrawlerRequest
|
|
32
|
+
|
|
33
|
+
_OPENLIGADB_LIMITER = AcquisitionLimiter(
|
|
34
|
+
max_concurrency=2,
|
|
35
|
+
min_interval_seconds=1.0,
|
|
36
|
+
)
|
|
37
|
+
_PUBLIC_HTTPS_LIMITER = AcquisitionLimiter(max_concurrency=2, min_interval_seconds=1.0)
|
|
38
|
+
|
|
39
|
+
# The ranged bulk-fetch envelope for capacity-constrained public origins (EPA AirData is
|
|
40
|
+
# the motivating case: a 60-80 MB archive on a single-IP Apache origin that advertises
|
|
41
|
+
# ``Accept-Ranges: bytes`` and stalls whole-file streams mid-transfer under load). Every
|
|
42
|
+
# figure here is restated, in milliseconds, inside the ``ranged_fetch`` envelope of
|
|
43
|
+
# ``PUBLIC_HTTPS_EGRESS_POLICY`` in hosted_crawler_protocol.py, and a test holds the two
|
|
44
|
+
# spellings together: the attestation must never claim an envelope this path does not run.
|
|
45
|
+
#
|
|
46
|
+
# An object whose 0-0 probe names a total above the threshold is fetched as consecutive
|
|
47
|
+
# ranges instead of one stream, so a mid-transfer stall costs one range and its retries
|
|
48
|
+
# rather than the whole file. Below the threshold the ranged machinery would only add
|
|
49
|
+
# requests to a transfer the single-shot path already survives.
|
|
50
|
+
RANGED_FETCH_THRESHOLD_BYTES = 32 * 1024 * 1024
|
|
51
|
+
RANGE_FETCH_BYTES = 16 * 1024 * 1024
|
|
52
|
+
RANGE_FETCH_PARALLELISM = 2
|
|
53
|
+
RANGE_FETCH_ATTEMPTS = 4
|
|
54
|
+
# Per-range stall detection is deliberately much tighter than the single-shot 120-second
|
|
55
|
+
# read / 600-second total budget: a 16 MiB range that makes no progress for 30 seconds is
|
|
56
|
+
# a stalled range worth retrying, not a slow origin worth waiting out.
|
|
57
|
+
RANGE_FETCH_READ_TIMEOUT_SECONDS = 30.0
|
|
58
|
+
RANGE_FETCH_TOTAL_TIMEOUT_SECONDS = 120.0
|
|
59
|
+
# The aggregate wall-clock bound for the whole ranged fetch, probe included, equal to the
|
|
60
|
+
# attested single-shot total (total_timeout_milliseconds = 600_000): splitting a fetch
|
|
61
|
+
# into ranges must not quietly widen how long one acquisition may hold the origin.
|
|
62
|
+
# Admission-bounded, not a hard wall: the deadline is checked before each range attempt
|
|
63
|
+
# is admitted, and one attempt already admitted may then run to its own per-range total,
|
|
64
|
+
# so the wall-clock worst case is this bound plus one RANGE_FETCH_TOTAL_TIMEOUT_SECONDS
|
|
65
|
+
# attempt (~720 s). The probe-failure fallback composes the bounded probe (at most the
|
|
66
|
+
# per-range total) with one single-shot fetch (at most 600 s), so it shares the same
|
|
67
|
+
# ~720 s worst case.
|
|
68
|
+
RANGED_FETCH_DEADLINE_SECONDS = 600.0
|
|
69
|
+
# Headroom over the exact range size so the transport's byte ceiling refuses a response
|
|
70
|
+
# that overruns its ordered span without refusing the span itself.
|
|
71
|
+
RANGE_FETCH_RESPONSE_SLACK_BYTES = 64 * 1024
|
|
72
|
+
# The only per-range failures worth a fresh attempt: a peer that stalled or disconnected
|
|
73
|
+
# (TRANSPORT_FAILURE) and a range that timed out its own per-range budget (TOTAL_TIMEOUT).
|
|
74
|
+
# Every other refusal is a policy or integrity verdict that a retry cannot change and
|
|
75
|
+
# must stay loud.
|
|
76
|
+
_RETRYABLE_RANGE_FAILURE_CODES = frozenset({"TRANSPORT_FAILURE", "TOTAL_TIMEOUT"})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def production_retriever() -> PinnedHttpsRetriever:
|
|
80
|
+
"""Return the single reviewed OpenLigaDB HTTPS retriever."""
|
|
81
|
+
|
|
82
|
+
return PinnedHttpsRetriever(
|
|
83
|
+
resolver=SystemResolver(),
|
|
84
|
+
egress_policy=EgressPolicy(
|
|
85
|
+
allowed_hostnames=("api.openligadb.de",),
|
|
86
|
+
allowed_ports=frozenset({443}),
|
|
87
|
+
),
|
|
88
|
+
transport=StdlibPinnedTransport(),
|
|
89
|
+
limits=RetrievalLimits(
|
|
90
|
+
# The reviewed OpenLigaDB policy caps one response at 8 MiB; its egress-policy
|
|
91
|
+
# attestation states exactly this value and does not move with the public-HTTPS
|
|
92
|
+
# ceiling in MAX_RAW_RESPONSE_BYTES.
|
|
93
|
+
max_response_bytes=8 * 1024 * 1024,
|
|
94
|
+
max_redirects=0,
|
|
95
|
+
max_requests=1,
|
|
96
|
+
connect_timeout_seconds=5.0,
|
|
97
|
+
read_timeout_seconds=15.0,
|
|
98
|
+
total_timeout_seconds=20.0,
|
|
99
|
+
max_concurrency=2,
|
|
100
|
+
min_interval_seconds=1.0,
|
|
101
|
+
allowed_media_types=("application/json",),
|
|
102
|
+
user_agent="MostlyRightDataHarness-OpenLigaDB/1.0",
|
|
103
|
+
),
|
|
104
|
+
limiter=_OPENLIGADB_LIMITER,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _public_https_retriever(
|
|
109
|
+
request: HostedCrawlerRequest,
|
|
110
|
+
hostname: str,
|
|
111
|
+
*,
|
|
112
|
+
max_response_bytes: int,
|
|
113
|
+
read_timeout_seconds: float,
|
|
114
|
+
total_timeout_seconds: float,
|
|
115
|
+
) -> PinnedHttpsRetriever:
|
|
116
|
+
"""The one reviewed public-HTTPS retriever construction.
|
|
117
|
+
|
|
118
|
+
The single-shot fetch and the per-range fetch share every term of this construction --
|
|
119
|
+
resolver, egress pin, transport, redirect and request budgets, media-type allowlist,
|
|
120
|
+
limiter -- and differ only in the three explicit caps a caller names: the byte ceiling
|
|
121
|
+
and the two stall timeouts. Stated once so the ranged path cannot drift into a second,
|
|
122
|
+
subtly different transport policy.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
return PinnedHttpsRetriever(
|
|
126
|
+
resolver=SystemResolver(),
|
|
127
|
+
egress_policy=EgressPolicy(
|
|
128
|
+
allowed_hostnames=(hostname,),
|
|
129
|
+
allowed_ports=frozenset({443}),
|
|
130
|
+
),
|
|
131
|
+
transport=StdlibPinnedTransport(),
|
|
132
|
+
limits=RetrievalLimits(
|
|
133
|
+
max_response_bytes=max_response_bytes,
|
|
134
|
+
max_redirects=3,
|
|
135
|
+
max_requests=4,
|
|
136
|
+
connect_timeout_seconds=5.0,
|
|
137
|
+
read_timeout_seconds=read_timeout_seconds,
|
|
138
|
+
total_timeout_seconds=total_timeout_seconds,
|
|
139
|
+
max_concurrency=2,
|
|
140
|
+
min_interval_seconds=1.0,
|
|
141
|
+
allowed_media_types=_public_expected_media_types(request.query),
|
|
142
|
+
user_agent="MostlyRightDataHarness-PublicHttps/1.0",
|
|
143
|
+
),
|
|
144
|
+
limiter=_PUBLIC_HTTPS_LIMITER,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def fetch_crawler_source(
|
|
149
|
+
request: HostedCrawlerRequest,
|
|
150
|
+
*,
|
|
151
|
+
expected_egress_policy_attestation: str,
|
|
152
|
+
retriever: PinnedHttpsRetriever | None = None,
|
|
153
|
+
) -> RetrievedBytes:
|
|
154
|
+
"""Fetch one exact reviewed URL for the crawler child to normalize.
|
|
155
|
+
|
|
156
|
+
The retrieved bytes are framed by encode_normalization_input and handed to the
|
|
157
|
+
mr-data-crawler child, which hosted_crawler_job spawns under a seccomp filter that
|
|
158
|
+
denies socket and connect. That child is not acquisition.sandbox.CrawlerSandbox.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
coordinate = validate_crawler_request(
|
|
162
|
+
request,
|
|
163
|
+
expected_egress_policy_attestation=expected_egress_policy_attestation,
|
|
164
|
+
)
|
|
165
|
+
if request.adapter_id == "public.https":
|
|
166
|
+
requested_url = request.query["url"]
|
|
167
|
+
hostname = canonical_public_hostname(urlsplit(requested_url))
|
|
168
|
+
retrieved = _retrieve_public_https(
|
|
169
|
+
request,
|
|
170
|
+
requested_url=requested_url,
|
|
171
|
+
hostname=hostname,
|
|
172
|
+
injected_retriever=retriever,
|
|
173
|
+
)
|
|
174
|
+
if (
|
|
175
|
+
retrieved.source_url != requested_url
|
|
176
|
+
or canonical_public_hostname(urlsplit(retrieved.final_url)) != hostname
|
|
177
|
+
):
|
|
178
|
+
raise HostedCrawlerError(
|
|
179
|
+
"CRAWLER_ENDPOINT_MISMATCH",
|
|
180
|
+
"retrieval changed the reviewed public HTTPS source authority",
|
|
181
|
+
)
|
|
182
|
+
return retrieved
|
|
183
|
+
league, season = coordinate
|
|
184
|
+
requested_url = f"https://api.openligadb.de/getmatchdata/{league}/{season}"
|
|
185
|
+
retrieved = (retriever or production_retriever()).retrieve(requested_url)
|
|
186
|
+
if retrieved.source_url != requested_url or retrieved.final_url != requested_url:
|
|
187
|
+
raise HostedCrawlerError(
|
|
188
|
+
"CRAWLER_ENDPOINT_MISMATCH",
|
|
189
|
+
"retrieval changed the exact reviewed OpenLigaDB endpoint",
|
|
190
|
+
)
|
|
191
|
+
if retrieved.media_type != "application/json":
|
|
192
|
+
raise HostedCrawlerError(
|
|
193
|
+
"CRAWLER_MEDIA_TYPE_MISMATCH",
|
|
194
|
+
"retrieval changed the exact reviewed OpenLigaDB media type",
|
|
195
|
+
)
|
|
196
|
+
return retrieved
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _retrieve_public_https(
|
|
200
|
+
request: HostedCrawlerRequest,
|
|
201
|
+
*,
|
|
202
|
+
requested_url: str,
|
|
203
|
+
hostname: str,
|
|
204
|
+
injected_retriever: PinnedHttpsRetriever | None,
|
|
205
|
+
) -> RetrievedBytes:
|
|
206
|
+
"""Retrieve one public HTTPS source, ranged when the origin supports it and size warrants.
|
|
207
|
+
|
|
208
|
+
Look-then-lock, applied to bulk transfer: the existing one-byte presence-style probe
|
|
209
|
+
asks the origin whether it answers ranges and how large the object is. The probe runs
|
|
210
|
+
under the ranged-tight retriever construction, not the single-shot one, so an origin
|
|
211
|
+
that ignores the range and streams its whole body costs at most one range-sized
|
|
212
|
+
response before the refusal surfaces, never the whole object. Only an origin that
|
|
213
|
+
answered 206 with a parseable complete size above RANGED_FETCH_THRESHOLD_BYTES *and*
|
|
214
|
+
named at least one validator (ETag or Last-Modified) on the probe takes the ranged
|
|
215
|
+
strategy: without a validator there is no way to prove every range came from the same
|
|
216
|
+
edition of the object, so a validator-less origin cannot be ranged safely and keeps
|
|
217
|
+
the single-shot path. Every other outcome -- a 200 that ignored the range, a refused
|
|
218
|
+
or unparseable probe, an unknown ``*`` total, an object at or below the threshold --
|
|
219
|
+
also falls back to the exact single-shot path this module always had.
|
|
220
|
+
|
|
221
|
+
Whatever the probe transferred is never dropped from the sealed evidence: a fallback
|
|
222
|
+
result carries the probe's measured hops -- from the probe itself when it succeeded,
|
|
223
|
+
or from the typed refusal's own hop evidence when it did not -- ahead of the
|
|
224
|
+
single-shot hops, with the single-shot 200 as the terminal hop, so the composed
|
|
225
|
+
``total_response_body_size_bytes`` accounts every response byte the origin shipped.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
max_source_bytes = request.query["limits"]["max_source_bytes"]
|
|
229
|
+
range_retriever = injected_retriever or _public_https_retriever(
|
|
230
|
+
request,
|
|
231
|
+
hostname,
|
|
232
|
+
max_response_bytes=RANGE_FETCH_BYTES + RANGE_FETCH_RESPONSE_SLACK_BYTES,
|
|
233
|
+
read_timeout_seconds=RANGE_FETCH_READ_TIMEOUT_SECONDS,
|
|
234
|
+
total_timeout_seconds=RANGE_FETCH_TOTAL_TIMEOUT_SECONDS,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def single_shot(probe_hops: tuple[RetrievalHop, ...]) -> RetrievedBytes:
|
|
238
|
+
retriever = injected_retriever or _public_https_retriever(
|
|
239
|
+
request,
|
|
240
|
+
hostname,
|
|
241
|
+
max_response_bytes=max_source_bytes,
|
|
242
|
+
read_timeout_seconds=120.0,
|
|
243
|
+
total_timeout_seconds=600.0,
|
|
244
|
+
)
|
|
245
|
+
return _seal_probe_evidence(probe_hops, retriever.retrieve(requested_url))
|
|
246
|
+
|
|
247
|
+
started = float(time.monotonic())
|
|
248
|
+
# A transport-class probe failure means an unlucky connection, not a
|
|
249
|
+
# range-incapable origin: retrying the one-byte probe is far cheaper than
|
|
250
|
+
# surrendering to the single-shot path this feature exists to replace.
|
|
251
|
+
# Structural answers (a 200, a redirect on Range) still mean single-shot.
|
|
252
|
+
probe = None
|
|
253
|
+
probe_error: AcquisitionSecurityError | None = None
|
|
254
|
+
for _ in range(RANGE_FETCH_ATTEMPTS):
|
|
255
|
+
try:
|
|
256
|
+
probe = range_retriever.retrieve_presence_evidence(requested_url)
|
|
257
|
+
break
|
|
258
|
+
except AcquisitionSecurityError as error:
|
|
259
|
+
probe_error = error
|
|
260
|
+
if error.code not in _RETRYABLE_RANGE_FAILURE_CODES:
|
|
261
|
+
break
|
|
262
|
+
if float(time.monotonic()) - started >= RANGED_FETCH_DEADLINE_SECONDS:
|
|
263
|
+
break
|
|
264
|
+
if probe is None:
|
|
265
|
+
# The refusal's own hop evidence carries whatever the failed probe consumed
|
|
266
|
+
# (a range-ignoring origin's 200 body, for instance) into the sealed result.
|
|
267
|
+
return single_shot(tuple(getattr(probe_error, "hops", ())))
|
|
268
|
+
total = probe.complete_size_bytes
|
|
269
|
+
if total is None or total <= RANGED_FETCH_THRESHOLD_BYTES:
|
|
270
|
+
return single_shot(tuple(probe.hops))
|
|
271
|
+
if total > max_source_bytes:
|
|
272
|
+
# The single-shot path would stream up to the cap and then refuse; the probe already
|
|
273
|
+
# proved the object cannot fit, so refuse with the same code before moving a byte.
|
|
274
|
+
raise AcquisitionSecurityError(
|
|
275
|
+
"RESPONSE_LIMIT",
|
|
276
|
+
"probed complete object size exceeds the reviewed source byte budget",
|
|
277
|
+
)
|
|
278
|
+
probe_terminal = probe.hops[-1]
|
|
279
|
+
validators = (probe_terminal.etag, probe_terminal.last_modified)
|
|
280
|
+
if validators == (None, None):
|
|
281
|
+
return single_shot(tuple(probe.hops))
|
|
282
|
+
return _fetch_ranged(
|
|
283
|
+
requested_url,
|
|
284
|
+
hostname=hostname,
|
|
285
|
+
probe=probe,
|
|
286
|
+
total=total,
|
|
287
|
+
validators=validators,
|
|
288
|
+
retriever=range_retriever,
|
|
289
|
+
deadline=started + RANGED_FETCH_DEADLINE_SECONDS,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _seal_probe_evidence(
|
|
294
|
+
probe_hops: tuple[RetrievalHop, ...],
|
|
295
|
+
retrieved: RetrievedBytes,
|
|
296
|
+
) -> RetrievedBytes:
|
|
297
|
+
"""Seal the probe's measured hops ahead of a single-shot result's own evidence."""
|
|
298
|
+
|
|
299
|
+
if not probe_hops:
|
|
300
|
+
return retrieved
|
|
301
|
+
hops = probe_hops + retrieved.hops
|
|
302
|
+
return RetrievedBytes(
|
|
303
|
+
source_url=retrieved.source_url,
|
|
304
|
+
final_url=retrieved.final_url,
|
|
305
|
+
media_type=retrieved.media_type,
|
|
306
|
+
content=retrieved.content,
|
|
307
|
+
content_sha256=retrieved.content_sha256,
|
|
308
|
+
hops=hops,
|
|
309
|
+
transport_evidence_digest=canonical_sha256([hop.to_dict() for hop in hops]),
|
|
310
|
+
complete_size_bytes=retrieved.complete_size_bytes,
|
|
311
|
+
rate_observation=retrieved.rate_observation,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _fetch_ranged(
|
|
316
|
+
url: str,
|
|
317
|
+
*,
|
|
318
|
+
hostname: str,
|
|
319
|
+
probe: RetrievedBytes,
|
|
320
|
+
total: int,
|
|
321
|
+
validators: tuple[str | None, str | None],
|
|
322
|
+
retriever: PinnedHttpsRetriever,
|
|
323
|
+
deadline: float,
|
|
324
|
+
) -> RetrievedBytes:
|
|
325
|
+
"""Fetch [0, total) as consecutive ranges and compose one RetrievedBytes.
|
|
326
|
+
|
|
327
|
+
The transport layer already proves each delivered slice: exactly 206, a Content-Range
|
|
328
|
+
naming exactly the ordered span, and span, declared length and body agreeing byte for
|
|
329
|
+
byte (RANGE_MISMATCH / RANGE_LENGTH / CONTENT_RANGE in acquisition.http). This
|
|
330
|
+
function adds only what a single-range call cannot know: every range must stay on the
|
|
331
|
+
reviewed hostname, answer with exactly the validators the probe pinned, name the same
|
|
332
|
+
complete size the probe named, and assemble in offset order to exactly that size.
|
|
333
|
+
|
|
334
|
+
The validator pin is what makes ranged assembly of a *mutable* origin sound. Three
|
|
335
|
+
lengths agreeing proves each slice is internally consistent; it cannot prove two
|
|
336
|
+
slices came from the same edition of the object, and a same-size origin update
|
|
337
|
+
mid-fetch would otherwise assemble interleaved bytes of two editions into one sealed
|
|
338
|
+
digest. Comparing the recorded validators, rather than sending ``If-Range``, keeps
|
|
339
|
+
the attested outgoing header set exactly as it is: the refusal is decided from the
|
|
340
|
+
same hop evidence the result seals, not from origin-side conditional behavior.
|
|
341
|
+
|
|
342
|
+
``deadline`` is admission-bounded: it is checked before each attempt is admitted, and
|
|
343
|
+
an attempt already admitted may run to the retriever's own per-range total, so the
|
|
344
|
+
wall clock may overrun the deadline by at most one RANGE_FETCH_TOTAL_TIMEOUT_SECONDS.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
ranges = tuple(
|
|
348
|
+
ByteRange(first_byte=first_byte, last_byte=min(first_byte + RANGE_FETCH_BYTES, total) - 1)
|
|
349
|
+
for first_byte in range(0, total, RANGE_FETCH_BYTES)
|
|
350
|
+
)
|
|
351
|
+
require_range_plan_within_budget(len(ranges), max_ranges=retriever.limits.max_ranges)
|
|
352
|
+
|
|
353
|
+
def fetch_one(byte_range: ByteRange) -> RetrievedBytes:
|
|
354
|
+
failure: AcquisitionSecurityError | None = None
|
|
355
|
+
for _attempt in range(RANGE_FETCH_ATTEMPTS):
|
|
356
|
+
if float(time.monotonic()) > deadline:
|
|
357
|
+
raise AcquisitionSecurityError(
|
|
358
|
+
"TOTAL_TIMEOUT",
|
|
359
|
+
"ranged retrieval exceeded its aggregate time budget",
|
|
360
|
+
)
|
|
361
|
+
try:
|
|
362
|
+
piece = retriever.retrieve(url, byte_range=byte_range)
|
|
363
|
+
except AcquisitionSecurityError as error:
|
|
364
|
+
if error.code not in _RETRYABLE_RANGE_FAILURE_CODES:
|
|
365
|
+
raise
|
|
366
|
+
failure = error
|
|
367
|
+
continue
|
|
368
|
+
if canonical_public_hostname(urlsplit(piece.final_url)) != hostname:
|
|
369
|
+
raise HostedCrawlerError(
|
|
370
|
+
"CRAWLER_ENDPOINT_MISMATCH",
|
|
371
|
+
"retrieval changed the reviewed public HTTPS source authority",
|
|
372
|
+
)
|
|
373
|
+
terminal = piece.hops[-1]
|
|
374
|
+
if (terminal.etag, terminal.last_modified) != validators:
|
|
375
|
+
# An integrity verdict, not a stall: the origin changed the object under
|
|
376
|
+
# the fetch, and no retry of the same range can make two editions one.
|
|
377
|
+
raise AcquisitionSecurityError(
|
|
378
|
+
"RANGE_VALIDATOR_MISMATCH",
|
|
379
|
+
"range response validators differ from the probed object edition",
|
|
380
|
+
)
|
|
381
|
+
if piece.complete_size_bytes != total:
|
|
382
|
+
raise AcquisitionSecurityError(
|
|
383
|
+
"CONTENT_RANGE",
|
|
384
|
+
"range response names a complete size different from the probed object",
|
|
385
|
+
)
|
|
386
|
+
return piece
|
|
387
|
+
# A range that exhausted its attempts fails the whole fetch with the last
|
|
388
|
+
# transport failure exactly as a stalled single-shot fetch would fail.
|
|
389
|
+
assert failure is not None
|
|
390
|
+
raise failure
|
|
391
|
+
|
|
392
|
+
# Wrapper-side worker threads in the coordinator process; nothing here runs in the
|
|
393
|
+
# seccomp-filtered normalizer child.
|
|
394
|
+
with ThreadPoolExecutor(max_workers=RANGE_FETCH_PARALLELISM) as pool:
|
|
395
|
+
futures = [pool.submit(fetch_one, byte_range) for byte_range in ranges]
|
|
396
|
+
try:
|
|
397
|
+
pieces = [future.result() for future in futures]
|
|
398
|
+
except BaseException:
|
|
399
|
+
for future in futures:
|
|
400
|
+
future.cancel()
|
|
401
|
+
raise
|
|
402
|
+
content = b"".join(piece.content for piece in pieces)
|
|
403
|
+
if len(content) != total:
|
|
404
|
+
raise AcquisitionSecurityError(
|
|
405
|
+
"RANGE_LENGTH",
|
|
406
|
+
"assembled ranged content does not match the probed complete size",
|
|
407
|
+
)
|
|
408
|
+
# One composed retrieval, shaped exactly like a single-shot RetrievedBytes so every
|
|
409
|
+
# downstream consumer (normalization framing, receipts, sealing, CLI validation) is
|
|
410
|
+
# untouched. Identity fields come from the probe and the first range; the evidence is
|
|
411
|
+
# the probe hop followed by every per-range hop in offset order, and the digest is the
|
|
412
|
+
# same canonical hash over that exact hop list that RetrievedBytes itself re-enforces.
|
|
413
|
+
hops = tuple(probe.hops) + tuple(hop for piece in pieces for hop in piece.hops)
|
|
414
|
+
return RetrievedBytes(
|
|
415
|
+
source_url=url,
|
|
416
|
+
final_url=probe.final_url,
|
|
417
|
+
media_type=pieces[0].media_type,
|
|
418
|
+
content=content,
|
|
419
|
+
content_sha256=sha256_bytes(content),
|
|
420
|
+
hops=hops,
|
|
421
|
+
transport_evidence_digest=canonical_sha256([hop.to_dict() for hop in hops]),
|
|
422
|
+
complete_size_bytes=total,
|
|
423
|
+
)
|