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,444 @@
|
|
|
1
|
+
"""Comparing two builds: what is different between the one on the left and the one on the right.
|
|
2
|
+
|
|
3
|
+
``recipe-verify`` can already compare a new version against the version before it. This module
|
|
4
|
+
answers the other question, the one a person actually has when a refresh looks wrong: here are two
|
|
5
|
+
builds, what is different about them?
|
|
6
|
+
|
|
7
|
+
Three rules shape everything below.
|
|
8
|
+
|
|
9
|
+
**Both sides are verified before anything is compared.** Each side goes through
|
|
10
|
+
:func:`recipe.version_summary`, which calls ``pipeline.verify_candidate`` and then reads the
|
|
11
|
+
profile and lineage evidence off the verified snapshot. A comparison of unverified bytes would be
|
|
12
|
+
worth nothing, because the bytes could say anything.
|
|
13
|
+
|
|
14
|
+
**There is one drift vocabulary, not two.** Every change this module reports names the reviewed
|
|
15
|
+
drift code that governs the same condition, in :data:`RELATED_DRIFT_CODES`: a column set that moved
|
|
16
|
+
is ``RECIPE_SCHEMA_DRIFT``, a type that moved is ``RECIPE_TYPE_DRIFT``, a row count that moved is
|
|
17
|
+
``RECIPE_ROW_COUNT_DRIFT``. Those codes are read from ``recipe._drift_report`` rather than invented
|
|
18
|
+
here. Two conditions have no reviewed code, because a comparison of two arbitrary builds can meet
|
|
19
|
+
cases a successor-against-predecessor check never does, and both are additions made here:
|
|
20
|
+
|
|
21
|
+
* ``data_changed`` -- the shape is untouched and the bytes are not. The drift report has no such
|
|
22
|
+
code: it recomputes a version against its own predecessor under a recipe, where a change of data
|
|
23
|
+
with no change of shape is the ordinary case rather than a finding.
|
|
24
|
+
* ``sources_changed`` -- even the data fingerprint matches, and only the recorded lineage differs.
|
|
25
|
+
The drift report folds lineage into ``EXACT_CANDIDATE_REPLAY`` for one build; it never compares
|
|
26
|
+
the lineage of two.
|
|
27
|
+
|
|
28
|
+
Naming the reviewed code is a pointer, not a claim: it says which alert would look at this
|
|
29
|
+
condition when a Recipe and its refresh bounds are in play. This module raises no alert.
|
|
30
|
+
|
|
31
|
+
**A comparison describes; it never judges.** No severity, no eligibility, no opinion about whether
|
|
32
|
+
a difference is acceptable -- ``recipe-verify`` owns that judgement and a read-only command that
|
|
33
|
+
implied it would be putting a release opinion where nobody asked for one. That is also why the
|
|
34
|
+
``DriftFinding`` shape is borrowed here without its ``severity`` field.
|
|
35
|
+
|
|
36
|
+
The change list is the shortest true explanation rather than a restatement of every field: a
|
|
37
|
+
removed column already explains the data fingerprint that moved with it, so ``data_changed`` is
|
|
38
|
+
reported only when the shape is identical, and ``sources_changed`` only when even the data matches.
|
|
39
|
+
Every fingerprint on both sides is carried in the payload regardless, so nothing is hidden.
|
|
40
|
+
|
|
41
|
+
Nothing here prints. Every function returns facts or text, so the same comparison can be rendered
|
|
42
|
+
into a browser page later without re-deriving it.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
from dataclasses import dataclass
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
from mostlyright.data_harness.pipeline import verify_candidate
|
|
52
|
+
from mostlyright.data_harness.recipe import VersionSummary, version_summary
|
|
53
|
+
from mostlyright.data_harness.ux.readers import require_a_build
|
|
54
|
+
|
|
55
|
+
# Every change this module can report, in the order a reader meets them: what the columns did,
|
|
56
|
+
# then what the rows did, then what the bytes did.
|
|
57
|
+
CHANGE_CODES: tuple[str, ...] = (
|
|
58
|
+
"column_added",
|
|
59
|
+
"column_removed",
|
|
60
|
+
"column_type_changed",
|
|
61
|
+
"row_count_changed",
|
|
62
|
+
"data_changed",
|
|
63
|
+
"sources_changed",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# The reviewed drift code that governs the same condition, or ``None`` where the drift review has
|
|
67
|
+
# no equivalent. Both ``None`` entries are argued for in the module docstring above.
|
|
68
|
+
RELATED_DRIFT_CODES: dict[str, str | None] = {
|
|
69
|
+
"column_added": "RECIPE_SCHEMA_DRIFT",
|
|
70
|
+
"column_removed": "RECIPE_SCHEMA_DRIFT",
|
|
71
|
+
"column_type_changed": "RECIPE_TYPE_DRIFT",
|
|
72
|
+
"row_count_changed": "RECIPE_ROW_COUNT_DRIFT",
|
|
73
|
+
"data_changed": None,
|
|
74
|
+
"sources_changed": None,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# The changes that are about the columns themselves, for `--columns-only`.
|
|
78
|
+
COLUMN_CHANGE_CODES: frozenset[str] = frozenset(
|
|
79
|
+
{"column_added", "column_removed", "column_type_changed"}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
_STATUS_BY_RESULT = {"identical": "builds_identical", "differs": "builds_differ"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class BuildChange:
|
|
87
|
+
"""One difference between two builds, named at the granularity a person can act on.
|
|
88
|
+
|
|
89
|
+
A user chasing a broken refresh needs the column's name, not the sentence "the schema
|
|
90
|
+
changed", so every column-level difference is its own change carrying its own name.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
code: str
|
|
94
|
+
summary: str
|
|
95
|
+
column: str | None = None
|
|
96
|
+
was: Any = None
|
|
97
|
+
now: Any = None
|
|
98
|
+
difference: str | None = None
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
if self.code not in RELATED_DRIFT_CODES:
|
|
102
|
+
raise ValueError(f"unknown change code: {self.code!r}")
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def related_drift_code(self) -> str | None:
|
|
106
|
+
return RELATED_DRIFT_CODES[self.code]
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_column_change(self) -> bool:
|
|
110
|
+
return self.code in COLUMN_CHANGE_CODES
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
"""The one record both renderings are built from.
|
|
114
|
+
|
|
115
|
+
The column name is a value and never a key (29-04's rule): a column called ``max_temp_c``
|
|
116
|
+
used as a key would be shown to a person as ``Max temp c``, a name its data does not have.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
payload: dict[str, Any] = {"what": self.code}
|
|
120
|
+
if self.column is not None:
|
|
121
|
+
payload["column"] = self.column
|
|
122
|
+
if self.was is not None:
|
|
123
|
+
# `from` and `to` rather than the field names: read in order, a person meets them as
|
|
124
|
+
# "from this, to that", and that is also the order the plain rendering sorts them in.
|
|
125
|
+
payload["from"] = self.was
|
|
126
|
+
if self.now is not None:
|
|
127
|
+
payload["to"] = self.now
|
|
128
|
+
if self.difference is not None:
|
|
129
|
+
payload["difference"] = self.difference
|
|
130
|
+
payload["related drift alert"] = self.related_drift_code
|
|
131
|
+
return payload
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class ComparedBuild:
|
|
136
|
+
"""One side of a comparison: what it is, who made it, and the verified shape of its data."""
|
|
137
|
+
|
|
138
|
+
run_dir: Path
|
|
139
|
+
manifest_version: str
|
|
140
|
+
candidate_digest: str
|
|
141
|
+
producer_principal: str
|
|
142
|
+
summary: VersionSummary
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def table_sha256(self) -> str:
|
|
146
|
+
return self.summary.table_sha256
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def row_count(self) -> int:
|
|
150
|
+
return self.summary.row_count
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def columns(self) -> tuple[str, ...]:
|
|
154
|
+
return self.summary.columns
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def column_types(self) -> dict[str, str]:
|
|
158
|
+
return dict(self.summary.column_types)
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def lineage_digest(self) -> str:
|
|
162
|
+
return self.summary.lineage_digest
|
|
163
|
+
|
|
164
|
+
def to_dict(self) -> dict[str, Any]:
|
|
165
|
+
return {
|
|
166
|
+
"run_dir": str(self.run_dir),
|
|
167
|
+
"manifest_version": self.manifest_version,
|
|
168
|
+
"candidate_digest": self.candidate_digest,
|
|
169
|
+
"table_sha256": self.table_sha256,
|
|
170
|
+
"lineage_digest": self.lineage_digest,
|
|
171
|
+
"built_by": self.producer_principal,
|
|
172
|
+
"row_count": self.row_count,
|
|
173
|
+
"columns": list(self.columns),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class BuildComparison:
|
|
179
|
+
"""Two verified builds and every difference between them."""
|
|
180
|
+
|
|
181
|
+
left: ComparedBuild
|
|
182
|
+
right: ComparedBuild
|
|
183
|
+
changes: tuple[BuildChange, ...]
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def status(self) -> str:
|
|
187
|
+
return "identical" if not self.changes else "differs"
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def same_build(self) -> bool:
|
|
191
|
+
"""Whether both sides are the one build.
|
|
192
|
+
|
|
193
|
+
Kept apart from :attr:`status` on purpose. Two builds can hold the same data in the same
|
|
194
|
+
shape and still be two builds -- made by different people, under different Receipt
|
|
195
|
+
versions -- and answering "is the data the same?" with "these are the same build" would be
|
|
196
|
+
a different, false statement.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
return self.left.candidate_digest == self.right.candidate_digest
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def column_changes(self) -> tuple[BuildChange, ...]:
|
|
203
|
+
return tuple(change for change in self.changes if change.is_column_change)
|
|
204
|
+
|
|
205
|
+
def to_dict(self, *, columns_only: bool = False) -> dict[str, Any]:
|
|
206
|
+
shown = self.column_changes if columns_only else self.changes
|
|
207
|
+
hidden = len(self.changes) - len(shown)
|
|
208
|
+
payload: dict[str, Any] = {
|
|
209
|
+
"status": _STATUS_BY_RESULT[self.status],
|
|
210
|
+
"summary": plain_comparison(self, columns_only=columns_only),
|
|
211
|
+
# The full count, always. A narrowed view narrows what is listed, never what is true.
|
|
212
|
+
"change count": len(self.changes),
|
|
213
|
+
"changes": _numbered(shown),
|
|
214
|
+
"first build": self.left.to_dict(),
|
|
215
|
+
"second build": self.right.to_dict(),
|
|
216
|
+
}
|
|
217
|
+
if hidden:
|
|
218
|
+
# A caller reading the record rather than the sentences still has to be able to tell
|
|
219
|
+
# that the list in front of it is not the whole list. The prose caveat is inside
|
|
220
|
+
# `summary`; this is the same fact in one field, so nobody has to parse prose for it.
|
|
221
|
+
payload["columns only"] = True
|
|
222
|
+
return payload
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def compare_builds(left: Path | str, right: Path | str) -> BuildComparison:
|
|
226
|
+
"""Compare two sealed builds, verifying both before comparing either.
|
|
227
|
+
|
|
228
|
+
``left`` is read as the earlier of the two and ``right`` as the later, so "added" means it is
|
|
229
|
+
in the second and not the first. Neither has to be the other's successor, and the two may
|
|
230
|
+
carry different Receipt versions.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
first = _side(left)
|
|
234
|
+
second = _side(right)
|
|
235
|
+
return BuildComparison(
|
|
236
|
+
left=first, right=second, changes=_changes(first.summary, second.summary)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def plain_comparison(comparison: BuildComparison, *, columns_only: bool = False) -> str:
|
|
241
|
+
"""The plain-language answer, written from the same changes the record carries.
|
|
242
|
+
|
|
243
|
+
A comparison with nothing to report still says a sentence. An empty screen is not an answer.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
if not comparison.changes:
|
|
247
|
+
return "\n".join(_nothing_changed_lines(comparison))
|
|
248
|
+
shown = comparison.column_changes if columns_only else comparison.changes
|
|
249
|
+
hidden = len(comparison.changes) - len(shown)
|
|
250
|
+
lines = [
|
|
251
|
+
f"Going from the first build to the second, {_things(len(comparison.changes))} changed:"
|
|
252
|
+
]
|
|
253
|
+
if shown:
|
|
254
|
+
lines.extend(f" {change.summary}" for change in shown)
|
|
255
|
+
else:
|
|
256
|
+
lines.append(" no column changed.")
|
|
257
|
+
if hidden:
|
|
258
|
+
lines.append(f" {_left_out_sentence(hidden)}")
|
|
259
|
+
return "\n".join(lines)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ------------------------------------------------------------------------------------------------
|
|
263
|
+
# Reading one side
|
|
264
|
+
# ------------------------------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _side(run_dir: Path | str) -> ComparedBuild:
|
|
268
|
+
"""Verify one side and read back everything the comparison needs from it.
|
|
269
|
+
|
|
270
|
+
``version_summary`` verifies the sealed tree and returns the shape of the data; it does not
|
|
271
|
+
carry the Receipt version or who made the build, and both belong in the answer, so
|
|
272
|
+
``verify_candidate`` is called for those. A folder with no build in it is named as such first,
|
|
273
|
+
because the verified reader's refusal for a missing tree reads like a break-in rather than the
|
|
274
|
+
typo it usually is.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
target = Path(run_dir)
|
|
278
|
+
require_a_build(target)
|
|
279
|
+
verified = verify_candidate(target)
|
|
280
|
+
return ComparedBuild(
|
|
281
|
+
run_dir=target,
|
|
282
|
+
manifest_version=verified.manifest_version,
|
|
283
|
+
candidate_digest=verified.candidate_digest,
|
|
284
|
+
producer_principal=verified.producer_principal,
|
|
285
|
+
summary=version_summary(target),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ------------------------------------------------------------------------------------------------
|
|
290
|
+
# The comparison itself
|
|
291
|
+
# ------------------------------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _changes(before: VersionSummary, after: VersionSummary) -> tuple[BuildChange, ...]:
|
|
295
|
+
"""Every difference between two verified summaries, most specific explanation first.
|
|
296
|
+
|
|
297
|
+
The shape is compared field by field over ``to_drift_dict``. Only when nothing about the shape
|
|
298
|
+
moved does the data fingerprint get its own change, and only when even that matches does the
|
|
299
|
+
recorded lineage get one: a removed column already accounts for the fingerprint that moved with
|
|
300
|
+
it, and saying so twice would read as two problems where there is one.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
changes = [
|
|
304
|
+
*_column_changes(before, after),
|
|
305
|
+
*_type_changes(before, after),
|
|
306
|
+
*_row_count_changes(before, after),
|
|
307
|
+
]
|
|
308
|
+
if changes:
|
|
309
|
+
return tuple(changes)
|
|
310
|
+
if before.table_sha256 != after.table_sha256:
|
|
311
|
+
return (
|
|
312
|
+
BuildChange(
|
|
313
|
+
code="data_changed",
|
|
314
|
+
summary=(
|
|
315
|
+
"the data changed but its shape did not: the same columns, the same types "
|
|
316
|
+
"and the same number of rows, over different data"
|
|
317
|
+
),
|
|
318
|
+
was=before.table_sha256,
|
|
319
|
+
now=after.table_sha256,
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
if before.lineage_digest != after.lineage_digest:
|
|
323
|
+
return (
|
|
324
|
+
BuildChange(
|
|
325
|
+
code="sources_changed",
|
|
326
|
+
summary=("the data is the same, but where it came from is recorded differently"),
|
|
327
|
+
was=before.lineage_digest,
|
|
328
|
+
now=after.lineage_digest,
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
return ()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _column_changes(before: VersionSummary, after: VersionSummary) -> list[BuildChange]:
|
|
335
|
+
gone = sorted(set(before.columns) - set(after.columns))
|
|
336
|
+
arrived = sorted(set(after.columns) - set(before.columns))
|
|
337
|
+
return [
|
|
338
|
+
*(
|
|
339
|
+
BuildChange(
|
|
340
|
+
code="column_removed",
|
|
341
|
+
summary=f"the column {name} was removed",
|
|
342
|
+
column=name,
|
|
343
|
+
was=dict(before.column_types).get(name),
|
|
344
|
+
)
|
|
345
|
+
for name in gone
|
|
346
|
+
),
|
|
347
|
+
*(
|
|
348
|
+
BuildChange(
|
|
349
|
+
code="column_added",
|
|
350
|
+
summary=f"the column {name} was added",
|
|
351
|
+
column=name,
|
|
352
|
+
now=dict(after.column_types).get(name),
|
|
353
|
+
)
|
|
354
|
+
for name in arrived
|
|
355
|
+
),
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _type_changes(before: VersionSummary, after: VersionSummary) -> list[BuildChange]:
|
|
360
|
+
old = dict(before.column_types)
|
|
361
|
+
new = dict(after.column_types)
|
|
362
|
+
return [
|
|
363
|
+
BuildChange(
|
|
364
|
+
code="column_type_changed",
|
|
365
|
+
summary=f"the column {name} changed type from {old[name]} to {new[name]}",
|
|
366
|
+
column=name,
|
|
367
|
+
was=old[name],
|
|
368
|
+
now=new[name],
|
|
369
|
+
)
|
|
370
|
+
for name in sorted(set(old) & set(new))
|
|
371
|
+
if old[name] != new[name]
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _row_count_changes(before: VersionSummary, after: VersionSummary) -> list[BuildChange]:
|
|
376
|
+
if before.row_count == after.row_count:
|
|
377
|
+
return []
|
|
378
|
+
difference = _signed(after.row_count - before.row_count)
|
|
379
|
+
return [
|
|
380
|
+
BuildChange(
|
|
381
|
+
code="row_count_changed",
|
|
382
|
+
summary=(
|
|
383
|
+
f"the row count went from {before.row_count} to {after.row_count} ({difference})"
|
|
384
|
+
),
|
|
385
|
+
was=before.row_count,
|
|
386
|
+
now=after.row_count,
|
|
387
|
+
difference=difference,
|
|
388
|
+
)
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ------------------------------------------------------------------------------------------------
|
|
393
|
+
# Saying it plainly
|
|
394
|
+
# ------------------------------------------------------------------------------------------------
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _nothing_changed_lines(comparison: BuildComparison) -> list[str]:
|
|
398
|
+
lines = ["Nothing about the data or its shape is different."]
|
|
399
|
+
if comparison.same_build:
|
|
400
|
+
lines.append("Both sides are the same build: one build fingerprint, read twice.")
|
|
401
|
+
return lines
|
|
402
|
+
lines.append("These are still two builds, not one: their build fingerprints differ.")
|
|
403
|
+
if comparison.left.manifest_version != comparison.right.manifest_version:
|
|
404
|
+
lines.append(
|
|
405
|
+
f"They were written under different Receipt versions: "
|
|
406
|
+
f"{comparison.left.manifest_version} and {comparison.right.manifest_version}."
|
|
407
|
+
)
|
|
408
|
+
return lines
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _left_out_sentence(hidden: int) -> str:
|
|
412
|
+
thing = "change is" if hidden == 1 else "changes are"
|
|
413
|
+
return (
|
|
414
|
+
f"only column changes were asked for, so {hidden} other {thing} not shown here; "
|
|
415
|
+
"run this again without --columns-only to see everything"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _things(count: int) -> str:
|
|
420
|
+
return "1 thing" if count == 1 else f"{count} things"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _signed(delta: int) -> str:
|
|
424
|
+
return f"+{delta}" if delta > 0 else str(delta)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _numbered(changes: tuple[BuildChange, ...]) -> dict[str, Any]:
|
|
428
|
+
width = len(str(max(len(changes), 1)))
|
|
429
|
+
return {
|
|
430
|
+
f"change {index:0{width}d}": change.to_dict()
|
|
431
|
+
for index, change in enumerate(changes, start=1)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
__all__ = [
|
|
436
|
+
"CHANGE_CODES",
|
|
437
|
+
"COLUMN_CHANGE_CODES",
|
|
438
|
+
"RELATED_DRIFT_CODES",
|
|
439
|
+
"BuildChange",
|
|
440
|
+
"BuildComparison",
|
|
441
|
+
"ComparedBuild",
|
|
442
|
+
"compare_builds",
|
|
443
|
+
"plain_comparison",
|
|
444
|
+
]
|