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,524 @@
|
|
|
1
|
+
"""Bounded stream-batch adapter with durable replay-safe watermark evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import fcntl
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Protocol
|
|
12
|
+
|
|
13
|
+
from mostlyright.data_harness.acquisition.http import seal_content_addressed_snapshot
|
|
14
|
+
from mostlyright.data_harness.acquisition.sandbox import CrawlerSandbox
|
|
15
|
+
from mostlyright.data_harness.canonical import (
|
|
16
|
+
MAX_CANONICAL_MEMBERS,
|
|
17
|
+
CanonicalJSONError,
|
|
18
|
+
canonical_json_bytes,
|
|
19
|
+
canonical_sha256,
|
|
20
|
+
parse_json,
|
|
21
|
+
sha256_bytes,
|
|
22
|
+
)
|
|
23
|
+
from mostlyright.data_harness.sources._adapter_steps import (
|
|
24
|
+
_authorize_candidate,
|
|
25
|
+
_output,
|
|
26
|
+
_parse_limits,
|
|
27
|
+
_require_sandbox_binding,
|
|
28
|
+
)
|
|
29
|
+
from mostlyright.data_harness.sources.adapters import AdapterGovernance
|
|
30
|
+
from mostlyright.data_harness.sources.contracts import (
|
|
31
|
+
AcquisitionRequest,
|
|
32
|
+
AdapterDescriptor,
|
|
33
|
+
SourceContractError,
|
|
34
|
+
)
|
|
35
|
+
from mostlyright.data_harness.sources.governance import (
|
|
36
|
+
classify_before_persistence,
|
|
37
|
+
require_admissible,
|
|
38
|
+
)
|
|
39
|
+
from mostlyright.data_harness.sources.registry import AcquisitionContext, AcquisitionOutput
|
|
40
|
+
|
|
41
|
+
_STATE_VERSION = "harness-stream-watermark.v1"
|
|
42
|
+
# The exact coordinate a Recipe pins and a registry resolves. Named once here so a caller
|
|
43
|
+
# that has to recognise a stream source reads the adapter's own identity rather than
|
|
44
|
+
# repeating the string.
|
|
45
|
+
STREAM_BATCH_ADAPTER_ID = "user.stream-batch"
|
|
46
|
+
STREAM_BATCH_ADAPTER_VERSION = "1.0.0"
|
|
47
|
+
# The one grammar an event name has to be in to enter the ledger. Named once, here, because the
|
|
48
|
+
# Recorder that assigns names and the ledger that admits them must not drift apart.
|
|
49
|
+
EVENT_ID_GRAMMAR = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$")
|
|
50
|
+
_EVENT_ID = EVENT_ID_GRAMMAR
|
|
51
|
+
MAX_BATCH_EVENTS = 100_000
|
|
52
|
+
MAX_STATE_BYTES = 64 * 1024 * 1024
|
|
53
|
+
# The state object itself contributes eight canonical members. Keeping the ledger
|
|
54
|
+
# inside the same parser budget is required in addition to the serialized byte cap.
|
|
55
|
+
MAX_SEEN_EVENTS = MAX_CANONICAL_MEMBERS - 8
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class StreamBatch:
|
|
60
|
+
"""One bounded provider batch from a StreamGateway, with the watermark it advances to."""
|
|
61
|
+
|
|
62
|
+
query_digest: str
|
|
63
|
+
previous_watermark: str | None
|
|
64
|
+
next_watermark: str
|
|
65
|
+
provider_event_ids: tuple[str, ...]
|
|
66
|
+
content: bytes
|
|
67
|
+
content_sha256: str
|
|
68
|
+
media_type: str
|
|
69
|
+
data_format: str
|
|
70
|
+
filename: str
|
|
71
|
+
observed_at: str
|
|
72
|
+
available_at: str
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
if sha256_bytes(self.content) != self.content_sha256:
|
|
76
|
+
raise SourceContractError(
|
|
77
|
+
"STREAM_CONTENT_DIGEST",
|
|
78
|
+
"stream_batch.content_sha256",
|
|
79
|
+
"does not match exact batch bytes",
|
|
80
|
+
)
|
|
81
|
+
if not self.provider_event_ids or len(self.provider_event_ids) > MAX_BATCH_EVENTS:
|
|
82
|
+
raise SourceContractError(
|
|
83
|
+
"STREAM_EVENT_LIMIT",
|
|
84
|
+
"stream_batch.provider_event_ids",
|
|
85
|
+
"must be non-empty and bounded",
|
|
86
|
+
)
|
|
87
|
+
if len(set(self.provider_event_ids)) != len(self.provider_event_ids) or any(
|
|
88
|
+
_EVENT_ID.fullmatch(value) is None for value in self.provider_event_ids
|
|
89
|
+
):
|
|
90
|
+
raise SourceContractError(
|
|
91
|
+
"STREAM_EVENT_ID",
|
|
92
|
+
"stream_batch.provider_event_ids",
|
|
93
|
+
"provider event IDs must be valid and unique",
|
|
94
|
+
)
|
|
95
|
+
if not self.next_watermark or self.next_watermark == self.previous_watermark:
|
|
96
|
+
raise SourceContractError(
|
|
97
|
+
"STREAM_WATERMARK",
|
|
98
|
+
"stream_batch.next_watermark",
|
|
99
|
+
"must advance to a non-empty different value",
|
|
100
|
+
)
|
|
101
|
+
for name, value in (
|
|
102
|
+
("previous_watermark", self.previous_watermark),
|
|
103
|
+
("next_watermark", self.next_watermark),
|
|
104
|
+
):
|
|
105
|
+
if value is not None and (
|
|
106
|
+
not isinstance(value, str)
|
|
107
|
+
or not value
|
|
108
|
+
or len(value) > 512
|
|
109
|
+
or any(0xD800 <= ord(char) <= 0xDFFF for char in value)
|
|
110
|
+
):
|
|
111
|
+
raise SourceContractError(
|
|
112
|
+
"STREAM_WATERMARK",
|
|
113
|
+
f"stream_batch.{name}",
|
|
114
|
+
"must be a bounded valid string or null",
|
|
115
|
+
)
|
|
116
|
+
if self.data_format not in {"json", "ndjson"}:
|
|
117
|
+
raise SourceContractError(
|
|
118
|
+
"STREAM_FORMAT",
|
|
119
|
+
"stream_batch.data_format",
|
|
120
|
+
"must be json or ndjson",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class StreamGateway(Protocol):
|
|
125
|
+
def read_batch(self, request: AcquisitionRequest) -> StreamBatch:
|
|
126
|
+
"""Return one coordinator-bounded provider batch."""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class StreamState:
|
|
131
|
+
"""Durable per-source watermark and admitted event-ID ledger; StreamWatermarkStore writes it."""
|
|
132
|
+
|
|
133
|
+
source_id: str
|
|
134
|
+
watermark: str
|
|
135
|
+
seen_event_ids: tuple[str, ...]
|
|
136
|
+
last_batch_content_sha256: str
|
|
137
|
+
last_batch_event_ids_digest: str
|
|
138
|
+
last_query_digest: str
|
|
139
|
+
updated_at: str
|
|
140
|
+
schema_version: str = _STATE_VERSION
|
|
141
|
+
|
|
142
|
+
def __post_init__(self) -> None:
|
|
143
|
+
if self.schema_version != _STATE_VERSION:
|
|
144
|
+
raise SourceContractError(
|
|
145
|
+
"STREAM_STATE_VERSION",
|
|
146
|
+
"stream_state.schema_version",
|
|
147
|
+
"is unsupported",
|
|
148
|
+
)
|
|
149
|
+
if not re.fullmatch(r"[a-z][a-z0-9._-]{0,127}", self.source_id):
|
|
150
|
+
raise SourceContractError("STREAM_STATE", "stream_state.source_id", "is invalid")
|
|
151
|
+
if not self.watermark or len(self.watermark) > 512:
|
|
152
|
+
raise SourceContractError("STREAM_STATE", "stream_state.watermark", "is invalid")
|
|
153
|
+
if (
|
|
154
|
+
len(self.seen_event_ids) > MAX_SEEN_EVENTS
|
|
155
|
+
or len(set(self.seen_event_ids)) != len(self.seen_event_ids)
|
|
156
|
+
or tuple(sorted(self.seen_event_ids)) != self.seen_event_ids
|
|
157
|
+
or any(_EVENT_ID.fullmatch(value) is None for value in self.seen_event_ids)
|
|
158
|
+
):
|
|
159
|
+
raise SourceContractError(
|
|
160
|
+
"STREAM_STATE",
|
|
161
|
+
"stream_state.seen_event_ids",
|
|
162
|
+
"must be a bounded canonical unique event-ID ledger",
|
|
163
|
+
)
|
|
164
|
+
for name, value in (
|
|
165
|
+
("last_batch_content_sha256", self.last_batch_content_sha256),
|
|
166
|
+
("last_batch_event_ids_digest", self.last_batch_event_ids_digest),
|
|
167
|
+
("last_query_digest", self.last_query_digest),
|
|
168
|
+
):
|
|
169
|
+
if len(value) != 64 or any(char not in "0123456789abcdef" for char in value):
|
|
170
|
+
raise SourceContractError("STREAM_STATE", f"stream_state.{name}", "is invalid")
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def digest(self) -> str:
|
|
174
|
+
return canonical_sha256(self.to_dict())
|
|
175
|
+
|
|
176
|
+
def to_dict(self) -> dict[str, Any]:
|
|
177
|
+
return {
|
|
178
|
+
"schema_version": self.schema_version,
|
|
179
|
+
"source_id": self.source_id,
|
|
180
|
+
"watermark": self.watermark,
|
|
181
|
+
"seen_event_ids": list(self.seen_event_ids),
|
|
182
|
+
"last_batch_content_sha256": self.last_batch_content_sha256,
|
|
183
|
+
"last_batch_event_ids_digest": self.last_batch_event_ids_digest,
|
|
184
|
+
"last_query_digest": self.last_query_digest,
|
|
185
|
+
"updated_at": self.updated_at,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True)
|
|
190
|
+
class StreamWatermarkStore:
|
|
191
|
+
root: Path
|
|
192
|
+
|
|
193
|
+
def __post_init__(self) -> None:
|
|
194
|
+
if not self.root.is_absolute() or self.root.is_symlink() or not self.root.is_dir():
|
|
195
|
+
raise SourceContractError(
|
|
196
|
+
"STREAM_STATE_ROOT",
|
|
197
|
+
"watermark_store.root",
|
|
198
|
+
"must be an existing absolute non-symlink directory",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def commit(
|
|
202
|
+
self,
|
|
203
|
+
*,
|
|
204
|
+
source_id: str,
|
|
205
|
+
batch: StreamBatch,
|
|
206
|
+
updated_at: str,
|
|
207
|
+
) -> StreamState:
|
|
208
|
+
if not re.fullmatch(r"[a-z][a-z0-9._-]{0,127}", source_id):
|
|
209
|
+
raise SourceContractError("STREAM_SOURCE_ID", "source_id", "is invalid")
|
|
210
|
+
root_fd = os.open(
|
|
211
|
+
self.root,
|
|
212
|
+
os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
|
|
213
|
+
)
|
|
214
|
+
state_name = f"watermark-{source_id}.json"
|
|
215
|
+
try:
|
|
216
|
+
fcntl.flock(root_fd, fcntl.LOCK_EX)
|
|
217
|
+
current = _load_state(root_fd, state_name)
|
|
218
|
+
event_digest = canonical_sha256(list(batch.provider_event_ids))
|
|
219
|
+
if current is not None and (
|
|
220
|
+
current.watermark == batch.next_watermark
|
|
221
|
+
and current.last_batch_content_sha256 == batch.content_sha256
|
|
222
|
+
and current.last_batch_event_ids_digest == event_digest
|
|
223
|
+
and current.last_query_digest == batch.query_digest
|
|
224
|
+
):
|
|
225
|
+
return current
|
|
226
|
+
current_watermark = current.watermark if current is not None else None
|
|
227
|
+
if batch.previous_watermark != current_watermark:
|
|
228
|
+
raise SourceContractError(
|
|
229
|
+
"STREAM_WATERMARK_CONFLICT",
|
|
230
|
+
"stream_batch.previous_watermark",
|
|
231
|
+
"does not match durable coordinator state",
|
|
232
|
+
)
|
|
233
|
+
seen = set(current.seen_event_ids if current is not None else ())
|
|
234
|
+
duplicates = sorted(seen & set(batch.provider_event_ids))
|
|
235
|
+
if duplicates:
|
|
236
|
+
raise SourceContractError(
|
|
237
|
+
"STREAM_EVENT_REPLAY",
|
|
238
|
+
"stream_batch.provider_event_ids",
|
|
239
|
+
f"provider event IDs were already admitted: {duplicates[:10]}",
|
|
240
|
+
)
|
|
241
|
+
combined = tuple(sorted((*seen, *batch.provider_event_ids)))
|
|
242
|
+
if len(combined) > MAX_SEEN_EVENTS:
|
|
243
|
+
raise SourceContractError(
|
|
244
|
+
"STREAM_SEEN_EVENT_LIMIT",
|
|
245
|
+
"stream_state.seen_event_ids",
|
|
246
|
+
"durable event-ID ledger reached its V1 bound",
|
|
247
|
+
)
|
|
248
|
+
successor = StreamState(
|
|
249
|
+
source_id=source_id,
|
|
250
|
+
watermark=batch.next_watermark,
|
|
251
|
+
seen_event_ids=combined,
|
|
252
|
+
last_batch_content_sha256=batch.content_sha256,
|
|
253
|
+
last_batch_event_ids_digest=event_digest,
|
|
254
|
+
last_query_digest=batch.query_digest,
|
|
255
|
+
updated_at=updated_at,
|
|
256
|
+
)
|
|
257
|
+
_atomic_state_replace(root_fd, state_name, successor)
|
|
258
|
+
return successor
|
|
259
|
+
finally:
|
|
260
|
+
with contextlib.suppress(OSError):
|
|
261
|
+
fcntl.flock(root_fd, fcntl.LOCK_UN)
|
|
262
|
+
os.close(root_fd)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _stream_descriptor() -> AdapterDescriptor:
|
|
266
|
+
return AdapterDescriptor(
|
|
267
|
+
adapter_id=STREAM_BATCH_ADAPTER_ID,
|
|
268
|
+
adapter_version=STREAM_BATCH_ADAPTER_VERSION,
|
|
269
|
+
source_class="stream",
|
|
270
|
+
data_formats=("json", "ndjson"),
|
|
271
|
+
capabilities=("snapshot", "stream", "refresh"),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass(frozen=True)
|
|
276
|
+
class StreamBatchAdapter:
|
|
277
|
+
gateway: StreamGateway
|
|
278
|
+
sandbox: CrawlerSandbox
|
|
279
|
+
governance: AdapterGovernance
|
|
280
|
+
watermark_store: StreamWatermarkStore
|
|
281
|
+
descriptor: AdapterDescriptor = field(default_factory=_stream_descriptor)
|
|
282
|
+
|
|
283
|
+
def acquire(
|
|
284
|
+
self,
|
|
285
|
+
request: AcquisitionRequest,
|
|
286
|
+
context: AcquisitionContext,
|
|
287
|
+
) -> AcquisitionOutput:
|
|
288
|
+
if (request.adapter_id, request.adapter_version) != (
|
|
289
|
+
self.descriptor.adapter_id,
|
|
290
|
+
self.descriptor.adapter_version,
|
|
291
|
+
):
|
|
292
|
+
raise SourceContractError(
|
|
293
|
+
"ADAPTER_BINDING",
|
|
294
|
+
"request",
|
|
295
|
+
"request does not bind the exact stream adapter",
|
|
296
|
+
)
|
|
297
|
+
if request.credential_reference_id is not None:
|
|
298
|
+
raise SourceContractError(
|
|
299
|
+
"STREAM_CREDENTIAL_FORBIDDEN",
|
|
300
|
+
"request.credential_reference_id",
|
|
301
|
+
"stream gateway credentials remain in the trusted gateway",
|
|
302
|
+
)
|
|
303
|
+
_stream_query(request.query)
|
|
304
|
+
_authorize_candidate(request, self.governance)
|
|
305
|
+
batch = self.gateway.read_batch(request)
|
|
306
|
+
if batch.query_digest != request.query_digest:
|
|
307
|
+
raise SourceContractError(
|
|
308
|
+
"STREAM_QUERY_BINDING",
|
|
309
|
+
"stream_batch.query_digest",
|
|
310
|
+
"batch does not bind the exact canonical query",
|
|
311
|
+
)
|
|
312
|
+
if (
|
|
313
|
+
request.query["expected_watermark"] != batch.previous_watermark
|
|
314
|
+
or request.query["data_format"] != batch.data_format
|
|
315
|
+
or request.query["filename"] != batch.filename
|
|
316
|
+
):
|
|
317
|
+
raise SourceContractError(
|
|
318
|
+
"STREAM_QUERY_BINDING",
|
|
319
|
+
"stream_batch",
|
|
320
|
+
"batch watermark, format, or filename differs from the exact request",
|
|
321
|
+
)
|
|
322
|
+
if (
|
|
323
|
+
batch.observed_at != self.governance.observation.observed_at
|
|
324
|
+
or batch.available_at != self.governance.observation.available_at
|
|
325
|
+
):
|
|
326
|
+
raise SourceContractError(
|
|
327
|
+
"STREAM_TEMPORAL_BINDING",
|
|
328
|
+
"stream_batch",
|
|
329
|
+
"batch times differ from the recorded observation evidence",
|
|
330
|
+
)
|
|
331
|
+
if batch.data_format not in self.descriptor.data_formats:
|
|
332
|
+
raise SourceContractError(
|
|
333
|
+
"STREAM_FORMAT",
|
|
334
|
+
"stream_batch.data_format",
|
|
335
|
+
"batch format is not supported by the adapter",
|
|
336
|
+
)
|
|
337
|
+
classification = classify_before_persistence(
|
|
338
|
+
batch.content,
|
|
339
|
+
declared_classification=self.governance.declared_classification,
|
|
340
|
+
lawful_basis_declared=self.governance.lawful_basis_declared,
|
|
341
|
+
)
|
|
342
|
+
require_admissible(classification)
|
|
343
|
+
parsed = self.sandbox.parse(
|
|
344
|
+
request_id=request.request_id,
|
|
345
|
+
content=batch.content,
|
|
346
|
+
data_format=batch.data_format,
|
|
347
|
+
media_type=batch.media_type,
|
|
348
|
+
filename=batch.filename,
|
|
349
|
+
limits=_parse_limits(context),
|
|
350
|
+
)
|
|
351
|
+
_require_sandbox_binding(parsed.policy_digest, context)
|
|
352
|
+
if parsed.parsed is None or parsed.parsed.input_sha256 != batch.content_sha256:
|
|
353
|
+
raise SourceContractError(
|
|
354
|
+
"STREAM_CONTENT_BINDING",
|
|
355
|
+
"sandbox.parsed",
|
|
356
|
+
"parsed result does not bind the exact stream bytes",
|
|
357
|
+
)
|
|
358
|
+
snapshot_path = seal_content_addressed_snapshot(
|
|
359
|
+
context.snapshot_root,
|
|
360
|
+
content=batch.content,
|
|
361
|
+
suffix=batch.data_format,
|
|
362
|
+
)
|
|
363
|
+
state = self.watermark_store.commit(
|
|
364
|
+
source_id=request.source_id,
|
|
365
|
+
batch=batch,
|
|
366
|
+
updated_at=request.requested_at,
|
|
367
|
+
)
|
|
368
|
+
content_urn = f"urn:sha256:{batch.content_sha256}"
|
|
369
|
+
return _output(
|
|
370
|
+
request=request,
|
|
371
|
+
descriptor=self.descriptor,
|
|
372
|
+
governance=self.governance,
|
|
373
|
+
snapshot_path=snapshot_path,
|
|
374
|
+
content=batch.content,
|
|
375
|
+
data_format=batch.data_format,
|
|
376
|
+
media_type=batch.media_type,
|
|
377
|
+
source_uri=content_urn,
|
|
378
|
+
final_uri=content_urn,
|
|
379
|
+
classification=classification.classification,
|
|
380
|
+
transport_evidence_digest=state.digest,
|
|
381
|
+
parsed_schema_digest=parsed.parsed.schema_digest,
|
|
382
|
+
next_watermark=state.watermark,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _stream_query(value: Any) -> None:
|
|
387
|
+
if not isinstance(value, dict) or set(value) != {
|
|
388
|
+
"stream_id",
|
|
389
|
+
"data_format",
|
|
390
|
+
"filename",
|
|
391
|
+
"expected_watermark",
|
|
392
|
+
}:
|
|
393
|
+
raise SourceContractError(
|
|
394
|
+
"STREAM_QUERY",
|
|
395
|
+
"request.query",
|
|
396
|
+
"must contain the exact stream batch query fields",
|
|
397
|
+
)
|
|
398
|
+
if value["data_format"] not in {"json", "ndjson"}:
|
|
399
|
+
raise SourceContractError(
|
|
400
|
+
"STREAM_QUERY",
|
|
401
|
+
"request.query.data_format",
|
|
402
|
+
"must be json or ndjson",
|
|
403
|
+
)
|
|
404
|
+
if value["expected_watermark"] is not None and not isinstance(value["expected_watermark"], str):
|
|
405
|
+
raise SourceContractError(
|
|
406
|
+
"STREAM_QUERY",
|
|
407
|
+
"request.query.expected_watermark",
|
|
408
|
+
"must be a string or null",
|
|
409
|
+
)
|
|
410
|
+
for name in ("stream_id", "filename"):
|
|
411
|
+
if not isinstance(value[name], str) or not value[name] or len(value[name]) > 512:
|
|
412
|
+
raise SourceContractError(
|
|
413
|
+
"STREAM_QUERY",
|
|
414
|
+
f"request.query.{name}",
|
|
415
|
+
"must be a non-empty bounded string",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _load_state(root_fd: int, name: str) -> StreamState | None:
|
|
420
|
+
try:
|
|
421
|
+
descriptor = os.open(
|
|
422
|
+
name,
|
|
423
|
+
os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0),
|
|
424
|
+
dir_fd=root_fd,
|
|
425
|
+
)
|
|
426
|
+
except FileNotFoundError:
|
|
427
|
+
return None
|
|
428
|
+
try:
|
|
429
|
+
raw = _read_bounded(descriptor, MAX_STATE_BYTES)
|
|
430
|
+
finally:
|
|
431
|
+
os.close(descriptor)
|
|
432
|
+
value = parse_json(raw)
|
|
433
|
+
if not isinstance(value, dict) or set(value) != {
|
|
434
|
+
"schema_version",
|
|
435
|
+
"source_id",
|
|
436
|
+
"watermark",
|
|
437
|
+
"seen_event_ids",
|
|
438
|
+
"last_batch_content_sha256",
|
|
439
|
+
"last_batch_event_ids_digest",
|
|
440
|
+
"last_query_digest",
|
|
441
|
+
"updated_at",
|
|
442
|
+
}:
|
|
443
|
+
raise SourceContractError("STREAM_STATE", "watermark_state", "shape is invalid")
|
|
444
|
+
if value["schema_version"] != _STATE_VERSION or not isinstance(value["seen_event_ids"], list):
|
|
445
|
+
raise SourceContractError("STREAM_STATE", "watermark_state", "version is invalid")
|
|
446
|
+
return StreamState(
|
|
447
|
+
source_id=value["source_id"],
|
|
448
|
+
watermark=value["watermark"],
|
|
449
|
+
seen_event_ids=tuple(value["seen_event_ids"]),
|
|
450
|
+
last_batch_content_sha256=value["last_batch_content_sha256"],
|
|
451
|
+
last_batch_event_ids_digest=value["last_batch_event_ids_digest"],
|
|
452
|
+
last_query_digest=value["last_query_digest"],
|
|
453
|
+
updated_at=value["updated_at"],
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _atomic_state_replace(root_fd: int, name: str, state: StreamState) -> None:
|
|
458
|
+
payload = _serialize_state(state)
|
|
459
|
+
temporary = f".{name}.{os.getpid()}.tmp"
|
|
460
|
+
with contextlib.suppress(FileNotFoundError):
|
|
461
|
+
os.unlink(temporary, dir_fd=root_fd)
|
|
462
|
+
descriptor = os.open(
|
|
463
|
+
temporary,
|
|
464
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
|
|
465
|
+
0o600,
|
|
466
|
+
dir_fd=root_fd,
|
|
467
|
+
)
|
|
468
|
+
try:
|
|
469
|
+
view = memoryview(payload)
|
|
470
|
+
written = 0
|
|
471
|
+
while written < len(view):
|
|
472
|
+
count = os.write(descriptor, view[written:])
|
|
473
|
+
if count <= 0:
|
|
474
|
+
raise SourceContractError(
|
|
475
|
+
"STREAM_STATE_WRITE",
|
|
476
|
+
"watermark_state",
|
|
477
|
+
"write made no progress",
|
|
478
|
+
)
|
|
479
|
+
written += count
|
|
480
|
+
os.fsync(descriptor)
|
|
481
|
+
finally:
|
|
482
|
+
os.close(descriptor)
|
|
483
|
+
try:
|
|
484
|
+
os.replace(temporary, name, src_dir_fd=root_fd, dst_dir_fd=root_fd)
|
|
485
|
+
os.fsync(root_fd)
|
|
486
|
+
finally:
|
|
487
|
+
with contextlib.suppress(FileNotFoundError):
|
|
488
|
+
os.unlink(temporary, dir_fd=root_fd)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _serialize_state(state: StreamState) -> bytes:
|
|
492
|
+
try:
|
|
493
|
+
payload = canonical_json_bytes(state.to_dict())
|
|
494
|
+
except CanonicalJSONError as error:
|
|
495
|
+
raise SourceContractError(
|
|
496
|
+
"STREAM_STATE_LIMIT",
|
|
497
|
+
"stream_state.seen_event_ids",
|
|
498
|
+
"state exceeds the canonical parser member budget",
|
|
499
|
+
) from error
|
|
500
|
+
if len(payload) > MAX_STATE_BYTES:
|
|
501
|
+
raise SourceContractError(
|
|
502
|
+
"STREAM_STATE_LIMIT",
|
|
503
|
+
"watermark_state",
|
|
504
|
+
f"canonical state exceeds its {MAX_STATE_BYTES}-byte durable loader budget",
|
|
505
|
+
)
|
|
506
|
+
return payload
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _read_bounded(descriptor: int, maximum: int) -> bytes:
|
|
510
|
+
chunks: list[bytes] = []
|
|
511
|
+
total = 0
|
|
512
|
+
while total <= maximum:
|
|
513
|
+
chunk = os.read(descriptor, min(64 * 1024, maximum + 1 - total))
|
|
514
|
+
if not chunk:
|
|
515
|
+
break
|
|
516
|
+
chunks.append(chunk)
|
|
517
|
+
total += len(chunk)
|
|
518
|
+
if total > maximum:
|
|
519
|
+
raise SourceContractError(
|
|
520
|
+
"STREAM_STATE_LIMIT",
|
|
521
|
+
"watermark_state",
|
|
522
|
+
"state exceeds its byte budget",
|
|
523
|
+
)
|
|
524
|
+
return b"".join(chunks)
|