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,485 @@
|
|
|
1
|
+
"""What ``mr-data approve`` derives, and the plain summary of what freezing a Recipe binds.
|
|
2
|
+
|
|
3
|
+
Approving a Recipe used to mean typing four identifiers correctly -- an approval name, an approver,
|
|
4
|
+
a timestamp, and a policy fingerprint -- and the README's own example passed sixty-four zeros for
|
|
5
|
+
the last one, which is how that goes in practice. Everything here exists so none of those has to be
|
|
6
|
+
typed, and so the person is shown what they are freezing before it is frozen.
|
|
7
|
+
|
|
8
|
+
Two rules hold this module together.
|
|
9
|
+
|
|
10
|
+
**Every derived value satisfies the contract by construction.** The validators in
|
|
11
|
+
:mod:`mostlyright.data_harness.recipe` are the truth about what a ``RecipeApproval`` accepts, and
|
|
12
|
+
the derivations below are written against them, not against a copy of their grammar.
|
|
13
|
+
|
|
14
|
+
**The approver is a person who ran a command.** ``derive_approved_by`` reads the flag the operator
|
|
15
|
+
passed or the owner of this process, and nothing else -- not the environment, not the Recipe, not a
|
|
16
|
+
file. When neither yields a usable name it refuses, because an approval naming the wrong person is
|
|
17
|
+
worse than an approval that did not happen.
|
|
18
|
+
|
|
19
|
+
Nothing here prints, reads a file, or writes one. :func:`approval_summary` returns structured
|
|
20
|
+
facts for terminal and browser rendering without going through argparse.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import pwd
|
|
27
|
+
from collections.abc import Mapping, Sequence
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from datetime import UTC, datetime
|
|
30
|
+
from types import MappingProxyType
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
from mostlyright.data_harness.canonical import canonical_json_bytes, sha256_bytes
|
|
34
|
+
from mostlyright.data_harness.local_contracts import GraphTablePlan
|
|
35
|
+
from mostlyright.data_harness.recipe import (
|
|
36
|
+
FrozenRecipe,
|
|
37
|
+
RecipeError,
|
|
38
|
+
_identifier,
|
|
39
|
+
_timestamp,
|
|
40
|
+
)
|
|
41
|
+
from mostlyright.data_harness.ux.render import render_lines
|
|
42
|
+
|
|
43
|
+
# The identifier grammar `recipe._IDENT` accepts: a leading letter or digit, then up to 127 more
|
|
44
|
+
# characters drawn from letters, digits, and `_ . -`. The derivations below fit inside it rather
|
|
45
|
+
# than hand the contract a value it would refuse.
|
|
46
|
+
MAX_IDENTIFIER_CHARACTERS = 128
|
|
47
|
+
IDENTIFIER_CHARACTERS = "_.-"
|
|
48
|
+
APPROVAL_ID_PREFIX = "approval-"
|
|
49
|
+
FINGERPRINT_CHARACTERS = 12
|
|
50
|
+
|
|
51
|
+
# The canonical UTC grammar `recipe._timestamp` accepts, to the second.
|
|
52
|
+
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
|
53
|
+
|
|
54
|
+
# The approval policy when the operator supplies none: a policy with no rules written down. Its
|
|
55
|
+
# fingerprint is a real sha256 of real canonical bytes, which is the whole point -- a fingerprint
|
|
56
|
+
# of sixty-four zeros means "I did not think about this", and that is worse than no fingerprint.
|
|
57
|
+
#
|
|
58
|
+
# Changing this value changes every fingerprint derived from it. It is a published constant, not
|
|
59
|
+
# an implementation detail: an approval recorded last year can only be re-checked against the same
|
|
60
|
+
# bytes it was hashed from.
|
|
61
|
+
EMPTY_APPROVAL_POLICY: Mapping[str, Any] = MappingProxyType({})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------------------------------------
|
|
65
|
+
# The derivations
|
|
66
|
+
# ------------------------------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def derive_approval_id(recipe: FrozenRecipe) -> str:
|
|
70
|
+
"""Name this approval after the exact Recipe it approves.
|
|
71
|
+
|
|
72
|
+
The name carries the Recipe's own name, its version, and the first
|
|
73
|
+
:data:`FINGERPRINT_CHARACTERS` characters of its fingerprint, so reading the name tells you
|
|
74
|
+
what was approved. It is derived from the Recipe alone and from nothing else, so approving the
|
|
75
|
+
same Recipe twice derives the same name: the second run is a visible refusal to overwrite
|
|
76
|
+
rather than a second approval of the same thing under a different name.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
tail = f"-v{recipe.recipe_version}-{recipe.digest[:FINGERPRINT_CHARACTERS]}"
|
|
80
|
+
budget = MAX_IDENTIFIER_CHARACTERS - len(APPROVAL_ID_PREFIX) - len(tail)
|
|
81
|
+
stem = recipe.recipe_id[:budget].rstrip(IDENTIFIER_CHARACTERS) if budget > 0 else ""
|
|
82
|
+
derived = f"{APPROVAL_ID_PREFIX}{stem}{tail}" if stem else f"approval{tail}"
|
|
83
|
+
return _identifier(derived, "approval.approval_id")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def derive_approved_by(explicit: str | None = None) -> str:
|
|
87
|
+
"""Name the person approving: the flag they passed, or the owner of this process.
|
|
88
|
+
|
|
89
|
+
There is deliberately no third source. The environment is not one of them either: the standard
|
|
90
|
+
``getpass.getuser`` reads ``LOGNAME``/``USER``/``LNAME``/``USERNAME`` before it reads the
|
|
91
|
+
password database, so an approval built on it would name whoever last exported a variable
|
|
92
|
+
(T-29-26). This reads the process owner from the password database directly.
|
|
93
|
+
|
|
94
|
+
A name that cannot be established is a refusal naming ``--approved-by``. Nothing is invented:
|
|
95
|
+
this field is what the review trail rests on.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
if explicit is not None:
|
|
99
|
+
if _as_identifier(explicit) != explicit.strip():
|
|
100
|
+
raise RecipeError(
|
|
101
|
+
"RECIPE_APPROVER",
|
|
102
|
+
"approval.approved_by",
|
|
103
|
+
"--approved-by must be letters, digits, and - . _ only, starting with a letter "
|
|
104
|
+
"or digit",
|
|
105
|
+
)
|
|
106
|
+
return _identifier(explicit.strip(), "approval.approved_by")
|
|
107
|
+
try:
|
|
108
|
+
owner = _process_owner()
|
|
109
|
+
except OSError as error:
|
|
110
|
+
raise RecipeError(
|
|
111
|
+
"RECIPE_APPROVER",
|
|
112
|
+
"approval.approved_by",
|
|
113
|
+
"this account has no name on this machine; pass --approved-by to say who is approving",
|
|
114
|
+
) from error
|
|
115
|
+
derived = _as_identifier(owner)
|
|
116
|
+
if not derived:
|
|
117
|
+
raise RecipeError(
|
|
118
|
+
"RECIPE_APPROVER",
|
|
119
|
+
"approval.approved_by",
|
|
120
|
+
"the owner of this process has no usable name; pass --approved-by to say who is "
|
|
121
|
+
"approving",
|
|
122
|
+
)
|
|
123
|
+
return _identifier(derived, "approval.approved_by")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def derive_approved_at(explicit: str | None = None) -> str:
|
|
127
|
+
"""The moment of approval, in the exact UTC grammar the contract accepts.
|
|
128
|
+
|
|
129
|
+
An explicit value is checked rather than trusted, so a test can pin the timestamp and a typo
|
|
130
|
+
is named at the point it was typed.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
if explicit is not None:
|
|
134
|
+
return _timestamp(explicit, "approval.approved_at")
|
|
135
|
+
return _timestamp(datetime.now(UTC).strftime(TIMESTAMP_FORMAT), "approval.approved_at")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def derive_policy_digest(policy_value: Mapping[str, Any] | None = None) -> str:
|
|
139
|
+
"""Fingerprint the approval policy the approver acted under.
|
|
140
|
+
|
|
141
|
+
With no policy supplied this is the fingerprint of :data:`EMPTY_APPROVAL_POLICY` -- a real hash
|
|
142
|
+
of the real canonical bytes of a policy with no rules in it. It is never sixty-four zeros.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
value = EMPTY_APPROVAL_POLICY if policy_value is None else policy_value
|
|
146
|
+
if not isinstance(value, Mapping):
|
|
147
|
+
raise RecipeError(
|
|
148
|
+
"RECIPE_APPROVAL_POLICY",
|
|
149
|
+
"approval.approval_policy",
|
|
150
|
+
"an approval policy must be a JSON object",
|
|
151
|
+
)
|
|
152
|
+
try:
|
|
153
|
+
raw = canonical_json_bytes(dict(value))
|
|
154
|
+
except (TypeError, ValueError) as error:
|
|
155
|
+
raise RecipeError(
|
|
156
|
+
"RECIPE_APPROVAL_POLICY",
|
|
157
|
+
"approval.approval_policy",
|
|
158
|
+
"an approval policy must hold JSON values only",
|
|
159
|
+
) from error
|
|
160
|
+
return sha256_bytes(raw)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _process_owner() -> str:
|
|
164
|
+
"""The name of the account this process runs as, read from the password database."""
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
return pwd.getpwuid(os.getuid()).pw_name
|
|
168
|
+
except KeyError as error:
|
|
169
|
+
raise OSError("this user id has no password database entry") from error
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _as_identifier(value: str) -> str:
|
|
173
|
+
"""``value`` reduced to the identifier grammar, or the empty string when nothing survives."""
|
|
174
|
+
|
|
175
|
+
kept = [
|
|
176
|
+
character
|
|
177
|
+
for character in value.strip()
|
|
178
|
+
if character.isascii() and (character.isalnum() or character in IDENTIFIER_CHARACTERS)
|
|
179
|
+
]
|
|
180
|
+
while kept and not (kept[0].isascii() and kept[0].isalnum()):
|
|
181
|
+
kept.pop(0)
|
|
182
|
+
return "".join(kept)[:MAX_IDENTIFIER_CHARACTERS]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------------------------------------
|
|
186
|
+
# What you are freezing
|
|
187
|
+
# ------------------------------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
# Plain sentences for the checks the pinned engine always runs. An unknown check id falls through
|
|
190
|
+
# to its own name, so a new engine check appears in the summary the day it ships.
|
|
191
|
+
_CHECK_SENTENCES: Mapping[str, str] = MappingProxyType(
|
|
192
|
+
{
|
|
193
|
+
"EXACT_CANDIDATE_REPLAY": "the sealed result replays byte for byte",
|
|
194
|
+
"RECIPE_DRIFT": "the result stays inside the refresh bounds below",
|
|
195
|
+
"SOURCE_AUTHORITY": "every input file matches the fingerprint the run pins",
|
|
196
|
+
"OUTPUT_SEMANTICS": "every column keeps the type and meaning declared above",
|
|
197
|
+
"TEMPORAL_INTEGRITY": "dates and times stay inside the approved window",
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
_RELATIONSHIPS: Mapping[str, str] = MappingProxyType(
|
|
202
|
+
{
|
|
203
|
+
"one_to_one": "one row on each side matches at most one row on the other",
|
|
204
|
+
"many_to_one": "many rows on the left match one row on the right",
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
_CLEANING_SENTENCES: Mapping[str, str] = MappingProxyType(
|
|
209
|
+
{
|
|
210
|
+
"trim": "trim the spaces around {columns} in {source}",
|
|
211
|
+
"empty_to_null": "treat empty text as missing in {columns} of {source}",
|
|
212
|
+
"rename": "rename {columns} in {source}",
|
|
213
|
+
"cast": "read {columns} in {source}",
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
_CLOSING = (
|
|
218
|
+
"Freezing is permanent: this fingerprint is what runs from now on, it can be replayed for as "
|
|
219
|
+
"long as the inputs are kept, and any change at all means a new Recipe with a new fingerprint. "
|
|
220
|
+
"Nothing is approved until this command finishes, and nothing can approve it for you."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True)
|
|
225
|
+
class ApprovalSummary:
|
|
226
|
+
"""Everything approving one Recipe binds, in the words a person reads before deciding.
|
|
227
|
+
|
|
228
|
+
Every field is derived from one parsed :class:`FrozenRecipe` and from no second source, so the
|
|
229
|
+
summary cannot describe something other than what the fingerprint binds (T-29-27).
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
recipe: str
|
|
233
|
+
recipe_digest: str
|
|
234
|
+
question: str
|
|
235
|
+
one_row_is: str
|
|
236
|
+
population: str
|
|
237
|
+
columns: tuple[str, ...]
|
|
238
|
+
sources: tuple[str, ...]
|
|
239
|
+
cleaning: tuple[str, ...]
|
|
240
|
+
join: str
|
|
241
|
+
checks: tuple[str, ...]
|
|
242
|
+
refresh_bounds: str
|
|
243
|
+
backfill_window: str
|
|
244
|
+
engine: str
|
|
245
|
+
what_it_is_for: str
|
|
246
|
+
plan_shape: str = "joined"
|
|
247
|
+
closing: str = _CLOSING
|
|
248
|
+
|
|
249
|
+
def to_dict(self) -> dict[str, Any]:
|
|
250
|
+
"""The structured facts, in the order a person reads them.
|
|
251
|
+
|
|
252
|
+
Facts taken from the Recipe -- column names, source names, check names -- go in values,
|
|
253
|
+
never in keys, so nothing the data happens to be called is shown as a label.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
steps_label = (
|
|
257
|
+
"the ordered graph steps" if self.plan_shape == "graph" else "the cleaning steps"
|
|
258
|
+
)
|
|
259
|
+
structure_label = "the graph output" if self.plan_shape == "graph" else "the join"
|
|
260
|
+
return {
|
|
261
|
+
"what this answers": self.question,
|
|
262
|
+
"one row is": self.one_row_is,
|
|
263
|
+
"it covers": self.population,
|
|
264
|
+
"what the result may be used for": self.what_it_is_for,
|
|
265
|
+
"the columns": _numbered("column", self.columns),
|
|
266
|
+
"the sources": _numbered("source", self.sources),
|
|
267
|
+
steps_label: _numbered("step", self.cleaning),
|
|
268
|
+
structure_label: self.join,
|
|
269
|
+
"the checks it must pass": _numbered("check", self.checks),
|
|
270
|
+
"the refresh bounds": self.refresh_bounds,
|
|
271
|
+
"the backfill window": self.backfill_window,
|
|
272
|
+
"the pinned versions": self.engine,
|
|
273
|
+
"the recipe": self.recipe,
|
|
274
|
+
"the fingerprint being frozen": self.recipe_digest,
|
|
275
|
+
"what freezing means": self.closing,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
def lines(self) -> tuple[str, ...]:
|
|
279
|
+
"""The terminal rendering of :meth:`to_dict`, and not a second truth.
|
|
280
|
+
|
|
281
|
+
The same renderer the command line uses for every other payload is used here, so what a
|
|
282
|
+
person reads on the terminal and what a caller reads under ``--json`` are the same facts
|
|
283
|
+
in the same order.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
return tuple(render_lines(self.to_dict()))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def approval_summary(recipe: FrozenRecipe) -> ApprovalSummary:
|
|
290
|
+
"""Say, in plain words, exactly what approving ``recipe`` freezes."""
|
|
291
|
+
|
|
292
|
+
plan = recipe.transform_plan
|
|
293
|
+
requirements = recipe.requirements
|
|
294
|
+
proposals = {item.source_id: item for item in recipe.source_proposals}
|
|
295
|
+
plan_sources = {item.source_id: item for item in plan.sources}
|
|
296
|
+
semantics = {item.name: item for item in recipe.output_semantics}
|
|
297
|
+
predecessor = (
|
|
298
|
+
"no earlier version"
|
|
299
|
+
if recipe.predecessor_recipe_digest is None
|
|
300
|
+
else f"replacing the version fingerprinted {recipe.predecessor_recipe_digest}"
|
|
301
|
+
)
|
|
302
|
+
covers = requirements.time_range
|
|
303
|
+
graph_plan = isinstance(plan, GraphTablePlan)
|
|
304
|
+
return ApprovalSummary(
|
|
305
|
+
recipe=f"{recipe.recipe_id}, version {recipe.recipe_version}, {predecessor}",
|
|
306
|
+
recipe_digest=recipe.digest,
|
|
307
|
+
question=recipe.question.text,
|
|
308
|
+
one_row_is=requirements.output_grain.description,
|
|
309
|
+
population=(
|
|
310
|
+
f"{requirements.population} From {covers.start_inclusive} up to {covers.end_exclusive}."
|
|
311
|
+
),
|
|
312
|
+
columns=tuple(_column_sentence(semantics[name]) for name in plan.select),
|
|
313
|
+
sources=tuple(
|
|
314
|
+
_source_sentence(
|
|
315
|
+
source,
|
|
316
|
+
plan_sources.get(source.source_id),
|
|
317
|
+
proposals.get(source.source_id),
|
|
318
|
+
)
|
|
319
|
+
for source in sorted(recipe.sources, key=lambda item: item.source_id)
|
|
320
|
+
),
|
|
321
|
+
cleaning=(
|
|
322
|
+
tuple(_graph_node_sentence(node) for node in plan.nodes)
|
|
323
|
+
if graph_plan
|
|
324
|
+
else tuple(_cleaning_sentence(step) for step in plan.cleaning)
|
|
325
|
+
),
|
|
326
|
+
join=_graph_output_sentence(plan) if graph_plan else _join_sentence(plan.join),
|
|
327
|
+
checks=_check_sentences(recipe, plan.quality),
|
|
328
|
+
refresh_bounds=_refresh_sentence(recipe.refresh_policy),
|
|
329
|
+
backfill_window=_backfill_sentence(recipe.backfill_policy),
|
|
330
|
+
engine=(
|
|
331
|
+
f"engine {recipe.engine_version}, operations {recipe.operation_contract_version}, "
|
|
332
|
+
f"readers {recipe.reader_contract_version}, "
|
|
333
|
+
f"PyArrow {recipe.pyarrow_version}, checks {recipe.validation_policy_version} "
|
|
334
|
+
f"({recipe.validation_policy_digest})"
|
|
335
|
+
),
|
|
336
|
+
what_it_is_for=_intent_sentence(plan.output_intent),
|
|
337
|
+
plan_shape="graph" if graph_plan else "joined",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# What the frozen intent means, said as a permission rather than as the identifier it is stored
|
|
342
|
+
# under. These are the three `local_contracts._OUTPUT_INTENTS`, and the guard below fails when that
|
|
343
|
+
# set grows a fourth, because an intent with no sentence here would be frozen unstated.
|
|
344
|
+
_INTENT_SENTENCES: Mapping[str, str] = MappingProxyType(
|
|
345
|
+
{
|
|
346
|
+
"local_use": "use on this machine only -- not published, not hosted",
|
|
347
|
+
"redistribute": "passing the result on to others",
|
|
348
|
+
"host": "serving the result from a hosted service",
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _intent_sentence(intent: str) -> str:
|
|
354
|
+
"""What this Recipe declares the result is for, and what that costs every source.
|
|
355
|
+
|
|
356
|
+
This is not decoration beside the rights already listed per source. It is the frozen claim the
|
|
357
|
+
build enforces: `pipeline` refuses to build when a source's recorded permissions do not include
|
|
358
|
+
this intent, so freezing `redistribute` and freezing `local_use` are two different approvals of
|
|
359
|
+
the same data, and a summary that shows neither asked somebody to agree to whichever one it
|
|
360
|
+
happened to be.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
meaning = _INTENT_SENTENCES.get(intent)
|
|
364
|
+
plain = f"{intent}: {meaning}" if meaning is not None else intent
|
|
365
|
+
return f"{plain}. Every source above has to permit it, and a build stops when one does not."
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _column_sentence(field: Any) -> str:
|
|
369
|
+
parts = [f"{field.name}: {field.logical_type}, {field.semantic_type}"]
|
|
370
|
+
if field.unit not in {"none", "count"}:
|
|
371
|
+
parts.append(f"in {field.unit}")
|
|
372
|
+
elif field.unit == "count":
|
|
373
|
+
parts.append("a count")
|
|
374
|
+
if field.temporal_role != "none":
|
|
375
|
+
parts.append(f"used as the {field.temporal_role.replace('_', ' ')}")
|
|
376
|
+
if field.timezone is not None:
|
|
377
|
+
parts.append(f"in {field.timezone}")
|
|
378
|
+
return ", ".join(parts)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _source_sentence(source: Any, plan_source: Any, proposal: Any) -> str:
|
|
382
|
+
parts = [f"{source.source_id}: {source.plan_source_path} ({source.data_format})"]
|
|
383
|
+
if proposal is not None:
|
|
384
|
+
parts.append(f"chosen as {proposal.display_name}, rights {proposal.rights_status}")
|
|
385
|
+
if plan_source is not None:
|
|
386
|
+
rights = plan_source.rights
|
|
387
|
+
parts.append(f"from {plan_source.origin}, obtained by {plan_source.acquisition_method}")
|
|
388
|
+
parts.append(f"rights {rights.status}, may {', '.join(rights.permissions)}")
|
|
389
|
+
parts.append(f"rights evidence {rights.evidence}")
|
|
390
|
+
parts.append(f"opened by {source.adapter_id} {source.adapter_version}")
|
|
391
|
+
parts.append(f"treated as {source.declared_classification}, under {source.lawful_basis}")
|
|
392
|
+
parts.append(
|
|
393
|
+
"keeps no Bookmark"
|
|
394
|
+
if source.watermark_kind == "none"
|
|
395
|
+
else f"Bookmark from {source.watermark_field} ({source.watermark_kind})"
|
|
396
|
+
)
|
|
397
|
+
return "; ".join(parts)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _cleaning_sentence(step: Any) -> str:
|
|
401
|
+
columns = step.columns
|
|
402
|
+
if columns and isinstance(columns[0], tuple):
|
|
403
|
+
written = ", ".join(f"{name} as {target}" for name, target in columns)
|
|
404
|
+
else:
|
|
405
|
+
written = ", ".join(str(name) for name in columns)
|
|
406
|
+
template = _CLEANING_SENTENCES.get(step.operation)
|
|
407
|
+
if template is None:
|
|
408
|
+
return f"{step.operation} {written} in {step.source_id}"
|
|
409
|
+
return template.format(columns=written, source=step.source_id)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _join_sentence(join: Any) -> str:
|
|
413
|
+
relationship = _RELATIONSHIPS.get(join.cardinality, join.cardinality)
|
|
414
|
+
return (
|
|
415
|
+
f"{join.left} joined to {join.right} on {', '.join(join.on)} "
|
|
416
|
+
f"({join.kind} join): {relationship} ({join.cardinality})"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _graph_node_sentence(node: Any) -> str:
|
|
421
|
+
inputs = ", ".join(node.inputs) if node.inputs else "none"
|
|
422
|
+
parameters = canonical_json_bytes(node.to_dict()["parameters"]).decode("utf-8")
|
|
423
|
+
return (
|
|
424
|
+
f"{node.node_id}: {node.operation} {node.operation_version}; "
|
|
425
|
+
f"inputs: {inputs}; parameters {parameters}"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _graph_output_sentence(plan: GraphTablePlan) -> str:
|
|
430
|
+
join_nodes = tuple(node.node_id for node in plan.nodes if node.operation == "join")
|
|
431
|
+
joins = f"join steps {', '.join(join_nodes)}" if join_nodes else "no join step"
|
|
432
|
+
return (
|
|
433
|
+
f"the closed graph ends at {plan.terminal} after {len(plan.nodes)} ordered steps; {joins}"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _check_sentences(recipe: FrozenRecipe, quality: Any) -> tuple[str, ...]:
|
|
438
|
+
written = [f"at least {quality.min_rows} rows"]
|
|
439
|
+
if quality.not_null:
|
|
440
|
+
written.append(f"never missing: {', '.join(quality.not_null)}")
|
|
441
|
+
written.append(f"each {', '.join(recipe.transform_plan.grain)} appears exactly once")
|
|
442
|
+
written.extend(
|
|
443
|
+
f"{check_id}: {_CHECK_SENTENCES.get(check_id, 'the pinned engine check of that name')}"
|
|
444
|
+
for check_id in recipe.required_check_ids
|
|
445
|
+
)
|
|
446
|
+
return tuple(written)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _refresh_sentence(policy: Any) -> str:
|
|
450
|
+
additions = (
|
|
451
|
+
"new columns are allowed" if policy.allow_column_additions else "new columns are refused"
|
|
452
|
+
)
|
|
453
|
+
return (
|
|
454
|
+
f"a later run must return between {policy.min_row_count_percent}% and "
|
|
455
|
+
f"{policy.max_row_count_percent}% of the rows this version has, and {additions}"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _backfill_sentence(policy: Any) -> str:
|
|
460
|
+
envelope = policy.coverage_envelope
|
|
461
|
+
scope = policy.partition_scope
|
|
462
|
+
fields = ", ".join(scope.fields) if scope.fields else "the whole result"
|
|
463
|
+
pieces = scope.max_partitions_per_run
|
|
464
|
+
return (
|
|
465
|
+
f"backfills may cover {envelope.start_inclusive} up to {envelope.end_exclusive}, "
|
|
466
|
+
f"at most {_duration(policy.max_window_seconds)} in one run, "
|
|
467
|
+
f"over {fields} in at most {pieces} {'piece' if pieces == 1 else 'pieces'} per run, "
|
|
468
|
+
f"overlapping released coverage: {policy.overlap_policy}"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _duration(seconds: int) -> str:
|
|
473
|
+
"""Seconds, with the plain reading of them alongside when it is an exact number of days."""
|
|
474
|
+
|
|
475
|
+
if seconds % 86_400 == 0:
|
|
476
|
+
days = seconds // 86_400
|
|
477
|
+
return f"{seconds} seconds ({days} {'day' if days == 1 else 'days'})"
|
|
478
|
+
return f"{seconds} seconds"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _numbered(noun: str, items: Sequence[str]) -> dict[str, str]:
|
|
482
|
+
"""One numbered key per item, so the order a person reads is the order the Recipe runs."""
|
|
483
|
+
|
|
484
|
+
width = len(str(max(len(items), 1)))
|
|
485
|
+
return {f"{noun} {index:0{width}d}": item for index, item in enumerate(items, start=1)}
|