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,534 @@
|
|
|
1
|
+
"""Unsealed run-progress events for the live hosted timeline (ADR 0021).
|
|
2
|
+
|
|
3
|
+
WHAT THIS IS NOT. This is not the Build event vocabulary. `events` holds that one, and it holds an
|
|
4
|
+
invariant this module deliberately does not: every BUILD name there must be re-derivable from
|
|
5
|
+
sealed bytes by `events.project_sealed_run`, which is why a time-throttled heartbeat is refused
|
|
6
|
+
there and why the module says so in its own docstring. Widening that vocabulary to move a progress
|
|
7
|
+
bar would trade a proof for a spinner. So progress got its own vocabulary instead, modelled on the
|
|
8
|
+
one exception that vocabulary already carries -- the post-seal notebook sidecar, three names that
|
|
9
|
+
narrate an unsealed convenience write and are separated so a consumer cannot mistake them for Build
|
|
10
|
+
evidence.
|
|
11
|
+
|
|
12
|
+
THE BOUNDARY, stated as the five things a reader has to be able to rely on:
|
|
13
|
+
|
|
14
|
+
* **Disjoint names.** `PROGRESS_EVENT_NAMES` shares not one member with `events.EVENT_NAMES` or
|
|
15
|
+
`events.RESERVED_EVENT_NAMES`. Every progress name that mirrors a Build name carries the
|
|
16
|
+
``build_`` prefix precisely so the two can never be confused in a log line, a dashboard row or a
|
|
17
|
+
grep. A test asserts the disjointness rather than trusting the prefix.
|
|
18
|
+
* **Blind to projection.** `events.project_sealed_run` has no branch for any name here and must
|
|
19
|
+
never grow one. A projection of a sealed Build is the same list of records whether or not a
|
|
20
|
+
progress stream existed, because a progress record is not derived from sealed bytes and could
|
|
21
|
+
not be re-derived from them afterwards. A test projects a real Build and asserts the projection
|
|
22
|
+
names are exactly the sealed vocabulary.
|
|
23
|
+
* **Outside attestation.** No progress record reaches a candidate member, a manifest, a digest that
|
|
24
|
+
anything is verified against, or a receipt. The only digest a progress record produces is
|
|
25
|
+
`progress_payload_digest`, which is a content address for the record's OWN payload on the wire --
|
|
26
|
+
it attests the payload has not been rewritten in transit, and it attests nothing about the Build.
|
|
27
|
+
A verifier never reads this stream, and the verifier lane emits nothing here: it holds a signing
|
|
28
|
+
key and stays job-per-run (ADR 0021).
|
|
29
|
+
* **Never load-bearing.** Emission is best effort by contract. `emit` with no sink armed is a
|
|
30
|
+
ContextVar read and a return. A sink that raises is caught by the caller of record and the run
|
|
31
|
+
proceeds. A run that emitted zero progress events and a run that emitted all of them produce
|
|
32
|
+
byte-identical Builds.
|
|
33
|
+
* **Bounded, not chatty.** The vocabulary covers stage boundaries and per-source, per-artifact and
|
|
34
|
+
per-stage milestones. It has no per-row, per-byte, per-chunk or wall-clock heartbeat name, and
|
|
35
|
+
`MAX_PROGRESS_EVENTS_PER_ATTEMPT` is a hard per-attempt ceiling on top of that, so the stream
|
|
36
|
+
stays bounded by the shape of one run even if a caller loops.
|
|
37
|
+
|
|
38
|
+
THE WIRE. Studio's V3 producer append path (`POST /internal/v3/producer/attempts/{id}/events`,
|
|
39
|
+
`ProducerEventCommand`) carries `event_type` from a CLOSED six-value enum plus a `payload_digest`.
|
|
40
|
+
`STUDIO_EVENT_TYPE` is therefore part of this vocabulary and not an implementation detail: it is
|
|
41
|
+
the declared, total mapping from a progress name to the contract enum value it travels as, and it
|
|
42
|
+
lands inside `UNSEALED_STUDIO_EVENT_TYPES` only. The two values the hosted producer's sealed bulk
|
|
43
|
+
appends already own are reserved against exactly this vocabulary, so a consumer reading the wire
|
|
44
|
+
can still tell a proof from a progress tick with no new field.
|
|
45
|
+
|
|
46
|
+
The wire ALSO carries the payload body now, in the additive optional `payload` object Studio added
|
|
47
|
+
beside `payload_digest`. Studio stores it as the UNSEALED adjunct of the log record -- beside the
|
|
48
|
+
sealed record, never inside it, out of every sealed-bytes projection and every attestation -- and
|
|
49
|
+
refuses the append unless the payload's canonical-JSON SHA-256 equals `payload_digest`, so the
|
|
50
|
+
digest still binds exactly the bytes the worker claims. `progress_payload_and_digest` is what the
|
|
51
|
+
emitter calls: it returns the body and its content address from ONE canonicalization, so the two
|
|
52
|
+
cannot disagree at the call site. Sending the body is what makes the fact values below reach a
|
|
53
|
+
timeline instead of dying in the worker; a consumer that reads only `payload_digest` sees exactly
|
|
54
|
+
what it saw before.
|
|
55
|
+
|
|
56
|
+
A PROBE IS NOT AN ACQUISITION, which is why the pair below is its own two names rather than the
|
|
57
|
+
`source_acquire_*` ones a session worker first borrowed. Those two carry `index` and `total`,
|
|
58
|
+
because a Build acquires source i of a set the recipe fixes and a progress rail can render the
|
|
59
|
+
fraction. A session's probes are a conversation: how many there will be is not knowable when any of
|
|
60
|
+
them runs, so a worker reusing that shape had to send `index == total` and every probe read "1 of
|
|
61
|
+
1", "2 of 2", "3 of 3" -- a fraction that is always whole, which is worse than no fraction because
|
|
62
|
+
a rail will draw it. `source_probe_started` and `source_probe_settled` carry Studio's own `ordinal`
|
|
63
|
+
instead: a counter, monotonic and meaningful on its own, and no denominator that would have to be
|
|
64
|
+
invented.
|
|
65
|
+
|
|
66
|
+
TERMINAL RECORDS, and why a failure is a COMPLETION here rather than a diagnostic. A watcher on
|
|
67
|
+
this stream pairs a start with an end; a start with no end is indistinguishable from a probe still
|
|
68
|
+
running, forever. So every terminal outcome of a probe emits `source_probe_settled`, and it travels
|
|
69
|
+
as `source_probe_completed` -- the same contract enum value a successful one travels as -- because
|
|
70
|
+
that enum value means "a probe settled" and a refused probe settled. Routing failures to
|
|
71
|
+
`diagnostic_emitted` instead was the tempting reading of the two names, and it is the wrong one: a
|
|
72
|
+
consumer would then have to know two event types to answer "did this probe finish", and one of them
|
|
73
|
+
is the type an attempt-level narration also uses (`attempt_failed`), so counting probe endings would
|
|
74
|
+
have counted attempt diagnostics too. `diagnostic_emitted` stays what it already was -- narration
|
|
75
|
+
ABOUT a failure -- and the outcome lives in a typed fact on the completion, where a watcher reads it
|
|
76
|
+
without parsing a second stream. See `PROBE_OUTCOMES` for the values and what separates them.
|
|
77
|
+
|
|
78
|
+
THE BRIDGE. `bridge_build_events` watches the sealed feed through `events.observer` and republishes
|
|
79
|
+
a bounded subset of Build records as progress records. It exists because the hosted producer calls
|
|
80
|
+
`execute_recipe` as one opaque step: without it the timeline is silent for the whole build. It is a
|
|
81
|
+
one-way read -- sealed to progress, never the reverse -- with a hand-written translation table
|
|
82
|
+
(`SEALED_BUILD_EVENT_BRIDGE`), a per-event allowlist of the fact keys that cross
|
|
83
|
+
(`BRIDGED_FACT_KEYS`), and a value guard (`_safe_value`) that drops any record carrying a value
|
|
84
|
+
outside integers, booleans and short token strings. The guard is what keeps this side placeless:
|
|
85
|
+
the sealed feed redacts locations in its own writer, downstream of `emit`, so a watcher sees raw
|
|
86
|
+
facts and must do its own refusing.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
from __future__ import annotations
|
|
90
|
+
|
|
91
|
+
import contextlib
|
|
92
|
+
import re
|
|
93
|
+
from collections.abc import Callable, Iterator, Mapping
|
|
94
|
+
from contextvars import ContextVar
|
|
95
|
+
from typing import Any
|
|
96
|
+
|
|
97
|
+
from mostlyright.data_harness import events
|
|
98
|
+
from mostlyright.data_harness.canonical import canonical_json_bytes, sha256_bytes
|
|
99
|
+
|
|
100
|
+
PROGRESS_SCHEMA_VERSION = "mr-progress-events.v1"
|
|
101
|
+
|
|
102
|
+
# A hard per-attempt ceiling, enforced by the emitter rather than by hope. The vocabulary alone
|
|
103
|
+
# already bounds the stream by the shape of one run -- one record per source, per artifact and per
|
|
104
|
+
# stage -- so reaching this number means a caller is looping, and a looping caller must cost the
|
|
105
|
+
# run nothing rather than cost Studio an unbounded append.
|
|
106
|
+
MAX_PROGRESS_EVENTS_PER_ATTEMPT = 512
|
|
107
|
+
|
|
108
|
+
# The ordered producer stages a live timeline draws its skeleton from. `stage_started` and
|
|
109
|
+
# `stage_completed` carry an index into this tuple, so a dashboard can render the whole shape of a
|
|
110
|
+
# run from its first event instead of discovering stages as they arrive.
|
|
111
|
+
PRODUCER_STAGES: tuple[str, ...] = ("acquire", "build", "upload", "submit", "handoff")
|
|
112
|
+
|
|
113
|
+
# The six values Studio's `ProducerEventCommand.event_type` enum admits, spelled here as LITERALS
|
|
114
|
+
# on the `events._RECIPE_MANIFEST_VERSION` precedent: this module imports no generated client, so
|
|
115
|
+
# it stays loadable wherever the vocabulary is needed, and a test asserts the set equals the
|
|
116
|
+
# generated `ContractProducerEventCommandEventType` so the two spellings cannot drift.
|
|
117
|
+
STUDIO_EVENT_TYPES = frozenset(
|
|
118
|
+
{
|
|
119
|
+
"attempt_progress",
|
|
120
|
+
"source_probe_completed",
|
|
121
|
+
"artifact_upload_completed",
|
|
122
|
+
"candidate_build_completed",
|
|
123
|
+
"diagnostic_emitted",
|
|
124
|
+
"run_stage_reported",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Two of those six belong to the hosted producer's SEALED bulk appends and are off limits here.
|
|
129
|
+
# The wire carries no sealed/unsealed flag -- one enum value plus one opaque digest -- so if a
|
|
130
|
+
# progress record travelled as `artifact_upload_completed`, a consumer counting artifacts would see
|
|
131
|
+
# nine completions for eight artifacts and have no way to tell which one was evidence. Reserving
|
|
132
|
+
# these two is what keeps the wire unambiguous without asking Studio for a new field.
|
|
133
|
+
SEALED_STUDIO_EVENT_TYPES = frozenset({"artifact_upload_completed", "candidate_build_completed"})
|
|
134
|
+
UNSEALED_STUDIO_EVENT_TYPES = STUDIO_EVENT_TYPES - SEALED_STUDIO_EVENT_TYPES
|
|
135
|
+
|
|
136
|
+
# The authoritative declaration of what each progress event asserts, in the shape `events`
|
|
137
|
+
# declares its own: a name maps to its exact fact keys, and a producer that drifts is refused
|
|
138
|
+
# rather than emitted. Facts are numbers and short tokens only -- see `_safe_value`.
|
|
139
|
+
#
|
|
140
|
+
# The `build_*` half is written by the bridge and never by hand; the rest is written by the hosted
|
|
141
|
+
# producer. Both halves are one vocabulary because both are unsealed and both travel the same wire.
|
|
142
|
+
PROGRESS_EVENT_FACTS: Mapping[str, tuple[str, ...]] = {
|
|
143
|
+
# -- producer lifecycle ----------------------------------------------------------------
|
|
144
|
+
"attempt_started": ("role", "stage_total"),
|
|
145
|
+
"stage_started": ("stage", "index", "total"),
|
|
146
|
+
"stage_completed": ("stage", "index", "total"),
|
|
147
|
+
"source_acquire_started": ("source_id", "index", "total"),
|
|
148
|
+
"source_acquire_completed": ("source_id", "index", "total", "bytes"),
|
|
149
|
+
"source_probe_started": ("source_id", "probe_kind", "ordinal"),
|
|
150
|
+
"source_probe_settled": (
|
|
151
|
+
"source_id",
|
|
152
|
+
"probe_kind",
|
|
153
|
+
"ordinal",
|
|
154
|
+
"outcome",
|
|
155
|
+
"code",
|
|
156
|
+
"display_code",
|
|
157
|
+
"retry_after_seconds",
|
|
158
|
+
"bytes",
|
|
159
|
+
),
|
|
160
|
+
"artifact_upload_started": ("artifact_id", "kind", "index", "total", "bytes"),
|
|
161
|
+
"artifact_upload_sealed": ("artifact_id", "kind", "index", "total"),
|
|
162
|
+
"candidate_submitted": ("candidate_digest", "artifact_count"),
|
|
163
|
+
"verify_handoff": ("candidate_digest", "admission_id"),
|
|
164
|
+
"attempt_finished": ("outcome",),
|
|
165
|
+
"attempt_failed": ("code",),
|
|
166
|
+
# -- bridged from the sealed Build feed ------------------------------------------------
|
|
167
|
+
"build_sources_read_started": ("total",),
|
|
168
|
+
"build_source_read": ("source_id", "bytes"),
|
|
169
|
+
"build_sources_parse_started": ("total",),
|
|
170
|
+
"build_source_parsed": ("source_id", "rows", "columns"),
|
|
171
|
+
"build_cleaning_started": ("total",),
|
|
172
|
+
"build_cleaning_applied": ("step_index", "step_total", "source_id", "operation"),
|
|
173
|
+
"build_join_completed": ("left_rows", "right_rows", "output_rows"),
|
|
174
|
+
"build_rows_selected": ("rows", "columns"),
|
|
175
|
+
"build_checks_started": ("total",),
|
|
176
|
+
"build_checks_completed": ("passed", "total"),
|
|
177
|
+
"build_table_written": ("rows", "columns"),
|
|
178
|
+
"build_candidate_sealed": ("candidate_digest", "table_sha256", "row_count"),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
PROGRESS_EVENT_NAMES = frozenset(PROGRESS_EVENT_FACTS)
|
|
182
|
+
|
|
183
|
+
# How a settled probe ended, as a CLOSED set rather than as whatever token the emitter happened to
|
|
184
|
+
# hold. A watcher branches on this value -- it is the whole reason `source_probe_settled` exists --
|
|
185
|
+
# and a value it has never seen is worse than no record, because the record still terminates the
|
|
186
|
+
# probe on the timeline while saying nothing about how.
|
|
187
|
+
#
|
|
188
|
+
# These four REFINE the two Studio's `ContractProbeResultCommand.outcome` enum admits; they do not
|
|
189
|
+
# contradict them. `refused` and `timed_out` are both `failed` on the wire, because the wire is
|
|
190
|
+
# Studio's enum and widening it would be a contract change. The refinement lives here because this
|
|
191
|
+
# record is unsealed and harness-owned, which is exactly the licence this vocabulary was given:
|
|
192
|
+
#
|
|
193
|
+
# * `succeeded` -- the probe was answered and the answer was reported.
|
|
194
|
+
# * `refused` -- the probe was refused rather than answered: its request was outside the probe
|
|
195
|
+
# vocabulary, or named a source the session may not look at or a column the source
|
|
196
|
+
# does not have, or the source channel would not serve it at all. Re-asking the
|
|
197
|
+
# same probe refuses identically; the fix is a different probe, or a channel.
|
|
198
|
+
# * `timed_out` -- the probe was attempted and did not finish inside a ceiling. A post-acquisition
|
|
199
|
+
# `SANDBOX_TIMEOUT` may yield to a smaller scan; a source-acquisition timeout
|
|
200
|
+
# does not, because bytes never arrived.
|
|
201
|
+
# * `throttled` -- an acquisition admission limit, not a source or probe refusal. The worker keeps
|
|
202
|
+
# its wire code, emits a display code, and carries Studio's retry window only
|
|
203
|
+
# when Studio supplied one; it never guesses a wait. This is unrelated to model
|
|
204
|
+
# tokens, billing, authentication, or source credentials.
|
|
205
|
+
# * `failed` -- attempted, and broke for a reason that is none of the above.
|
|
206
|
+
PROBE_OUTCOMES: tuple[str, ...] = ("succeeded", "refused", "throttled", "failed", "timed_out")
|
|
207
|
+
|
|
208
|
+
# The `code` fact is a failure code, and a succeeded probe has none. It carries this sentinel rather
|
|
209
|
+
# than being absent, because a fact set is exact here and an optional key would be the one thing
|
|
210
|
+
# `validate_progress_facts` exists to refuse. `OK` is spelled in the code namespace's own case so a
|
|
211
|
+
# reader grepping a timeline for a code sees one value per record and never an empty column.
|
|
212
|
+
PROBE_OUTCOME_OK_CODE = "OK"
|
|
213
|
+
|
|
214
|
+
# Fact values that are a closed enumeration rather than an open token. `_safe_value` bounds the
|
|
215
|
+
# SHAPE of every fact; this bounds the VALUE of the few that are a decision a consumer branches on.
|
|
216
|
+
# Keyed by (name, fact) so adding one is a two-line edit and forgetting to declare one is not
|
|
217
|
+
# silently permissive -- a fact absent from here is an ordinary token, which is the honest default.
|
|
218
|
+
PROGRESS_ENUM_FACT_VALUES: Mapping[tuple[str, str], tuple[str, ...]] = {
|
|
219
|
+
("source_probe_settled", "outcome"): PROBE_OUTCOMES,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# Which contract enum value each progress name travels as. TOTAL over the vocabulary -- a name
|
|
223
|
+
# without an entry cannot be emitted, which is checked at import rather than at the first send, so
|
|
224
|
+
# a new name that nobody mapped fails the test run and never the production attempt.
|
|
225
|
+
STUDIO_EVENT_TYPE: Mapping[str, str] = {
|
|
226
|
+
"attempt_started": "run_stage_reported",
|
|
227
|
+
"stage_started": "run_stage_reported",
|
|
228
|
+
"stage_completed": "run_stage_reported",
|
|
229
|
+
"source_acquire_started": "attempt_progress",
|
|
230
|
+
"source_acquire_completed": "source_probe_completed",
|
|
231
|
+
"source_probe_started": "attempt_progress",
|
|
232
|
+
"source_probe_settled": "source_probe_completed",
|
|
233
|
+
"artifact_upload_started": "attempt_progress",
|
|
234
|
+
"artifact_upload_sealed": "attempt_progress",
|
|
235
|
+
"candidate_submitted": "run_stage_reported",
|
|
236
|
+
"verify_handoff": "run_stage_reported",
|
|
237
|
+
"attempt_finished": "run_stage_reported",
|
|
238
|
+
"attempt_failed": "diagnostic_emitted",
|
|
239
|
+
"build_sources_read_started": "run_stage_reported",
|
|
240
|
+
"build_source_read": "attempt_progress",
|
|
241
|
+
"build_sources_parse_started": "run_stage_reported",
|
|
242
|
+
"build_source_parsed": "attempt_progress",
|
|
243
|
+
"build_cleaning_started": "run_stage_reported",
|
|
244
|
+
"build_cleaning_applied": "attempt_progress",
|
|
245
|
+
"build_join_completed": "attempt_progress",
|
|
246
|
+
"build_rows_selected": "attempt_progress",
|
|
247
|
+
"build_checks_started": "run_stage_reported",
|
|
248
|
+
"build_checks_completed": "attempt_progress",
|
|
249
|
+
"build_table_written": "attempt_progress",
|
|
250
|
+
"build_candidate_sealed": "attempt_progress",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Sealed Build name -> progress name. Hand written, and deliberately PARTIAL. What is left out and
|
|
254
|
+
# why:
|
|
255
|
+
#
|
|
256
|
+
# * `member_sealed`, `member_verified` and `check_completed` are per-item over sets whose size the
|
|
257
|
+
# plan fixes but does not bound tightly; a timeline gains a spinner from them and nothing else.
|
|
258
|
+
# * `run_started`, `rights_checked`, `evidence_matched`, `sources_profiled`,
|
|
259
|
+
# `members_sealed_started`, `snapshot_verify_started`, `snapshot_verified` and
|
|
260
|
+
# `candidate_installed` are Build evidence whose value is that they are PROVABLE. A live copy of
|
|
261
|
+
# a provable record is the weaker of the two and invites a reader to cite the copy.
|
|
262
|
+
# * `build_failed` and `run_interrupted` are terminal Build records. The hosted producer narrates
|
|
263
|
+
# its own failure through `attempt_failed`, from the error it is about to raise, with a code it
|
|
264
|
+
# controls -- rather than through a mirrored record whose `message` fact is free prose.
|
|
265
|
+
SEALED_BUILD_EVENT_BRIDGE: Mapping[str, str] = {
|
|
266
|
+
"sources_read_started": "build_sources_read_started",
|
|
267
|
+
"source_read": "build_source_read",
|
|
268
|
+
"sources_parsed_started": "build_sources_parse_started",
|
|
269
|
+
"source_parsed": "build_source_parsed",
|
|
270
|
+
"cleaning_started": "build_cleaning_started",
|
|
271
|
+
"cleaning_applied": "build_cleaning_applied",
|
|
272
|
+
"join_completed": "build_join_completed",
|
|
273
|
+
"rows_selected": "build_rows_selected",
|
|
274
|
+
"checks_started": "build_checks_started",
|
|
275
|
+
"checks_completed": "build_checks_completed",
|
|
276
|
+
"table_written": "build_table_written",
|
|
277
|
+
"build_sealed": "build_candidate_sealed",
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# The exact sealed fact keys that cross the bridge, per event. Everything else is dropped, including
|
|
281
|
+
# `evidence_member`: a member path is a Build pointer, and a progress record that carried one would
|
|
282
|
+
# be inviting a reader to resolve it against a candidate that may not exist yet.
|
|
283
|
+
BRIDGED_FACT_KEYS: Mapping[str, tuple[str, ...]] = {
|
|
284
|
+
"sources_read_started": ("total",),
|
|
285
|
+
"source_read": ("source_id", "bytes"),
|
|
286
|
+
"sources_parsed_started": ("total",),
|
|
287
|
+
"source_parsed": ("source_id", "rows", "columns"),
|
|
288
|
+
"cleaning_started": ("total",),
|
|
289
|
+
"cleaning_applied": ("step_index", "step_total", "source_id", "operation"),
|
|
290
|
+
"join_completed": ("left_rows", "right_rows", "output_rows"),
|
|
291
|
+
"rows_selected": ("rows", "columns"),
|
|
292
|
+
"checks_started": ("total",),
|
|
293
|
+
"checks_completed": ("passed", "total"),
|
|
294
|
+
"table_written": ("rows", "columns"),
|
|
295
|
+
"build_sealed": ("candidate_digest", "table_sha256", "row_count"),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# A fact string is a short token: identifiers, digests, enum words and dotted names. It is NOT
|
|
299
|
+
# prose and NOT a path. The class excludes the space, the backslash and every quote, so a sentence,
|
|
300
|
+
# a Windows drive prefix and a rooted POSIX path all fail it, and the record carrying one is dropped
|
|
301
|
+
# whole rather than half redacted.
|
|
302
|
+
_TOKEN_RE = re.compile(r"\A[A-Za-z0-9_.:+-]{1,128}\Z")
|
|
303
|
+
|
|
304
|
+
_MAX_FACT_INT = 2**53 - 1
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ProgressVocabularyError(ValueError):
|
|
308
|
+
"""A progress record does not match its declared name, fact set or value domain."""
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _safe_value(value: Any) -> bool:
|
|
312
|
+
"""Return whether one fact value is inside the progress value domain.
|
|
313
|
+
|
|
314
|
+
``bool`` is tested before ``int`` on purpose: in Python a bool IS an int, and a magnitude bound
|
|
315
|
+
applied to ``True`` reads as an accident rather than as a rule.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
if isinstance(value, bool):
|
|
319
|
+
return True
|
|
320
|
+
if isinstance(value, int):
|
|
321
|
+
return -_MAX_FACT_INT <= value <= _MAX_FACT_INT
|
|
322
|
+
if isinstance(value, str):
|
|
323
|
+
return _TOKEN_RE.match(value) is not None
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def is_progress_fact_value(value: Any) -> bool:
|
|
328
|
+
"""Return whether ``value`` may travel as a progress fact, for a caller building one.
|
|
329
|
+
|
|
330
|
+
The public view of `_safe_value`, and the only supported way to ask the question outside this
|
|
331
|
+
module. An emitter that has a value from somewhere else -- a failure code off an exception, say
|
|
332
|
+
-- can check it here and substitute a known-good one, rather than handing over a value that
|
|
333
|
+
makes the whole record get dropped at `emit`.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
return _safe_value(value)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def validate_progress_facts(name: str, facts: Mapping[str, Any]) -> None:
|
|
340
|
+
"""Enforce the declared name, fact set and value domain; raise naming the difference.
|
|
341
|
+
|
|
342
|
+
Raises ``ProgressVocabularyError``. Callers on the emission path never let it escape -- the
|
|
343
|
+
point of raising is that a drifted producer fails a test run, not a hosted attempt.
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
declared = PROGRESS_EVENT_FACTS.get(name)
|
|
347
|
+
if declared is None:
|
|
348
|
+
raise ProgressVocabularyError(f"progress name is outside the closed vocabulary: {name!r}")
|
|
349
|
+
if not isinstance(facts, Mapping):
|
|
350
|
+
raise ProgressVocabularyError(f"facts for {name!r} is not a mapping")
|
|
351
|
+
present = set(facts)
|
|
352
|
+
expected = set(declared)
|
|
353
|
+
if present != expected:
|
|
354
|
+
missing = sorted(expected - present)
|
|
355
|
+
extra = sorted(present - expected)
|
|
356
|
+
raise ProgressVocabularyError(
|
|
357
|
+
f"facts for {name!r} do not match the declared set: "
|
|
358
|
+
f"missing={missing} extra={extra} declared={list(declared)}"
|
|
359
|
+
)
|
|
360
|
+
for key, value in facts.items():
|
|
361
|
+
if not _safe_value(value):
|
|
362
|
+
raise ProgressVocabularyError(
|
|
363
|
+
f"fact {key!r} of {name!r} is outside the progress value domain: {type(value)}"
|
|
364
|
+
)
|
|
365
|
+
admitted = PROGRESS_ENUM_FACT_VALUES.get((name, key))
|
|
366
|
+
if admitted is not None and value not in admitted:
|
|
367
|
+
raise ProgressVocabularyError(
|
|
368
|
+
f"fact {key!r} of {name!r} is not one of {list(admitted)}: {value!r}"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def progress_payload(name: str, facts: Mapping[str, Any]) -> dict[str, Any]:
|
|
373
|
+
"""Return the canonical payload one progress record commits to.
|
|
374
|
+
|
|
375
|
+
``unsealed`` is a literal field rather than a comment. A payload that ever reaches a store
|
|
376
|
+
beside sealed evidence says what it is in its own bytes, so a reader that has only the payload
|
|
377
|
+
still cannot mistake it for Build evidence.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
validate_progress_facts(name, facts)
|
|
381
|
+
return {
|
|
382
|
+
"schema_version": PROGRESS_SCHEMA_VERSION,
|
|
383
|
+
"event": name,
|
|
384
|
+
"unsealed": True,
|
|
385
|
+
"studio_event_type": STUDIO_EVENT_TYPE[name],
|
|
386
|
+
"facts": {key: facts[key] for key in PROGRESS_EVENT_FACTS[name]},
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def progress_payload_and_digest(name: str, facts: Mapping[str, Any]) -> tuple[dict[str, Any], str]:
|
|
391
|
+
"""Return one progress payload and its content address, from a single canonicalization.
|
|
392
|
+
|
|
393
|
+
The wire carries BOTH halves -- the body in ``payload`` and the address in ``payload_digest``
|
|
394
|
+
-- and Studio refuses the append when they disagree. Deriving them separately at a call site is
|
|
395
|
+
therefore the one mistake worth designing out, so this is the only place that pairs them, and
|
|
396
|
+
:func:`progress_payload_digest` is the digest-only view of it rather than a second formula.
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
payload = progress_payload(name, facts)
|
|
400
|
+
return payload, "sha256:" + sha256_bytes(canonical_json_bytes(payload))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def progress_payload_digest(name: str, facts: Mapping[str, Any]) -> str:
|
|
404
|
+
"""Return the ``sha256:``-prefixed content address of one progress payload.
|
|
405
|
+
|
|
406
|
+
This is the ONLY digest this module produces and it attests one thing: the payload bytes. It is
|
|
407
|
+
not evidence about a Build, it is not an input to any manifest, and nothing verifies a candidate
|
|
408
|
+
against it.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
return progress_payload_and_digest(name, facts)[1]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
ProgressSink = Callable[[str, Mapping[str, Any]], None]
|
|
415
|
+
|
|
416
|
+
# A ContextVar for the same reason `events._EVENTS` is one, and a SEPARATE one so that arming or
|
|
417
|
+
# disarming either stream can never touch the other. A thread starts from an empty context, so one
|
|
418
|
+
# hosted attempt can never emit into another's stream.
|
|
419
|
+
_PROGRESS: ContextVar[ProgressSink | None] = ContextVar("mostlyright_run_progress", default=None)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def emit(name: str, facts: Mapping[str, Any]) -> None:
|
|
423
|
+
"""Emit one progress record. Returns nothing and, with no sink armed, does nothing.
|
|
424
|
+
|
|
425
|
+
On ANY exception from the sink the sink is DISARMED for the rest of the arming context rather
|
|
426
|
+
than retried, exactly as `events.emit` does: a progress stream missing its tail is a loud
|
|
427
|
+
failure, and a progress stream that fails a Build is not a tradeoff anyone agreed to.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
sink = _PROGRESS.get()
|
|
431
|
+
if sink is None:
|
|
432
|
+
return None
|
|
433
|
+
try:
|
|
434
|
+
sink(name, facts)
|
|
435
|
+
except Exception:
|
|
436
|
+
_PROGRESS.set(None)
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@contextlib.contextmanager
|
|
441
|
+
def progress_sink(sink: ProgressSink) -> Iterator[None]:
|
|
442
|
+
"""Arm ``sink`` for the duration of the block and restore the previous one on exit."""
|
|
443
|
+
|
|
444
|
+
token = _PROGRESS.set(sink)
|
|
445
|
+
try:
|
|
446
|
+
yield
|
|
447
|
+
finally:
|
|
448
|
+
_PROGRESS.reset(token)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def translate_build_event(
|
|
452
|
+
event: str, facts: Mapping[str, Any]
|
|
453
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
454
|
+
"""Return the progress record for one sealed Build record, or ``None`` to drop it.
|
|
455
|
+
|
|
456
|
+
PURE. Four reasons to return ``None``, all of them silent by design -- a bridge that raised
|
|
457
|
+
would surface a sealed-feed detail as a progress-stream failure:
|
|
458
|
+
|
|
459
|
+
* the Build name is not in `SEALED_BUILD_EVENT_BRIDGE`;
|
|
460
|
+
* a fact the translation needs is absent (a sealed producer that drifted);
|
|
461
|
+
* a fact value is outside the progress value domain, which is the guard that keeps a path or a
|
|
462
|
+
free-prose message from crossing;
|
|
463
|
+
* the translated record does not validate, which cannot happen while the tables agree and is
|
|
464
|
+
checked anyway because the tables are hand written.
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
name = SEALED_BUILD_EVENT_BRIDGE.get(event)
|
|
468
|
+
if name is None:
|
|
469
|
+
return None
|
|
470
|
+
carried = BRIDGED_FACT_KEYS[event]
|
|
471
|
+
if any(key not in facts for key in carried):
|
|
472
|
+
return None
|
|
473
|
+
translated = {key: facts[key] for key in carried}
|
|
474
|
+
try:
|
|
475
|
+
validate_progress_facts(name, translated)
|
|
476
|
+
except ProgressVocabularyError:
|
|
477
|
+
return None
|
|
478
|
+
return name, translated
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@contextlib.contextmanager
|
|
482
|
+
def bridge_build_events() -> Iterator[None]:
|
|
483
|
+
"""Republish the bridged subset of live Build records onto the armed progress sink.
|
|
484
|
+
|
|
485
|
+
One way only. This watches `events` and writes to `_PROGRESS`; nothing here can write a record
|
|
486
|
+
into the Build feed, and `events.observer` calls the previously armed Build sink first, so the
|
|
487
|
+
feed is unaffected whether or not this is installed.
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
def _watch(
|
|
491
|
+
event: str, facts: Mapping[str, Any], evidence: Mapping[str, Any] | None = None
|
|
492
|
+
) -> None:
|
|
493
|
+
translated = translate_build_event(event, facts)
|
|
494
|
+
if translated is not None:
|
|
495
|
+
emit(*translated)
|
|
496
|
+
|
|
497
|
+
with events.observer(_watch):
|
|
498
|
+
yield
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _check_tables() -> None:
|
|
502
|
+
"""Refuse to import a module whose hand-written tables disagree.
|
|
503
|
+
|
|
504
|
+
Every one of these is a table an author edits by hand, and every one of them is cheap to check
|
|
505
|
+
once at import. The alternative -- discovering a missing `STUDIO_EVENT_TYPE` entry when a hosted
|
|
506
|
+
attempt tries to send it -- turns an editing mistake into a production one.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
if set(STUDIO_EVENT_TYPE) != PROGRESS_EVENT_NAMES:
|
|
510
|
+
raise ProgressVocabularyError("STUDIO_EVENT_TYPE is not total over the progress vocabulary")
|
|
511
|
+
if not UNSEALED_STUDIO_EVENT_TYPES.issuperset(STUDIO_EVENT_TYPE.values()):
|
|
512
|
+
raise ProgressVocabularyError(
|
|
513
|
+
"a progress name maps outside the unsealed half of the Studio event-type enum"
|
|
514
|
+
)
|
|
515
|
+
for enum_name, enum_fact in PROGRESS_ENUM_FACT_VALUES:
|
|
516
|
+
if enum_fact not in PROGRESS_EVENT_FACTS.get(enum_name, ()):
|
|
517
|
+
raise ProgressVocabularyError(
|
|
518
|
+
f"PROGRESS_ENUM_FACT_VALUES names {enum_fact!r} of {enum_name!r}, "
|
|
519
|
+
"which is not a declared fact of a declared name"
|
|
520
|
+
)
|
|
521
|
+
for admitted in PROGRESS_ENUM_FACT_VALUES.values():
|
|
522
|
+
for value in admitted:
|
|
523
|
+
if not _safe_value(value):
|
|
524
|
+
raise ProgressVocabularyError(
|
|
525
|
+
f"an admitted enum value is outside the progress value domain: {value!r}"
|
|
526
|
+
)
|
|
527
|
+
if set(SEALED_BUILD_EVENT_BRIDGE) != set(BRIDGED_FACT_KEYS):
|
|
528
|
+
raise ProgressVocabularyError("the bridge and its fact allowlist cover different events")
|
|
529
|
+
for event, name in SEALED_BUILD_EVENT_BRIDGE.items():
|
|
530
|
+
if set(BRIDGED_FACT_KEYS[event]) != set(PROGRESS_EVENT_FACTS[name]):
|
|
531
|
+
raise ProgressVocabularyError(f"bridged fact keys for {event!r} are not {name!r}'s set")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
_check_tables()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The Toolbox: the closed set of certified Readers this product ships with.
|
|
2
|
+
|
|
3
|
+
A Reader is the code that turns fetched bytes into rows. ``docs/VOCABULARY.md`` states
|
|
4
|
+
what this package is in one sentence -- "the Toolbox, the set of certified Readers this
|
|
5
|
+
product ships with. A table in the software; nothing is uploaded to it" -- and that
|
|
6
|
+
sentence is the whole design. The table is closed: membership is exactly the family
|
|
7
|
+
instances written into ``registry.py``, resolution is on an exact ``(family_id,
|
|
8
|
+
family_version)`` key with no fallback to a nearby version, and there is no plugin
|
|
9
|
+
discovery, no import-by-name, and no environment override. Widening the table is a source
|
|
10
|
+
edit that goes through review, and two versions of one family ship side by side so that a
|
|
11
|
+
sealed recipe keeps resolving its own pin forever.
|
|
12
|
+
|
|
13
|
+
Import discipline
|
|
14
|
+
-----------------
|
|
15
|
+
This package may import ``formats``, ``canonical``, the standard library, and a family's
|
|
16
|
+
own pinned decoder library. It must not import ``acquisition``, ``sources``, ``recipe``,
|
|
17
|
+
or ``pipeline``. Those layers import this one -- the sandbox worker resolves a Reader
|
|
18
|
+
family from this table -- and a back-edge would close an import cycle whose failure is
|
|
19
|
+
order-dependent, which is worse than a hard error: a test run that imported ``readers``
|
|
20
|
+
first would fail while the application, which imports the other way round, stayed green.
|
|
21
|
+
|
|
22
|
+
The rule that keeps the discipline enforceable is that anything both ``acquisition`` and
|
|
23
|
+
``readers`` need lives in ``formats``, which is a true leaf and says so in its own
|
|
24
|
+
docstring. Never satisfy a shared need with an import from ``acquisition``. A test walks
|
|
25
|
+
this package and fails on any import of a layer above it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from mostlyright.data_harness.readers.contracts import (
|
|
29
|
+
DECODE_FLAGS,
|
|
30
|
+
READER_ERROR_CODES,
|
|
31
|
+
ReaderBudgets,
|
|
32
|
+
ReaderError,
|
|
33
|
+
ReaderFamily,
|
|
34
|
+
ReaderPin,
|
|
35
|
+
ReaderResult,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"DECODE_FLAGS",
|
|
40
|
+
"READER_ERROR_CODES",
|
|
41
|
+
"ReaderBudgets",
|
|
42
|
+
"ReaderError",
|
|
43
|
+
"ReaderFamily",
|
|
44
|
+
"ReaderPin",
|
|
45
|
+
"ReaderResult",
|
|
46
|
+
]
|