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.
Files changed (314) hide show
  1. mostlyright/data_harness/__init__.py +158 -0
  2. mostlyright/data_harness/acquisition/__init__.py +55 -0
  3. mostlyright/data_harness/acquisition/http.py +2773 -0
  4. mostlyright/data_harness/acquisition/parsing.py +809 -0
  5. mostlyright/data_harness/acquisition/ranges.py +495 -0
  6. mostlyright/data_harness/acquisition/result_download.py +360 -0
  7. mostlyright/data_harness/acquisition/retention_admission.py +248 -0
  8. mostlyright/data_harness/acquisition/sandbox.py +4888 -0
  9. mostlyright/data_harness/acquisition/url_policy.py +530 -0
  10. mostlyright/data_harness/agent_runtime.py +2743 -0
  11. mostlyright/data_harness/assets/logo-ink.svg +31 -0
  12. mostlyright/data_harness/backends/__init__.py +28 -0
  13. mostlyright/data_harness/backends/pandas_backend.py +350 -0
  14. mostlyright/data_harness/backends/polars_backend.py +366 -0
  15. mostlyright/data_harness/backends/protocol.py +124 -0
  16. mostlyright/data_harness/backends/reference.py +83 -0
  17. mostlyright/data_harness/backends/registry.py +55 -0
  18. mostlyright/data_harness/backends/restrictions.py +126 -0
  19. mostlyright/data_harness/canonical.py +333 -0
  20. mostlyright/data_harness/catalog_job.py +625 -0
  21. mostlyright/data_harness/cli.py +5398 -0
  22. mostlyright/data_harness/contracts.py +53 -0
  23. mostlyright/data_harness/coordinator.py +1307 -0
  24. mostlyright/data_harness/deploy.py +924 -0
  25. mostlyright/data_harness/deploy_target.py +312 -0
  26. mostlyright/data_harness/deployment_evidence.py +1067 -0
  27. mostlyright/data_harness/event_presentation.py +576 -0
  28. mostlyright/data_harness/events.py +2152 -0
  29. mostlyright/data_harness/fast_delimited.py +239 -0
  30. mostlyright/data_harness/fleet.py +237 -0
  31. mostlyright/data_harness/formats.py +236 -0
  32. mostlyright/data_harness/governors.py +1163 -0
  33. mostlyright/data_harness/hosted_bootstrap.py +972 -0
  34. mostlyright/data_harness/hosted_crawler.py +1115 -0
  35. mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
  36. mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
  37. mostlyright/data_harness/hosted_crawler_job.py +1277 -0
  38. mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
  39. mostlyright/data_harness/hosted_dataset.py +1500 -0
  40. mostlyright/data_harness/hosted_deploy.py +3037 -0
  41. mostlyright/data_harness/hosted_handoff.py +62 -0
  42. mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
  43. mostlyright/data_harness/hosted_ingestion_job.py +356 -0
  44. mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
  45. mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
  46. mostlyright/data_harness/hosted_session_worker.py +3554 -0
  47. mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
  48. mostlyright/data_harness/hosted_worker.py +6784 -0
  49. mostlyright/data_harness/ingestion/__init__.py +56 -0
  50. mostlyright/data_harness/ingestion/contracts.py +461 -0
  51. mostlyright/data_harness/ingestion/faults.py +42 -0
  52. mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
  53. mostlyright/data_harness/ingestion/spool.py +130 -0
  54. mostlyright/data_harness/ingestion/store.py +885 -0
  55. mostlyright/data_harness/key_seam.py +434 -0
  56. mostlyright/data_harness/linux_process_boundary.py +262 -0
  57. mostlyright/data_harness/local_contracts.py +2880 -0
  58. mostlyright/data_harness/local_search/__init__.py +5 -0
  59. mostlyright/data_harness/local_search/build_index.py +1087 -0
  60. mostlyright/data_harness/local_search/contracts.py +920 -0
  61. mostlyright/data_harness/local_search/query_trace.py +266 -0
  62. mostlyright/data_harness/local_search/retrieval.py +700 -0
  63. mostlyright/data_harness/local_search/sealed.py +474 -0
  64. mostlyright/data_harness/local_search/service.py +784 -0
  65. mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
  66. mostlyright/data_harness/nbrender/__init__.py +12 -0
  67. mostlyright/data_harness/nbrender/chrome.py +359 -0
  68. mostlyright/data_harness/nbrender/code_body.py +266 -0
  69. mostlyright/data_harness/nbrender/document.py +407 -0
  70. mostlyright/data_harness/nbrender/frame.py +275 -0
  71. mostlyright/data_harness/nbrender/interactive.py +337 -0
  72. mostlyright/data_harness/nbrender/markdown_body.py +477 -0
  73. mostlyright/data_harness/nbrender/mr_components.py +134 -0
  74. mostlyright/data_harness/nbrender/outputs_data.py +595 -0
  75. mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
  76. mostlyright/data_harness/nbrender/outputs_source.py +260 -0
  77. mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
  78. mostlyright/data_harness/nbrender/outputs_text.py +400 -0
  79. mostlyright/data_harness/nbrender/parse.py +394 -0
  80. mostlyright/data_harness/nbrender/status.py +40 -0
  81. mostlyright/data_harness/nbrender/tokens.py +1295 -0
  82. mostlyright/data_harness/notebook.py +1710 -0
  83. mostlyright/data_harness/offline.py +2049 -0
  84. mostlyright/data_harness/operation_registry.py +1007 -0
  85. mostlyright/data_harness/operator_setup.py +239 -0
  86. mostlyright/data_harness/pipeline.py +6428 -0
  87. mostlyright/data_harness/plan_graph.py +2026 -0
  88. mostlyright/data_harness/preparation/__init__.py +104 -0
  89. mostlyright/data_harness/preparation/contracts.py +1017 -0
  90. mostlyright/data_harness/preparation/engine.py +221 -0
  91. mostlyright/data_harness/preparation/errors.py +14 -0
  92. mostlyright/data_harness/preparation/gates.py +751 -0
  93. mostlyright/data_harness/preparation/joins.py +574 -0
  94. mostlyright/data_harness/preparation/profile.py +384 -0
  95. mostlyright/data_harness/preparation/table.py +217 -0
  96. mostlyright/data_harness/preparation/transforms.py +568 -0
  97. mostlyright/data_harness/progress_events.py +534 -0
  98. mostlyright/data_harness/readers/__init__.py +46 -0
  99. mostlyright/data_harness/readers/containers.py +963 -0
  100. mostlyright/data_harness/readers/contracts.py +542 -0
  101. mostlyright/data_harness/readers/delimited.py +257 -0
  102. mostlyright/data_harness/readers/grib2/__init__.py +33 -0
  103. mostlyright/data_harness/readers/grib2/admission.py +722 -0
  104. mostlyright/data_harness/readers/grib2/decode.py +1009 -0
  105. mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
  106. mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
  107. mostlyright/data_harness/readers/json_tabular.py +485 -0
  108. mostlyright/data_harness/readers/registry.py +514 -0
  109. mostlyright/data_harness/readers/samples/README.md +110 -0
  110. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
  111. mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
  112. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
  113. mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
  114. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
  115. mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
  116. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
  117. mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
  118. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
  119. mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
  120. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  121. mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
  122. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
  123. mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
  124. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
  125. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
  126. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
  127. mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
  128. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
  129. mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
  130. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
  131. mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
  132. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
  133. mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
  134. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
  135. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
  136. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  137. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  138. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
  139. mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
  140. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
  141. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
  142. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
  143. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
  144. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  145. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
  146. mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
  147. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
  148. mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
  149. mostlyright/data_harness/readers/samples.py +582 -0
  150. mostlyright/data_harness/readers/spreadsheet.py +803 -0
  151. mostlyright/data_harness/readers/tabular.py +510 -0
  152. mostlyright/data_harness/recipe.py +5321 -0
  153. mostlyright/data_harness/repair/__init__.py +78 -0
  154. mostlyright/data_harness/repair/adapters.py +274 -0
  155. mostlyright/data_harness/repair/contracts.py +872 -0
  156. mostlyright/data_harness/repair/coordinator.py +1099 -0
  157. mostlyright/data_harness/repair/errors.py +16 -0
  158. mostlyright/data_harness/review.py +2533 -0
  159. mostlyright/data_harness/rowset.py +283 -0
  160. mostlyright/data_harness/serving.py +1975 -0
  161. mostlyright/data_harness/serving_edge.py +590 -0
  162. mostlyright/data_harness/serving_http.py +1031 -0
  163. mostlyright/data_harness/session_probes.py +759 -0
  164. mostlyright/data_harness/signing.py +101 -0
  165. mostlyright/data_harness/source_discovery.py +898 -0
  166. mostlyright/data_harness/sources/__init__.py +209 -0
  167. mostlyright/data_harness/sources/_adapter_steps.py +213 -0
  168. mostlyright/data_harness/sources/adapters.py +1214 -0
  169. mostlyright/data_harness/sources/cadence.py +1428 -0
  170. mostlyright/data_harness/sources/cadence_emission.py +453 -0
  171. mostlyright/data_harness/sources/cadence_history.py +546 -0
  172. mostlyright/data_harness/sources/catalog/__init__.py +17 -0
  173. mostlyright/data_harness/sources/catalog/admission.py +477 -0
  174. mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
  175. mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
  176. mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
  177. mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
  178. mostlyright/data_harness/sources/catalog/channel.py +523 -0
  179. mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
  180. mostlyright/data_harness/sources/catalog/contracts.py +825 -0
  181. mostlyright/data_harness/sources/catalog/coverage.py +137 -0
  182. mostlyright/data_harness/sources/catalog/delta.py +1340 -0
  183. mostlyright/data_harness/sources/catalog/embedding.py +532 -0
  184. mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
  185. mostlyright/data_harness/sources/catalog/fill.py +3889 -0
  186. mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
  187. mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
  188. mostlyright/data_harness/sources/catalog/gating.py +374 -0
  189. mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
  190. mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
  191. mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
  192. mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
  193. mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
  194. mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
  195. mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
  196. mostlyright/data_harness/sources/catalog/health.py +447 -0
  197. mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
  198. mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
  199. mostlyright/data_harness/sources/catalog/neural.py +1618 -0
  200. mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
  201. mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
  202. mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
  203. mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
  204. mostlyright/data_harness/sources/catalog/recommend.py +171 -0
  205. mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
  206. mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
  207. mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
  208. mostlyright/data_harness/sources/catalog/sealed.py +560 -0
  209. mostlyright/data_harness/sources/catalog/search.py +230 -0
  210. mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
  211. mostlyright/data_harness/sources/catalog/update.py +891 -0
  212. mostlyright/data_harness/sources/collections.py +815 -0
  213. mostlyright/data_harness/sources/contracts.py +2223 -0
  214. mostlyright/data_harness/sources/deletion.py +761 -0
  215. mostlyright/data_harness/sources/fitness.py +162 -0
  216. mostlyright/data_harness/sources/governance.py +163 -0
  217. mostlyright/data_harness/sources/hosted.py +173 -0
  218. mostlyright/data_harness/sources/integration.py +218 -0
  219. mostlyright/data_harness/sources/range_reader.py +418 -0
  220. mostlyright/data_harness/sources/registry.py +514 -0
  221. mostlyright/data_harness/sources/rights_rule.py +59 -0
  222. mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
  223. mostlyright/data_harness/sources/sports.py +521 -0
  224. mostlyright/data_harness/sources/stream.py +524 -0
  225. mostlyright/data_harness/sources/stream_connector.py +418 -0
  226. mostlyright/data_harness/sources/stream_recorder.py +1404 -0
  227. mostlyright/data_harness/studio_boundary.py +2019 -0
  228. mostlyright/data_harness/thin/__init__.py +37 -0
  229. mostlyright/data_harness/thin/acquire.py +1137 -0
  230. mostlyright/data_harness/thin/acquire_cancel.py +579 -0
  231. mostlyright/data_harness/thin/approvals.py +617 -0
  232. mostlyright/data_harness/thin/commands.py +406 -0
  233. mostlyright/data_harness/thin/download.py +194 -0
  234. mostlyright/data_harness/thin/narrative.py +589 -0
  235. mostlyright/data_harness/thin/parity.py +1070 -0
  236. mostlyright/data_harness/thin/propose.py +2759 -0
  237. mostlyright/data_harness/thin/research.py +1663 -0
  238. mostlyright/data_harness/thin/router.py +924 -0
  239. mostlyright/data_harness/thin/runs.py +519 -0
  240. mostlyright/data_harness/thin/session.py +281 -0
  241. mostlyright/data_harness/thin/stream.py +501 -0
  242. mostlyright/data_harness/thin/transport.py +187 -0
  243. mostlyright/data_harness/thin/vocabulary.py +368 -0
  244. mostlyright/data_harness/thin/workers.py +164 -0
  245. mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
  246. mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
  247. mostlyright/data_harness/unit_flow.py +927 -0
  248. mostlyright/data_harness/units.py +572 -0
  249. mostlyright/data_harness/ux/__init__.py +9 -0
  250. mostlyright/data_harness/ux/approve.py +485 -0
  251. mostlyright/data_harness/ux/author_yaml.py +597 -0
  252. mostlyright/data_harness/ux/cloud_auth.py +447 -0
  253. mostlyright/data_harness/ux/commands/__init__.py +260 -0
  254. mostlyright/data_harness/ux/commands/approve.py +136 -0
  255. mostlyright/data_harness/ux/commands/auth.py +744 -0
  256. mostlyright/data_harness/ux/commands/author.py +79 -0
  257. mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
  258. mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
  259. mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
  260. mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
  261. mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
  262. mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
  263. mostlyright/data_harness/ux/commands/deploy.py +134 -0
  264. mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
  265. mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
  266. mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
  267. mostlyright/data_harness/ux/commands/diff.py +74 -0
  268. mostlyright/data_harness/ux/commands/index.py +84 -0
  269. mostlyright/data_harness/ux/commands/inventory.py +47 -0
  270. mostlyright/data_harness/ux/commands/list_builds.py +143 -0
  271. mostlyright/data_harness/ux/commands/login.py +63 -0
  272. mostlyright/data_harness/ux/commands/peek.py +236 -0
  273. mostlyright/data_harness/ux/commands/plan_check.py +90 -0
  274. mostlyright/data_harness/ux/commands/preflight.py +97 -0
  275. mostlyright/data_harness/ux/commands/record.py +107 -0
  276. mostlyright/data_harness/ux/commands/review_setup.py +47 -0
  277. mostlyright/data_harness/ux/commands/search.py +440 -0
  278. mostlyright/data_harness/ux/commands/show.py +61 -0
  279. mostlyright/data_harness/ux/commands/whoami.py +37 -0
  280. mostlyright/data_harness/ux/credential_native.py +551 -0
  281. mostlyright/data_harness/ux/credential_store.py +1055 -0
  282. mostlyright/data_harness/ux/credentials.py +631 -0
  283. mostlyright/data_harness/ux/diffing.py +444 -0
  284. mostlyright/data_harness/ux/headline.py +671 -0
  285. mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
  286. mostlyright/data_harness/ux/hosted_run_status.py +619 -0
  287. mostlyright/data_harness/ux/inventory.py +427 -0
  288. mostlyright/data_harness/ux/local_review.py +375 -0
  289. mostlyright/data_harness/ux/login.py +691 -0
  290. mostlyright/data_harness/ux/path_kind.py +147 -0
  291. mostlyright/data_harness/ux/peek.py +1000 -0
  292. mostlyright/data_harness/ux/plain_file.py +178 -0
  293. mostlyright/data_harness/ux/plan_check.py +311 -0
  294. mostlyright/data_harness/ux/preflight.py +918 -0
  295. mostlyright/data_harness/ux/readers.py +1124 -0
  296. mostlyright/data_harness/ux/remediation.py +2195 -0
  297. mostlyright/data_harness/ux/render.py +657 -0
  298. mostlyright/data_harness/ux/workload.py +1077 -0
  299. mostlyright/data_harness/viewer.py +3713 -0
  300. mostlyright/data_harness/visual_run/__init__.py +83 -0
  301. mostlyright/data_harness/visual_run/authoring.py +235 -0
  302. mostlyright/data_harness/visual_run/contracts.py +673 -0
  303. mostlyright/data_harness/visual_run/materialize.py +486 -0
  304. mostlyright/data_harness/visual_run/observations.py +874 -0
  305. mostlyright/data_harness/visual_run/query.py +259 -0
  306. mostlyright/data_harness/visual_run/reducer.py +280 -0
  307. mostlyright/data_harness/visual_run/sdk.py +892 -0
  308. mostlyright/data_harness/visual_run/store.py +584 -0
  309. mostlyright/data_harness/visual_run/transport.py +239 -0
  310. mostlyright/data_harness/watch.py +2999 -0
  311. mostlyright_data-0.9.0.dist-info/METADATA +607 -0
  312. mostlyright_data-0.9.0.dist-info/RECORD +314 -0
  313. mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
  314. 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
+ )