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,595 @@
|
|
|
1
|
+
"""Mostly Right data outputs rendered from portable Jupyter MIME bundles.
|
|
2
|
+
|
|
3
|
+
The column inspector is stored as a normal ``display_data`` output. Its structured MIME payload
|
|
4
|
+
is the rich contract used by this renderer; the generated notebook also carries ``text/html`` and
|
|
5
|
+
``text/plain`` fallbacks so ordinary Jupyter viewers still show useful, non-interactive content.
|
|
6
|
+
Nothing in this module reads the candidate or executes notebook code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from mostlyright.data_harness.nbrender.parse import Output, RenderContext, esc
|
|
16
|
+
from mostlyright.data_harness.nbrender.tokens import (
|
|
17
|
+
CHART_BAR_MAX_WIDTH,
|
|
18
|
+
CHART_BAR_MIN_WIDTH,
|
|
19
|
+
CHART_BASELINE,
|
|
20
|
+
CHART_LABEL_BASELINES,
|
|
21
|
+
CHART_LABEL_CHAR_WIDTH,
|
|
22
|
+
CHART_LABEL_GAP,
|
|
23
|
+
CHART_PLOT_HEIGHT,
|
|
24
|
+
CHART_PLOT_LEFT,
|
|
25
|
+
CHART_PLOT_RIGHT,
|
|
26
|
+
CHART_VIEWBOX_HEIGHT,
|
|
27
|
+
CHART_VIEWBOX_WIDTH,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
MIME = "application/vnd.mostlyright.column-profile.v1+json"
|
|
31
|
+
_MAX_CHART_ITEMS = 12
|
|
32
|
+
_MAX_COLUMNS = 256
|
|
33
|
+
_MAX_CUSTOM_MIME_BYTES = 1024 * 1024
|
|
34
|
+
_MAX_TEXT = 4096
|
|
35
|
+
_MAX_COUNT = (1 << 63) - 1
|
|
36
|
+
_SEMANTIC_AUTHORITIES = {"approved_recipe", "declared_local_plan", "none"}
|
|
37
|
+
_SEMANTIC_COMPLETENESS = {"complete", "partial", "absent"}
|
|
38
|
+
_SEMANTIC_RENDER_SCOPES = {"full", "reduced", "minimal"}
|
|
39
|
+
_UNIT_STATES = {"declared", "normalized", "not_applicable", "unknown"}
|
|
40
|
+
_CATEGORY_STATUSES = {"complete", "partial", "not_applicable", "unknown"}
|
|
41
|
+
_EVIDENCE_KINDS = {"source_id"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _bounded_payload(value: Any) -> bool:
|
|
45
|
+
"""Reject oversized/deep custom data before rendering or allocating a JSON copy."""
|
|
46
|
+
|
|
47
|
+
remaining = _MAX_CUSTOM_MIME_BYTES
|
|
48
|
+
stack: list[tuple[Any, int]] = [(value, 0)]
|
|
49
|
+
while stack:
|
|
50
|
+
item, depth = stack.pop()
|
|
51
|
+
if depth > 16:
|
|
52
|
+
return False
|
|
53
|
+
if item is None or isinstance(item, bool):
|
|
54
|
+
remaining -= 16
|
|
55
|
+
elif isinstance(item, int):
|
|
56
|
+
# Custom JSON is browser-facing. Keep integers inside the count domain used by the
|
|
57
|
+
# renderer and avoid later decimal/float conversions on attacker-sized Python ints.
|
|
58
|
+
if item.bit_length() > 63:
|
|
59
|
+
return False
|
|
60
|
+
remaining -= 16
|
|
61
|
+
elif isinstance(item, float):
|
|
62
|
+
if not math.isfinite(item):
|
|
63
|
+
return False
|
|
64
|
+
remaining -= 16
|
|
65
|
+
elif isinstance(item, str):
|
|
66
|
+
if len(item) > _MAX_CUSTOM_MIME_BYTES:
|
|
67
|
+
return False
|
|
68
|
+
remaining -= len(item.encode("utf-8")) + 2
|
|
69
|
+
elif isinstance(item, Mapping):
|
|
70
|
+
if len(item) > 1024:
|
|
71
|
+
return False
|
|
72
|
+
remaining -= len(item) * 4
|
|
73
|
+
stack.extend((key, depth + 1) for key in item)
|
|
74
|
+
stack.extend((nested, depth + 1) for nested in item.values())
|
|
75
|
+
elif isinstance(item, list):
|
|
76
|
+
if len(item) > 4096:
|
|
77
|
+
return False
|
|
78
|
+
remaining -= len(item) * 2
|
|
79
|
+
stack.extend((nested, depth + 1) for nested in item)
|
|
80
|
+
else:
|
|
81
|
+
return False
|
|
82
|
+
if remaining < 0:
|
|
83
|
+
return False
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_valid_column_profiles_payload(value: Any) -> bool:
|
|
88
|
+
"""Return whether an untrusted custom MIME value has the bounded v1 render shape."""
|
|
89
|
+
|
|
90
|
+
if not isinstance(value, Mapping) or not _bounded_payload(value):
|
|
91
|
+
return False
|
|
92
|
+
if value.get("schema_version") != "mostlyright.column-profile.v1":
|
|
93
|
+
return False
|
|
94
|
+
semantics = value.get("semantics")
|
|
95
|
+
if semantics is not None and not _valid_semantics(semantics):
|
|
96
|
+
return False
|
|
97
|
+
raw_columns = value.get("columns")
|
|
98
|
+
if not isinstance(raw_columns, list) or not 1 <= len(raw_columns) <= _MAX_COLUMNS:
|
|
99
|
+
return False
|
|
100
|
+
names: set[str] = set()
|
|
101
|
+
for profile in raw_columns:
|
|
102
|
+
if not isinstance(profile, Mapping):
|
|
103
|
+
return False
|
|
104
|
+
name = profile.get("name")
|
|
105
|
+
if not isinstance(name, str) or not name or len(name) > _MAX_TEXT or name in names:
|
|
106
|
+
return False
|
|
107
|
+
names.add(name)
|
|
108
|
+
if not all(
|
|
109
|
+
isinstance(profile.get(field), str) and len(profile[field]) <= _MAX_TEXT
|
|
110
|
+
for field in ("logical_type", "physical_type", "distinct_status")
|
|
111
|
+
):
|
|
112
|
+
return False
|
|
113
|
+
if "display_label" in profile and (
|
|
114
|
+
not isinstance(profile["display_label"], str)
|
|
115
|
+
or not 1 <= len(profile["display_label"]) <= 120
|
|
116
|
+
):
|
|
117
|
+
return False
|
|
118
|
+
if "description" in profile and (
|
|
119
|
+
not isinstance(profile["description"], str)
|
|
120
|
+
or not 1 <= len(profile["description"]) <= 1000
|
|
121
|
+
):
|
|
122
|
+
return False
|
|
123
|
+
unit = profile.get("unit")
|
|
124
|
+
if isinstance(unit, Mapping) and "state" in unit:
|
|
125
|
+
if unit.get("state") not in _UNIT_STATES:
|
|
126
|
+
return False
|
|
127
|
+
if unit.get("normalization") is not None and (
|
|
128
|
+
not isinstance(unit.get("normalization"), str) or len(unit["normalization"]) > 64
|
|
129
|
+
):
|
|
130
|
+
return False
|
|
131
|
+
if unit.get("reversible") is not None and type(unit.get("reversible")) is not bool:
|
|
132
|
+
return False
|
|
133
|
+
if (
|
|
134
|
+
"categories_status" in profile
|
|
135
|
+
and profile["categories_status"] not in _CATEGORY_STATUSES
|
|
136
|
+
):
|
|
137
|
+
return False
|
|
138
|
+
if "categories" in profile and not _valid_categories(profile["categories"]):
|
|
139
|
+
return False
|
|
140
|
+
if "evidence_refs" in profile and not _valid_evidence_refs(profile["evidence_refs"]):
|
|
141
|
+
return False
|
|
142
|
+
for field in ("row_count", "missing_count", "distinct_count", "sample_size"):
|
|
143
|
+
number = profile.get(field)
|
|
144
|
+
if (
|
|
145
|
+
isinstance(number, bool)
|
|
146
|
+
or not isinstance(number, int)
|
|
147
|
+
or not 0 <= number <= _MAX_COUNT
|
|
148
|
+
):
|
|
149
|
+
return False
|
|
150
|
+
if profile["sample_size"] > profile["row_count"]:
|
|
151
|
+
return False
|
|
152
|
+
if profile["missing_count"] > profile["row_count"]:
|
|
153
|
+
return False
|
|
154
|
+
for field in ("missing_rate", "distinct_rate"):
|
|
155
|
+
rate = profile.get(field)
|
|
156
|
+
if isinstance(rate, bool) or not isinstance(rate, (int, float)):
|
|
157
|
+
return False
|
|
158
|
+
if isinstance(rate, int):
|
|
159
|
+
if rate not in (0, 1):
|
|
160
|
+
return False
|
|
161
|
+
elif not math.isfinite(rate) or not 0 <= rate <= 1:
|
|
162
|
+
return False
|
|
163
|
+
if not all(
|
|
164
|
+
isinstance(profile.get(field), Mapping)
|
|
165
|
+
for field in ("unit", "summary", "chart", "source")
|
|
166
|
+
):
|
|
167
|
+
return False
|
|
168
|
+
operations = profile.get("transformations")
|
|
169
|
+
if not isinstance(operations, list) or any(
|
|
170
|
+
not isinstance(operation, str) or len(operation) > _MAX_TEXT for operation in operations
|
|
171
|
+
):
|
|
172
|
+
return False
|
|
173
|
+
items = profile["chart"].get("items")
|
|
174
|
+
if not isinstance(items, list) or len(items) > _MAX_CHART_ITEMS:
|
|
175
|
+
return False
|
|
176
|
+
for item in items:
|
|
177
|
+
if (
|
|
178
|
+
not isinstance(item, Mapping)
|
|
179
|
+
or not isinstance(item.get("label"), str)
|
|
180
|
+
or len(item["label"]) > _MAX_TEXT
|
|
181
|
+
):
|
|
182
|
+
return False
|
|
183
|
+
count = item.get("count")
|
|
184
|
+
if isinstance(count, bool) or not isinstance(count, (int, float)) or count < 0:
|
|
185
|
+
return False
|
|
186
|
+
if isinstance(count, int):
|
|
187
|
+
if count > _MAX_COUNT:
|
|
188
|
+
return False
|
|
189
|
+
elif not math.isfinite(count):
|
|
190
|
+
return False
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _valid_semantics(value: Any) -> bool:
|
|
195
|
+
if not isinstance(value, Mapping):
|
|
196
|
+
return False
|
|
197
|
+
required = {
|
|
198
|
+
"schema_version",
|
|
199
|
+
"authority",
|
|
200
|
+
"completeness",
|
|
201
|
+
"message",
|
|
202
|
+
"missing_column_count",
|
|
203
|
+
"column_count",
|
|
204
|
+
"render_scope",
|
|
205
|
+
}
|
|
206
|
+
allowed = required | {"summary", "grain_statement", "coverage", "limitations"}
|
|
207
|
+
if not required <= set(value) or not set(value) <= allowed:
|
|
208
|
+
return False
|
|
209
|
+
if value.get("schema_version") != "mostlyright.column-semantics.v1":
|
|
210
|
+
return False
|
|
211
|
+
if value.get("authority") not in _SEMANTIC_AUTHORITIES:
|
|
212
|
+
return False
|
|
213
|
+
if value.get("completeness") not in _SEMANTIC_COMPLETENESS:
|
|
214
|
+
return False
|
|
215
|
+
if value.get("render_scope") not in _SEMANTIC_RENDER_SCOPES:
|
|
216
|
+
return False
|
|
217
|
+
if not isinstance(value.get("message"), str) or not 1 <= len(value["message"]) <= 512:
|
|
218
|
+
return False
|
|
219
|
+
for field in ("missing_column_count", "column_count"):
|
|
220
|
+
number = value.get(field)
|
|
221
|
+
if type(number) is not int or not 0 <= number <= _MAX_COLUMNS:
|
|
222
|
+
return False
|
|
223
|
+
if value["missing_column_count"] > value["column_count"]:
|
|
224
|
+
return False
|
|
225
|
+
for field, maximum in (("summary", 1000), ("grain_statement", 400)):
|
|
226
|
+
if field in value and (
|
|
227
|
+
not isinstance(value[field], str) or not 1 <= len(value[field]) <= maximum
|
|
228
|
+
):
|
|
229
|
+
return False
|
|
230
|
+
coverage = value.get("coverage")
|
|
231
|
+
if coverage is not None:
|
|
232
|
+
if not isinstance(coverage, Mapping) or set(coverage) != {
|
|
233
|
+
"time_range",
|
|
234
|
+
"population",
|
|
235
|
+
"completeness_note",
|
|
236
|
+
}:
|
|
237
|
+
return False
|
|
238
|
+
if not isinstance(coverage.get("population"), str) or len(coverage["population"]) > 2000:
|
|
239
|
+
return False
|
|
240
|
+
if (
|
|
241
|
+
not isinstance(coverage.get("completeness_note"), str)
|
|
242
|
+
or len(coverage["completeness_note"]) > 1000
|
|
243
|
+
):
|
|
244
|
+
return False
|
|
245
|
+
time_range = coverage.get("time_range")
|
|
246
|
+
if time_range is not None and (
|
|
247
|
+
not isinstance(time_range, Mapping)
|
|
248
|
+
or set(time_range) != {"start_inclusive", "end_exclusive"}
|
|
249
|
+
or any(
|
|
250
|
+
not isinstance(time_range.get(field), str) or len(time_range[field]) > 128
|
|
251
|
+
for field in ("start_inclusive", "end_exclusive")
|
|
252
|
+
)
|
|
253
|
+
):
|
|
254
|
+
return False
|
|
255
|
+
limitations = value.get("limitations")
|
|
256
|
+
if limitations is not None and (
|
|
257
|
+
not isinstance(limitations, list)
|
|
258
|
+
or len(limitations) > 32
|
|
259
|
+
or any(not isinstance(item, str) or not 1 <= len(item) <= 1000 for item in limitations)
|
|
260
|
+
):
|
|
261
|
+
return False
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _valid_categories(value: Any) -> bool:
|
|
266
|
+
return value is None or (
|
|
267
|
+
isinstance(value, list)
|
|
268
|
+
and 1 <= len(value) <= 64
|
|
269
|
+
and all(
|
|
270
|
+
isinstance(item, Mapping)
|
|
271
|
+
and set(item) == {"code", "label"}
|
|
272
|
+
and isinstance(item.get("code"), str)
|
|
273
|
+
and 1 <= len(item["code"]) <= 64
|
|
274
|
+
and isinstance(item.get("label"), str)
|
|
275
|
+
and 1 <= len(item["label"]) <= 120
|
|
276
|
+
for item in value
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _valid_evidence_refs(value: Any) -> bool:
|
|
282
|
+
return (
|
|
283
|
+
isinstance(value, list)
|
|
284
|
+
and 1 <= len(value) <= 8
|
|
285
|
+
and all(
|
|
286
|
+
isinstance(item, Mapping)
|
|
287
|
+
and set(item) == {"kind", "ref"}
|
|
288
|
+
and item.get("kind") in _EVIDENCE_KINDS
|
|
289
|
+
and isinstance(item.get("ref"), str)
|
|
290
|
+
and 1 <= len(item["ref"]) <= 128
|
|
291
|
+
for item in value
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _mapping(value: Any) -> Mapping[str, Any]:
|
|
297
|
+
return value if isinstance(value, Mapping) else {}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _text(value: Any, fallback: str = "—") -> str:
|
|
301
|
+
if value is None or value == "":
|
|
302
|
+
return fallback
|
|
303
|
+
return str(value)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _percent(value: Any) -> str:
|
|
307
|
+
try:
|
|
308
|
+
return f"{float(value) * 100:.1f}%"
|
|
309
|
+
except (TypeError, ValueError):
|
|
310
|
+
return "—"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _fact(label: str, value: Any, detail: Any) -> str:
|
|
314
|
+
return (
|
|
315
|
+
'<div class="mr-col-fact">'
|
|
316
|
+
f'<span class="mr-col-label">{esc(label)}</span>'
|
|
317
|
+
f"<strong>{esc(_text(value))}</strong>"
|
|
318
|
+
f"<small>{esc(_text(detail))}</small>"
|
|
319
|
+
"</div>"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _semantics_banner(payload: Mapping[str, Any]) -> str:
|
|
324
|
+
semantics = _mapping(payload.get("semantics"))
|
|
325
|
+
message = _text(
|
|
326
|
+
semantics.get("message"),
|
|
327
|
+
"This Build declares no column meanings. Nothing here is inferred from column names.",
|
|
328
|
+
)
|
|
329
|
+
details: list[str] = []
|
|
330
|
+
if semantics.get("summary"):
|
|
331
|
+
details.append(f"<p><strong>Dataset.</strong> {esc(_text(semantics['summary']))}</p>")
|
|
332
|
+
if semantics.get("grain_statement"):
|
|
333
|
+
details.append(
|
|
334
|
+
f"<p><strong>One row.</strong> {esc(_text(semantics['grain_statement']))}</p>"
|
|
335
|
+
)
|
|
336
|
+
coverage = semantics.get("coverage")
|
|
337
|
+
if isinstance(coverage, Mapping):
|
|
338
|
+
time_range = coverage.get("time_range")
|
|
339
|
+
if isinstance(time_range, Mapping):
|
|
340
|
+
details.append(
|
|
341
|
+
"<p><strong>Time coverage.</strong> "
|
|
342
|
+
f"{esc(_text(time_range.get('start_inclusive'), 'Not declared'))} (inclusive) "
|
|
343
|
+
f"to {esc(_text(time_range.get('end_exclusive'), 'Not declared'))} "
|
|
344
|
+
"(exclusive).</p>"
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
details.append("<p><strong>Time coverage.</strong> Not declared.</p>")
|
|
348
|
+
details.append(
|
|
349
|
+
"<p><strong>Population.</strong> "
|
|
350
|
+
f"{esc(_text(coverage.get('population'), 'Not declared'))}</p>"
|
|
351
|
+
)
|
|
352
|
+
details.append(
|
|
353
|
+
"<p><strong>Completeness.</strong> "
|
|
354
|
+
f"{esc(_text(coverage.get('completeness_note'), 'Not declared'))}</p>"
|
|
355
|
+
)
|
|
356
|
+
limitations = semantics.get("limitations")
|
|
357
|
+
if isinstance(limitations, list):
|
|
358
|
+
if limitations:
|
|
359
|
+
details.append(
|
|
360
|
+
"<p><strong>Known limitations.</strong> "
|
|
361
|
+
+ esc("; ".join(str(item) for item in limitations))
|
|
362
|
+
+ "</p>"
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
details.append("<p><strong>Known limitations.</strong> None declared.</p>")
|
|
366
|
+
scope = semantics.get("render_scope")
|
|
367
|
+
if scope in {"reduced", "minimal"}:
|
|
368
|
+
omitted = (
|
|
369
|
+
"coverage, limitations, column descriptions, categorical definitions, and semantic "
|
|
370
|
+
"evidence references"
|
|
371
|
+
)
|
|
372
|
+
if scope == "minimal":
|
|
373
|
+
omitted = "dataset summary, row grain, column labels, and " + omitted
|
|
374
|
+
details.append(
|
|
375
|
+
"<p><strong>Interactive semantic detail truncated.</strong> "
|
|
376
|
+
f"This bounded view omits {esc(omitted)}. Open the notebook's Overview, Data "
|
|
377
|
+
"dictionary, HTML fallback, or plain-text fallback for the bounded semantic text "
|
|
378
|
+
"and explicit omission counts.</p>"
|
|
379
|
+
)
|
|
380
|
+
return (
|
|
381
|
+
'<aside class="mr-semantics-banner">'
|
|
382
|
+
f"<strong>{esc(message)}</strong>{''.join(details)}"
|
|
383
|
+
"</aside>"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _middle_elide(value: str, maximum: int) -> str:
|
|
388
|
+
"""Fit one axis label while preserving its identifying beginning and end."""
|
|
389
|
+
|
|
390
|
+
if len(value) <= maximum:
|
|
391
|
+
return value
|
|
392
|
+
visible = max(2, maximum - 1)
|
|
393
|
+
leading = (visible + 1) // 2
|
|
394
|
+
trailing = visible - leading
|
|
395
|
+
return value[:leading] + "…" + value[-trailing:]
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _chart(profile: Mapping[str, Any]) -> str:
|
|
399
|
+
chart = _mapping(profile.get("chart"))
|
|
400
|
+
raw_items = chart.get("items")
|
|
401
|
+
items = (
|
|
402
|
+
[item for item in raw_items if isinstance(item, Mapping)]
|
|
403
|
+
if isinstance(raw_items, list)
|
|
404
|
+
else []
|
|
405
|
+
)
|
|
406
|
+
items = items[:_MAX_CHART_ITEMS]
|
|
407
|
+
title = _text(chart.get("title"), "Distribution")
|
|
408
|
+
note = _text(chart.get("note"), "No chart available")
|
|
409
|
+
if not items:
|
|
410
|
+
return (
|
|
411
|
+
'<section class="mr-col-chart mr-col-chart--empty">'
|
|
412
|
+
f"<header><strong>{esc(title)}</strong><span>{esc(note)}</span></header>"
|
|
413
|
+
'<div class="mr-col-empty">This column does not need a distribution chart.</div>'
|
|
414
|
+
"</section>"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
counts: list[float] = []
|
|
418
|
+
count_texts: list[str] = []
|
|
419
|
+
for item in items:
|
|
420
|
+
raw_count = item.get("count", 0)
|
|
421
|
+
if isinstance(raw_count, int) and not isinstance(raw_count, bool):
|
|
422
|
+
bounded_count = max(0, raw_count)
|
|
423
|
+
counts.append(float(bounded_count))
|
|
424
|
+
count_texts.append(str(bounded_count))
|
|
425
|
+
continue
|
|
426
|
+
try:
|
|
427
|
+
count = max(0.0, float(raw_count))
|
|
428
|
+
counts.append(count)
|
|
429
|
+
count_texts.append(str(int(count)) if count.is_integer() else f"{count:.2f}")
|
|
430
|
+
except (OverflowError, TypeError, ValueError):
|
|
431
|
+
counts.append(0.0)
|
|
432
|
+
count_texts.append("0")
|
|
433
|
+
maximum = max(counts, default=1.0) or 1.0
|
|
434
|
+
slot = (CHART_PLOT_RIGHT - CHART_PLOT_LEFT) / max(1, len(items))
|
|
435
|
+
bar_width = min(CHART_BAR_MAX_WIDTH, max(CHART_BAR_MIN_WIDTH, slot * 0.62))
|
|
436
|
+
# Every other label shares a row. Two slots are therefore available between peers, while the
|
|
437
|
+
# symmetric outer margin bounds the first and last labels. The mono-cell estimate is
|
|
438
|
+
# deliberately conservative for the complete local system-font stack.
|
|
439
|
+
label_width = min(
|
|
440
|
+
(2 * slot) - CHART_LABEL_GAP,
|
|
441
|
+
(2 * CHART_PLOT_LEFT) + slot - CHART_LABEL_GAP,
|
|
442
|
+
)
|
|
443
|
+
label_characters = max(3, math.floor(label_width / CHART_LABEL_CHAR_WIDTH))
|
|
444
|
+
bars: list[str] = []
|
|
445
|
+
accessible_rows: list[str] = []
|
|
446
|
+
for index, (item, count, count_text) in enumerate(zip(items, counts, count_texts, strict=True)):
|
|
447
|
+
x = CHART_PLOT_LEFT + (slot * index) + ((slot - bar_width) / 2)
|
|
448
|
+
height = 3.0 if count == 0 else max(5.0, (count / maximum) * CHART_PLOT_HEIGHT)
|
|
449
|
+
y = CHART_BASELINE - height
|
|
450
|
+
label = _text(item.get("label"), "")
|
|
451
|
+
short = _middle_elide(label, label_characters)
|
|
452
|
+
label_baseline = CHART_LABEL_BASELINES[index % len(CHART_LABEL_BASELINES)]
|
|
453
|
+
rendered_label_width = min(label_width, max(1, len(short)) * CHART_LABEL_CHAR_WIDTH)
|
|
454
|
+
accessible_rows.append(
|
|
455
|
+
f'<tr><th scope="row">{esc(label)}</th><td>{esc(count_text)}</td></tr>'
|
|
456
|
+
)
|
|
457
|
+
bars.append(
|
|
458
|
+
"<g>"
|
|
459
|
+
f"<title>{esc(label)}: {esc(count_text)}</title>"
|
|
460
|
+
f'<rect x="{x:.2f}" y="{y:.2f}" width="{bar_width:.2f}" '
|
|
461
|
+
f'height="{height:.2f}" rx="4"></rect>'
|
|
462
|
+
f'<text class="mr-col-bar-value" x="{x + bar_width / 2:.2f}" y="{y - 8:.2f}" '
|
|
463
|
+
f'text-anchor="middle">{esc(count_text)}</text>'
|
|
464
|
+
f'<text class="mr-col-axis-label" x="{x + bar_width / 2:.2f}" '
|
|
465
|
+
f'y="{label_baseline:.2f}" text-anchor="middle" '
|
|
466
|
+
f'textLength="{rendered_label_width:.2f}" lengthAdjust="spacingAndGlyphs">'
|
|
467
|
+
f"{esc(short)}</text>"
|
|
468
|
+
"</g>"
|
|
469
|
+
)
|
|
470
|
+
name = _text(profile.get("name"), "column")
|
|
471
|
+
return (
|
|
472
|
+
'<section class="mr-col-chart">'
|
|
473
|
+
f"<header><strong>{esc(title)}</strong><span>{esc(note)}</span></header>"
|
|
474
|
+
f'<svg viewBox="0 0 {CHART_VIEWBOX_WIDTH:g} {CHART_VIEWBOX_HEIGHT:g}" role="img" '
|
|
475
|
+
f'aria-label="{esc(name)} {esc(title.lower())}">'
|
|
476
|
+
f'<line x1="{CHART_PLOT_LEFT}" y1="{CHART_BASELINE}" '
|
|
477
|
+
f'x2="{CHART_PLOT_RIGHT}" y2="{CHART_BASELINE}"></line>'
|
|
478
|
+
f"{''.join(bars)}</svg>"
|
|
479
|
+
'<table class="mr-col-chart-data">'
|
|
480
|
+
f"<caption>Full distribution data for {esc(name)}</caption>"
|
|
481
|
+
"<thead><tr><th>Label</th><th>Count</th></tr></thead>"
|
|
482
|
+
f"<tbody>{''.join(accessible_rows)}</tbody></table>"
|
|
483
|
+
"</section>"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _profile(profile: Mapping[str, Any], *, active: bool) -> str:
|
|
488
|
+
name = _text(profile.get("name"), "column")
|
|
489
|
+
label = _text(profile.get("display_label"), name)
|
|
490
|
+
description = _text(profile.get("description"), "No description was declared.")
|
|
491
|
+
logical = _text(profile.get("logical_type"), "unknown")
|
|
492
|
+
physical = _text(profile.get("physical_type"), "unknown")
|
|
493
|
+
unit = _mapping(profile.get("unit"))
|
|
494
|
+
unit_value = _text(unit.get("symbol"), _text(unit.get("label"), "Not declared"))
|
|
495
|
+
unit_detail = _text(unit.get("label"), "No unit metadata")
|
|
496
|
+
if unit.get("state") == "normalized":
|
|
497
|
+
reversible = "reversible" if unit.get("reversible") else "not reversible"
|
|
498
|
+
unit_detail = f"{_text(unit.get('normalization'), 'unknown normalization')} · {reversible}"
|
|
499
|
+
missing = int(profile.get("missing_count", 0) or 0)
|
|
500
|
+
distinct = int(profile.get("distinct_count", 0) or 0)
|
|
501
|
+
rows = int(profile.get("row_count", 0) or 0)
|
|
502
|
+
summary = _mapping(profile.get("summary"))
|
|
503
|
+
source = _mapping(profile.get("source"))
|
|
504
|
+
operations = profile.get("transformations")
|
|
505
|
+
operations_text = (
|
|
506
|
+
" · ".join(str(item) for item in operations) if isinstance(operations, list) else ""
|
|
507
|
+
)
|
|
508
|
+
source_id = _text(source.get("source_id"), "Not recorded")
|
|
509
|
+
source_column = _text(source.get("column"), name)
|
|
510
|
+
hidden = "" if active else " hidden"
|
|
511
|
+
active_class = " is-active" if active else ""
|
|
512
|
+
missing_detail = f"{_percent(profile.get('missing_rate'))} of {rows:,} rows"
|
|
513
|
+
distinct_detail = f"{_percent(profile.get('distinct_rate'))} unique"
|
|
514
|
+
distinct_status = _text(profile.get("distinct_status"), "exact")
|
|
515
|
+
if distinct_status != "exact":
|
|
516
|
+
distinct_detail = f"{distinct_detail} · {distinct_status}"
|
|
517
|
+
summary_label = _text(summary.get("label"), "Summary")
|
|
518
|
+
categories = profile.get("categories")
|
|
519
|
+
category_lines = ""
|
|
520
|
+
if isinstance(categories, list) and categories:
|
|
521
|
+
items = "".join(
|
|
522
|
+
f"<li><code>{esc(_text(item.get('code')))}</code> — "
|
|
523
|
+
f"{esc(_text(item.get('label')))}</li>"
|
|
524
|
+
for item in categories[:64]
|
|
525
|
+
if isinstance(item, Mapping)
|
|
526
|
+
)
|
|
527
|
+
category_lines = (
|
|
528
|
+
'<section class="mr-col-categories"><strong>Value definitions</strong>'
|
|
529
|
+
f"<ul>{items}</ul><small>"
|
|
530
|
+
f"{esc(_text(profile.get('categories_status'), 'unknown'))}</small>"
|
|
531
|
+
"</section>"
|
|
532
|
+
)
|
|
533
|
+
refs = profile.get("evidence_refs")
|
|
534
|
+
evidence_text = " · ".join(
|
|
535
|
+
f"{_text(item.get('kind'))}: {_text(item.get('ref'))}"
|
|
536
|
+
for item in refs or []
|
|
537
|
+
if isinstance(item, Mapping)
|
|
538
|
+
)
|
|
539
|
+
return (
|
|
540
|
+
f'<article class="mr-column-profile{active_class}" '
|
|
541
|
+
f'data-column-profile="{esc(name)}"{hidden}>'
|
|
542
|
+
f'<header class="mr-col-semantic"><strong>{esc(label)}</strong>'
|
|
543
|
+
f"<code>{esc(name)}</code><p>{esc(description)}</p></header>"
|
|
544
|
+
'<div class="mr-col-facts">'
|
|
545
|
+
f"{_fact('Type', logical, physical)}"
|
|
546
|
+
f"{_fact('Unit', unit_value, unit_detail)}"
|
|
547
|
+
f"{_fact('Missing', missing, missing_detail)}"
|
|
548
|
+
f"{_fact('Distinct', distinct, distinct_detail)}"
|
|
549
|
+
f"{_fact(summary_label, summary.get('value'), summary.get('detail'))}"
|
|
550
|
+
"</div>"
|
|
551
|
+
f"{_chart(profile)}"
|
|
552
|
+
f"{category_lines}"
|
|
553
|
+
'<footer class="mr-col-lineage">'
|
|
554
|
+
f"<span>Source <strong>{esc(source_id)}.{esc(source_column)}</strong></span>"
|
|
555
|
+
f"<span>Transformations <strong>{esc(operations_text or 'None')}</strong></span>"
|
|
556
|
+
f"<span>Semantic evidence <strong>{esc(evidence_text or 'Not declared')}</strong></span>"
|
|
557
|
+
"</footer>"
|
|
558
|
+
"</article>"
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def render_column_profiles(out: Output, _ctx: RenderContext) -> str:
|
|
563
|
+
"""Render the all-column inspector from its structured, versioned MIME payload."""
|
|
564
|
+
|
|
565
|
+
raw_payload = out.data.get(MIME)
|
|
566
|
+
if not is_valid_column_profiles_payload(raw_payload):
|
|
567
|
+
raise ValueError("invalid column-profile MIME payload")
|
|
568
|
+
payload = _mapping(raw_payload)
|
|
569
|
+
raw_columns = payload.get("columns")
|
|
570
|
+
columns = (
|
|
571
|
+
[column for column in raw_columns if isinstance(column, Mapping)]
|
|
572
|
+
if isinstance(raw_columns, list)
|
|
573
|
+
else []
|
|
574
|
+
)
|
|
575
|
+
if not columns:
|
|
576
|
+
return '<div class="mr-columns-empty">No column profiles were recorded.</div>'
|
|
577
|
+
options = "".join(
|
|
578
|
+
f'<option value="{esc(_text(column.get("name"), "column"))}">'
|
|
579
|
+
f"{esc(_text(column.get('display_label'), _text(column.get('name'), 'column')))}</option>"
|
|
580
|
+
for column in columns
|
|
581
|
+
)
|
|
582
|
+
profiles = "".join(_profile(column, active=index == 0) for index, column in enumerate(columns))
|
|
583
|
+
first_title = _text(columns[0].get("display_label"), _text(columns[0].get("name"), "column"))
|
|
584
|
+
return (
|
|
585
|
+
'<section class="mr-columns" data-column-inspector>'
|
|
586
|
+
f"{_semantics_banner(payload)}"
|
|
587
|
+
'<header class="mr-columns-head">'
|
|
588
|
+
"<div><span>Column inspector</span><strong data-column-title>"
|
|
589
|
+
f"{esc(first_title)}</strong></div>"
|
|
590
|
+
"<label><span>Inspect column</span>"
|
|
591
|
+
f'<select data-column-select aria-label="Inspect column">{options}</select></label>'
|
|
592
|
+
"</header>"
|
|
593
|
+
f"{profiles}"
|
|
594
|
+
"</section>"
|
|
595
|
+
)
|