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,56 @@
|
|
|
1
|
+
"""Core contracts and local durable state for resumable engine-wide ingestion."""
|
|
2
|
+
|
|
3
|
+
from mostlyright.data_harness.ingestion.contracts import (
|
|
4
|
+
CHUNK_REF_SCHEMA,
|
|
5
|
+
INGESTION_MANIFEST_SCHEMA,
|
|
6
|
+
SOURCE_VERSION_PIN_SCHEMA,
|
|
7
|
+
ChunkRef,
|
|
8
|
+
IngestionConflict,
|
|
9
|
+
IngestionCorrupt,
|
|
10
|
+
IngestionError,
|
|
11
|
+
IngestionManifestV1,
|
|
12
|
+
SourceVersionPinV1,
|
|
13
|
+
merkle_root_sha256,
|
|
14
|
+
)
|
|
15
|
+
from mostlyright.data_harness.ingestion.faults import DeterministicFaultInjector, FaultInjected
|
|
16
|
+
from mostlyright.data_harness.ingestion.gcs_store import (
|
|
17
|
+
GCSIngestionStore,
|
|
18
|
+
GCSObjectNotFound,
|
|
19
|
+
GCSObjectRead,
|
|
20
|
+
GCSObjectTransport,
|
|
21
|
+
GCSObjectVersion,
|
|
22
|
+
GCSPreconditionFailed,
|
|
23
|
+
GCSStorageError,
|
|
24
|
+
GoogleCloudStorageJSONTransport,
|
|
25
|
+
workload_identity_access_token,
|
|
26
|
+
)
|
|
27
|
+
from mostlyright.data_harness.ingestion.spool import SpooledChunk, spool_hash_stream
|
|
28
|
+
from mostlyright.data_harness.ingestion.store import IngestionStore, LocalIngestionStore
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"CHUNK_REF_SCHEMA",
|
|
32
|
+
"INGESTION_MANIFEST_SCHEMA",
|
|
33
|
+
"SOURCE_VERSION_PIN_SCHEMA",
|
|
34
|
+
"ChunkRef",
|
|
35
|
+
"DeterministicFaultInjector",
|
|
36
|
+
"FaultInjected",
|
|
37
|
+
"GCSIngestionStore",
|
|
38
|
+
"GCSObjectNotFound",
|
|
39
|
+
"GCSObjectRead",
|
|
40
|
+
"GCSObjectTransport",
|
|
41
|
+
"GCSObjectVersion",
|
|
42
|
+
"GCSPreconditionFailed",
|
|
43
|
+
"GCSStorageError",
|
|
44
|
+
"GoogleCloudStorageJSONTransport",
|
|
45
|
+
"IngestionConflict",
|
|
46
|
+
"IngestionCorrupt",
|
|
47
|
+
"IngestionError",
|
|
48
|
+
"IngestionManifestV1",
|
|
49
|
+
"IngestionStore",
|
|
50
|
+
"LocalIngestionStore",
|
|
51
|
+
"SourceVersionPinV1",
|
|
52
|
+
"SpooledChunk",
|
|
53
|
+
"merkle_root_sha256",
|
|
54
|
+
"spool_hash_stream",
|
|
55
|
+
"workload_identity_access_token",
|
|
56
|
+
]
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""Immutable contracts for the resumable ingestion engine.
|
|
2
|
+
|
|
3
|
+
The engine deliberately commits one source version at a time. A source is pinned before
|
|
4
|
+
the first byte is accepted, chunks are admitted in one gap-free ordinal sequence, and a
|
|
5
|
+
finalized manifest is an immutable, order-sensitive Merkle commitment. This leaves a
|
|
6
|
+
small, portable contract for remote stores while keeping the local store implementation
|
|
7
|
+
free to decide how it makes the individual state transitions durable.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from dataclasses import dataclass, replace
|
|
16
|
+
from typing import Any, Self
|
|
17
|
+
|
|
18
|
+
from mostlyright.data_harness.canonical import canonical_json_bytes, canonical_sha256
|
|
19
|
+
|
|
20
|
+
SOURCE_VERSION_PIN_SCHEMA = "harness-source-version-pin.v1"
|
|
21
|
+
CHUNK_REF_SCHEMA = "harness-ingestion-chunk-ref.v1"
|
|
22
|
+
INGESTION_MANIFEST_SCHEMA = "harness-ingestion-manifest.v1"
|
|
23
|
+
|
|
24
|
+
MAX_SOURCE_ID_LENGTH = 128
|
|
25
|
+
MAX_SOURCE_VERSION_LENGTH = 512
|
|
26
|
+
MAX_RUN_ID_LENGTH = 128
|
|
27
|
+
MAX_CHUNK_BYTES = 1 << 40
|
|
28
|
+
MAX_RECORDS_PER_CHUNK = (1 << 63) - 1
|
|
29
|
+
MAX_MANIFEST_CHUNKS = 10_000
|
|
30
|
+
|
|
31
|
+
_SOURCE_ID = re.compile(r"[a-z][a-z0-9._-]{0,127}\Z")
|
|
32
|
+
_RUN_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}\Z")
|
|
33
|
+
_DIGEST = re.compile(r"[0-9a-f]{64}\Z")
|
|
34
|
+
# A source version crosses the durable checkpoint boundary. It is not an opaque connector header:
|
|
35
|
+
# ETags, cursors, signed URLs and bearer values have all appeared in such headers in practice.
|
|
36
|
+
# Admit only a public immutable content identity or a monotonically assigned public generation.
|
|
37
|
+
# The connector-specific canonical evidence remains in ``fence_sha256``; no raw response value is
|
|
38
|
+
# needed in a manifest.
|
|
39
|
+
_PUBLIC_SOURCE_VERSION = re.compile(
|
|
40
|
+
r"(?:generation:(?:0|[1-9][0-9]{0,31})|(?:etag|version)-sha256:[0-9a-f]{64})\Z"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class IngestionError(ValueError):
|
|
45
|
+
"""A stable refusal or corruption error from the ingestion core."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, code: str, path: str, detail: str) -> None:
|
|
48
|
+
self.code = code
|
|
49
|
+
self.path = path
|
|
50
|
+
self.detail = detail
|
|
51
|
+
super().__init__(f"{path}: {detail} [{code}]")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IngestionConflict(IngestionError):
|
|
55
|
+
"""A fenced transition did not apply to the state the caller observed."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class IngestionCorrupt(IngestionError):
|
|
59
|
+
"""Durable bytes no longer satisfy the ingestion contract."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _require_digest(value: object, path: str) -> str:
|
|
63
|
+
if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
|
|
64
|
+
raise IngestionError("INGESTION_DIGEST", path, "must be a lowercase SHA-256 digest")
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _require_text(value: object, path: str, *, maximum: int) -> str:
|
|
69
|
+
if (
|
|
70
|
+
not isinstance(value, str)
|
|
71
|
+
or not value
|
|
72
|
+
or len(value) > maximum
|
|
73
|
+
or any(ord(character) < 0x20 or 0xD800 <= ord(character) <= 0xDFFF for character in value)
|
|
74
|
+
):
|
|
75
|
+
raise IngestionError("INGESTION_TEXT", path, "must be a bounded non-control string")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _require_exact_mapping(value: object, keys: frozenset[str], path: str) -> Mapping[str, Any]:
|
|
80
|
+
if not isinstance(value, Mapping) or set(value) != keys:
|
|
81
|
+
raise IngestionError("INGESTION_DOCUMENT", path, "has unsupported or missing members")
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class SourceVersionPinV1:
|
|
87
|
+
"""The exact source revision and acquisition policy a run is allowed to consume.
|
|
88
|
+
|
|
89
|
+
``source_version`` is one non-secret immutable identity: a public generation or a digest of
|
|
90
|
+
a public ETag/version. ``fence_sha256`` binds the connector-specific version evidence after
|
|
91
|
+
canonicalization. ``acquisition_policy_sha256`` binds the reviewed connector policy. Neither
|
|
92
|
+
permits a raw header/cursor or a later source version to enter durable resume state.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
source_id: str
|
|
96
|
+
source_version: str
|
|
97
|
+
fence_sha256: str
|
|
98
|
+
acquisition_policy_sha256: str
|
|
99
|
+
schema_version: str = SOURCE_VERSION_PIN_SCHEMA
|
|
100
|
+
|
|
101
|
+
def __post_init__(self) -> None:
|
|
102
|
+
if self.schema_version != SOURCE_VERSION_PIN_SCHEMA:
|
|
103
|
+
raise IngestionError(
|
|
104
|
+
"INGESTION_SOURCE_SCHEMA",
|
|
105
|
+
"source_version_pin.schema_version",
|
|
106
|
+
"is unsupported",
|
|
107
|
+
)
|
|
108
|
+
if not isinstance(self.source_id, str) or _SOURCE_ID.fullmatch(self.source_id) is None:
|
|
109
|
+
raise IngestionError(
|
|
110
|
+
"INGESTION_SOURCE_ID",
|
|
111
|
+
"source_version_pin.source_id",
|
|
112
|
+
"is invalid",
|
|
113
|
+
)
|
|
114
|
+
if (
|
|
115
|
+
not isinstance(self.source_version, str)
|
|
116
|
+
or len(self.source_version) > MAX_SOURCE_VERSION_LENGTH
|
|
117
|
+
or _PUBLIC_SOURCE_VERSION.fullmatch(self.source_version) is None
|
|
118
|
+
):
|
|
119
|
+
raise IngestionError(
|
|
120
|
+
"INGESTION_SOURCE_VERSION",
|
|
121
|
+
"source_version_pin.source_version",
|
|
122
|
+
"must be a public generation or a SHA-256-pinned public ETag/version",
|
|
123
|
+
)
|
|
124
|
+
_require_digest(self.fence_sha256, "source_version_pin.fence_sha256")
|
|
125
|
+
_require_digest(
|
|
126
|
+
self.acquisition_policy_sha256,
|
|
127
|
+
"source_version_pin.acquisition_policy_sha256",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def digest(self) -> str:
|
|
132
|
+
return canonical_sha256(self.to_dict())
|
|
133
|
+
|
|
134
|
+
def to_dict(self) -> dict[str, str]:
|
|
135
|
+
return {
|
|
136
|
+
"schema_version": self.schema_version,
|
|
137
|
+
"source_id": self.source_id,
|
|
138
|
+
"source_version": self.source_version,
|
|
139
|
+
"fence_sha256": self.fence_sha256,
|
|
140
|
+
"acquisition_policy_sha256": self.acquisition_policy_sha256,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_dict(cls, value: object) -> Self:
|
|
145
|
+
document = _require_exact_mapping(
|
|
146
|
+
value,
|
|
147
|
+
frozenset(
|
|
148
|
+
{
|
|
149
|
+
"schema_version",
|
|
150
|
+
"source_id",
|
|
151
|
+
"source_version",
|
|
152
|
+
"fence_sha256",
|
|
153
|
+
"acquisition_policy_sha256",
|
|
154
|
+
}
|
|
155
|
+
),
|
|
156
|
+
"source_version_pin",
|
|
157
|
+
)
|
|
158
|
+
return cls(
|
|
159
|
+
schema_version=document["schema_version"],
|
|
160
|
+
source_id=document["source_id"],
|
|
161
|
+
source_version=document["source_version"],
|
|
162
|
+
fence_sha256=document["fence_sha256"],
|
|
163
|
+
acquisition_policy_sha256=document["acquisition_policy_sha256"],
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(frozen=True)
|
|
168
|
+
class ChunkRef:
|
|
169
|
+
"""One immutable byte object, assigned exactly one position in a pinned run."""
|
|
170
|
+
|
|
171
|
+
ordinal: int
|
|
172
|
+
content_sha256: str
|
|
173
|
+
byte_length: int
|
|
174
|
+
record_count: int
|
|
175
|
+
source_pin_sha256: str
|
|
176
|
+
schema_version: str = CHUNK_REF_SCHEMA
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
if self.schema_version != CHUNK_REF_SCHEMA:
|
|
180
|
+
raise IngestionError(
|
|
181
|
+
"INGESTION_CHUNK_SCHEMA", "chunk_ref.schema_version", "is unsupported"
|
|
182
|
+
)
|
|
183
|
+
if isinstance(self.ordinal, bool) or not isinstance(self.ordinal, int) or self.ordinal < 0:
|
|
184
|
+
raise IngestionError(
|
|
185
|
+
"INGESTION_ORDINAL", "chunk_ref.ordinal", "must be a non-negative integer"
|
|
186
|
+
)
|
|
187
|
+
_require_digest(self.content_sha256, "chunk_ref.content_sha256")
|
|
188
|
+
_require_digest(self.source_pin_sha256, "chunk_ref.source_pin_sha256")
|
|
189
|
+
if (
|
|
190
|
+
isinstance(self.byte_length, bool)
|
|
191
|
+
or not isinstance(self.byte_length, int)
|
|
192
|
+
or not 0 <= self.byte_length <= MAX_CHUNK_BYTES
|
|
193
|
+
):
|
|
194
|
+
raise IngestionError(
|
|
195
|
+
"INGESTION_CHUNK_BYTES",
|
|
196
|
+
"chunk_ref.byte_length",
|
|
197
|
+
"is outside the supported range",
|
|
198
|
+
)
|
|
199
|
+
if (
|
|
200
|
+
isinstance(self.record_count, bool)
|
|
201
|
+
or not isinstance(self.record_count, int)
|
|
202
|
+
or not 0 <= self.record_count <= MAX_RECORDS_PER_CHUNK
|
|
203
|
+
):
|
|
204
|
+
raise IngestionError(
|
|
205
|
+
"INGESTION_RECORD_COUNT",
|
|
206
|
+
"chunk_ref.record_count",
|
|
207
|
+
"is outside the supported range",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def digest(self) -> str:
|
|
212
|
+
return canonical_sha256(self.to_dict())
|
|
213
|
+
|
|
214
|
+
def to_dict(self) -> dict[str, str | int]:
|
|
215
|
+
return {
|
|
216
|
+
"schema_version": self.schema_version,
|
|
217
|
+
"ordinal": self.ordinal,
|
|
218
|
+
"content_sha256": self.content_sha256,
|
|
219
|
+
"byte_length": self.byte_length,
|
|
220
|
+
"record_count": self.record_count,
|
|
221
|
+
"source_pin_sha256": self.source_pin_sha256,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def from_dict(cls, value: object) -> Self:
|
|
226
|
+
document = _require_exact_mapping(
|
|
227
|
+
value,
|
|
228
|
+
frozenset(
|
|
229
|
+
{
|
|
230
|
+
"schema_version",
|
|
231
|
+
"ordinal",
|
|
232
|
+
"content_sha256",
|
|
233
|
+
"byte_length",
|
|
234
|
+
"record_count",
|
|
235
|
+
"source_pin_sha256",
|
|
236
|
+
}
|
|
237
|
+
),
|
|
238
|
+
"chunk_ref",
|
|
239
|
+
)
|
|
240
|
+
return cls(
|
|
241
|
+
schema_version=document["schema_version"],
|
|
242
|
+
ordinal=document["ordinal"],
|
|
243
|
+
content_sha256=document["content_sha256"],
|
|
244
|
+
byte_length=document["byte_length"],
|
|
245
|
+
record_count=document["record_count"],
|
|
246
|
+
source_pin_sha256=document["source_pin_sha256"],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def merkle_root_sha256(chunks: tuple[ChunkRef, ...]) -> str:
|
|
251
|
+
"""Return an order-sensitive, domain-separated Merkle root for ``chunks``."""
|
|
252
|
+
|
|
253
|
+
if not chunks:
|
|
254
|
+
return hashlib.sha256(b"mostlyright-ingestion-merkle-v1\x00empty").hexdigest()
|
|
255
|
+
level = [
|
|
256
|
+
hashlib.sha256(
|
|
257
|
+
b"mostlyright-ingestion-merkle-v1\x00leaf\x00" + canonical_json_bytes(chunk.to_dict())
|
|
258
|
+
).digest()
|
|
259
|
+
for chunk in chunks
|
|
260
|
+
]
|
|
261
|
+
while len(level) > 1:
|
|
262
|
+
if len(level) % 2:
|
|
263
|
+
level.append(level[-1])
|
|
264
|
+
level = [
|
|
265
|
+
hashlib.sha256(b"mostlyright-ingestion-merkle-v1\x00node\x00" + left + right).digest()
|
|
266
|
+
for left, right in zip(level[::2], level[1::2], strict=True)
|
|
267
|
+
]
|
|
268
|
+
return level[0].hex()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True)
|
|
272
|
+
class IngestionManifestV1:
|
|
273
|
+
"""An append-only source-pinned state machine.
|
|
274
|
+
|
|
275
|
+
Each persisted state points at its immediate predecessor digest. That turns an
|
|
276
|
+
ambiguous successful replacement into a safe idempotent retry without allowing a
|
|
277
|
+
caller holding an arbitrary stale fence to overwrite unrelated work.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
run_id: str
|
|
281
|
+
source_pin: SourceVersionPinV1
|
|
282
|
+
chunks: tuple[ChunkRef, ...]
|
|
283
|
+
state: str
|
|
284
|
+
previous_manifest_sha256: str | None
|
|
285
|
+
merkle_root_sha256: str | None
|
|
286
|
+
schema_version: str = INGESTION_MANIFEST_SCHEMA
|
|
287
|
+
|
|
288
|
+
def __post_init__(self) -> None:
|
|
289
|
+
if self.schema_version != INGESTION_MANIFEST_SCHEMA:
|
|
290
|
+
raise IngestionError(
|
|
291
|
+
"INGESTION_MANIFEST_SCHEMA", "ingestion_manifest.schema_version", "is unsupported"
|
|
292
|
+
)
|
|
293
|
+
if not isinstance(self.run_id, str) or _RUN_ID.fullmatch(self.run_id) is None:
|
|
294
|
+
raise IngestionError("INGESTION_RUN_ID", "ingestion_manifest.run_id", "is invalid")
|
|
295
|
+
if not isinstance(self.source_pin, SourceVersionPinV1):
|
|
296
|
+
raise IngestionError(
|
|
297
|
+
"INGESTION_SOURCE_PIN",
|
|
298
|
+
"ingestion_manifest.source_pin",
|
|
299
|
+
"must be SourceVersionPinV1",
|
|
300
|
+
)
|
|
301
|
+
if not isinstance(self.chunks, tuple) or len(self.chunks) > MAX_MANIFEST_CHUNKS:
|
|
302
|
+
raise IngestionError(
|
|
303
|
+
"INGESTION_CHUNK_LIMIT",
|
|
304
|
+
"ingestion_manifest.chunks",
|
|
305
|
+
"must be a bounded tuple",
|
|
306
|
+
)
|
|
307
|
+
expected_pin = self.source_pin.digest
|
|
308
|
+
for ordinal, chunk in enumerate(self.chunks):
|
|
309
|
+
if not isinstance(chunk, ChunkRef):
|
|
310
|
+
raise IngestionError(
|
|
311
|
+
"INGESTION_CHUNK", "ingestion_manifest.chunks", "must contain ChunkRef values"
|
|
312
|
+
)
|
|
313
|
+
if chunk.ordinal != ordinal:
|
|
314
|
+
raise IngestionError(
|
|
315
|
+
"INGESTION_GAP",
|
|
316
|
+
"ingestion_manifest.chunks",
|
|
317
|
+
"ordinals must be ordered and gap-free",
|
|
318
|
+
)
|
|
319
|
+
if chunk.source_pin_sha256 != expected_pin:
|
|
320
|
+
raise IngestionError(
|
|
321
|
+
"INGESTION_SOURCE_DRIFT",
|
|
322
|
+
"ingestion_manifest.chunks",
|
|
323
|
+
"chunk belongs to a different source pin",
|
|
324
|
+
)
|
|
325
|
+
if self.previous_manifest_sha256 is not None:
|
|
326
|
+
_require_digest(
|
|
327
|
+
self.previous_manifest_sha256, "ingestion_manifest.previous_manifest_sha256"
|
|
328
|
+
)
|
|
329
|
+
if not self.chunks and self.state == "open" and self.previous_manifest_sha256 is not None:
|
|
330
|
+
raise IngestionError(
|
|
331
|
+
"INGESTION_INITIAL_PREDECESSOR",
|
|
332
|
+
"ingestion_manifest.previous_manifest_sha256",
|
|
333
|
+
"initial open state must not have a predecessor",
|
|
334
|
+
)
|
|
335
|
+
if self.chunks or self.state == "finalized":
|
|
336
|
+
if self.previous_manifest_sha256 is None:
|
|
337
|
+
raise IngestionError(
|
|
338
|
+
"INGESTION_PREDECESSOR",
|
|
339
|
+
"ingestion_manifest.previous_manifest_sha256",
|
|
340
|
+
"non-initial state must name its predecessor",
|
|
341
|
+
)
|
|
342
|
+
if self.state == "open":
|
|
343
|
+
if self.merkle_root_sha256 is not None:
|
|
344
|
+
raise IngestionError(
|
|
345
|
+
"INGESTION_OPEN_ROOT",
|
|
346
|
+
"ingestion_manifest.merkle_root_sha256",
|
|
347
|
+
"open state must not have a Merkle root",
|
|
348
|
+
)
|
|
349
|
+
elif self.state == "finalized":
|
|
350
|
+
if self.merkle_root_sha256 != merkle_root_sha256(self.chunks):
|
|
351
|
+
raise IngestionError(
|
|
352
|
+
"INGESTION_MERKLE",
|
|
353
|
+
"ingestion_manifest.merkle_root_sha256",
|
|
354
|
+
"does not commit exactly the ordered chunk sequence",
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
raise IngestionError("INGESTION_STATE", "ingestion_manifest.state", "is unsupported")
|
|
358
|
+
|
|
359
|
+
@classmethod
|
|
360
|
+
def initial(cls, *, run_id: str, source_pin: SourceVersionPinV1) -> Self:
|
|
361
|
+
return cls(
|
|
362
|
+
run_id=run_id,
|
|
363
|
+
source_pin=source_pin,
|
|
364
|
+
chunks=(),
|
|
365
|
+
state="open",
|
|
366
|
+
previous_manifest_sha256=None,
|
|
367
|
+
merkle_root_sha256=None,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def digest(self) -> str:
|
|
372
|
+
return canonical_sha256(self.to_dict())
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def is_finalized(self) -> bool:
|
|
376
|
+
return self.state == "finalized"
|
|
377
|
+
|
|
378
|
+
def append(self, chunk: ChunkRef) -> Self:
|
|
379
|
+
if self.is_finalized:
|
|
380
|
+
raise IngestionConflict(
|
|
381
|
+
"INGESTION_FINALIZED",
|
|
382
|
+
"ingestion_manifest.state",
|
|
383
|
+
"cannot append to a finalized run",
|
|
384
|
+
)
|
|
385
|
+
if chunk.source_pin_sha256 != self.source_pin.digest:
|
|
386
|
+
raise IngestionConflict(
|
|
387
|
+
"INGESTION_SOURCE_DRIFT",
|
|
388
|
+
"chunk_ref.source_pin_sha256",
|
|
389
|
+
"does not equal the manifest source pin",
|
|
390
|
+
)
|
|
391
|
+
if chunk.ordinal != len(self.chunks):
|
|
392
|
+
raise IngestionConflict(
|
|
393
|
+
"INGESTION_ORDINAL_FENCE",
|
|
394
|
+
"chunk_ref.ordinal",
|
|
395
|
+
"must equal the next gap-free manifest ordinal",
|
|
396
|
+
)
|
|
397
|
+
if len(self.chunks) >= MAX_MANIFEST_CHUNKS:
|
|
398
|
+
raise IngestionConflict(
|
|
399
|
+
"INGESTION_CHUNK_LIMIT", "ingestion_manifest.chunks", "cannot accept another chunk"
|
|
400
|
+
)
|
|
401
|
+
return replace(
|
|
402
|
+
self,
|
|
403
|
+
chunks=(*self.chunks, chunk),
|
|
404
|
+
previous_manifest_sha256=self.digest,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def finalize(self) -> Self:
|
|
408
|
+
if self.is_finalized:
|
|
409
|
+
return self
|
|
410
|
+
return replace(
|
|
411
|
+
self,
|
|
412
|
+
state="finalized",
|
|
413
|
+
previous_manifest_sha256=self.digest,
|
|
414
|
+
merkle_root_sha256=merkle_root_sha256(self.chunks),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def is_direct_successor_of(self, predecessor_digest: str) -> bool:
|
|
418
|
+
return self.previous_manifest_sha256 == predecessor_digest
|
|
419
|
+
|
|
420
|
+
def to_dict(self) -> dict[str, Any]:
|
|
421
|
+
return {
|
|
422
|
+
"schema_version": self.schema_version,
|
|
423
|
+
"run_id": self.run_id,
|
|
424
|
+
"source_pin": self.source_pin.to_dict(),
|
|
425
|
+
"chunks": [chunk.to_dict() for chunk in self.chunks],
|
|
426
|
+
"state": self.state,
|
|
427
|
+
"previous_manifest_sha256": self.previous_manifest_sha256,
|
|
428
|
+
"merkle_root_sha256": self.merkle_root_sha256,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
@classmethod
|
|
432
|
+
def from_dict(cls, value: object) -> Self:
|
|
433
|
+
document = _require_exact_mapping(
|
|
434
|
+
value,
|
|
435
|
+
frozenset(
|
|
436
|
+
{
|
|
437
|
+
"schema_version",
|
|
438
|
+
"run_id",
|
|
439
|
+
"source_pin",
|
|
440
|
+
"chunks",
|
|
441
|
+
"state",
|
|
442
|
+
"previous_manifest_sha256",
|
|
443
|
+
"merkle_root_sha256",
|
|
444
|
+
}
|
|
445
|
+
),
|
|
446
|
+
"ingestion_manifest",
|
|
447
|
+
)
|
|
448
|
+
raw_chunks = document["chunks"]
|
|
449
|
+
if not isinstance(raw_chunks, list):
|
|
450
|
+
raise IngestionError(
|
|
451
|
+
"INGESTION_DOCUMENT", "ingestion_manifest.chunks", "must be a list"
|
|
452
|
+
)
|
|
453
|
+
return cls(
|
|
454
|
+
schema_version=document["schema_version"],
|
|
455
|
+
run_id=document["run_id"],
|
|
456
|
+
source_pin=SourceVersionPinV1.from_dict(document["source_pin"]),
|
|
457
|
+
chunks=tuple(ChunkRef.from_dict(item) for item in raw_chunks),
|
|
458
|
+
state=document["state"],
|
|
459
|
+
previous_manifest_sha256=document["previous_manifest_sha256"],
|
|
460
|
+
merkle_root_sha256=document["merkle_root_sha256"],
|
|
461
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Deterministic crash simulation for ingestion durability tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FaultInjected(RuntimeError):
|
|
11
|
+
"""Raised exactly at a configured durability checkpoint."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, checkpoint: str, occurrence: int) -> None:
|
|
14
|
+
self.checkpoint = checkpoint
|
|
15
|
+
self.occurrence = occurrence
|
|
16
|
+
super().__init__(f"fault injected at {checkpoint!r} occurrence {occurrence}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DeterministicFaultInjector:
|
|
21
|
+
"""Fail a named checkpoint on a configured occurrence, once per injector.
|
|
22
|
+
|
|
23
|
+
It is intentionally stateful and explicit rather than probabilistic so every crash
|
|
24
|
+
window can be regression-tested and replayed exactly.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
fail_on: Mapping[str, int] = field(default_factory=dict)
|
|
28
|
+
occurrences: Counter[str] = field(default_factory=Counter, init=False)
|
|
29
|
+
_fired: set[str] = field(default_factory=set, init=False)
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
for checkpoint, occurrence in self.fail_on.items():
|
|
33
|
+
if not isinstance(checkpoint, str) or not checkpoint:
|
|
34
|
+
raise ValueError("fault checkpoint must be a non-empty string")
|
|
35
|
+
if isinstance(occurrence, bool) or not isinstance(occurrence, int) or occurrence <= 0:
|
|
36
|
+
raise ValueError("fault occurrence must be a positive integer")
|
|
37
|
+
|
|
38
|
+
def checkpoint(self, name: str) -> None:
|
|
39
|
+
self.occurrences[name] += 1
|
|
40
|
+
if self.fail_on.get(name) == self.occurrences[name] and name not in self._fired:
|
|
41
|
+
self._fired.add(name)
|
|
42
|
+
raise FaultInjected(name, self.occurrences[name])
|