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,477 @@
1
+ """Group C · markdown (C13) and the inline entity-chip hook (F29).
2
+
3
+ Dispatched from ``document``. A hand-written, escaping-safe mini-markdown engine renders
4
+ prose — no markdown pip dependency — and every text run is escaped before any inline markup is
5
+ re-introduced, so a ``<script>`` in the source can never survive. Inline ``$…$`` hands off to
6
+ ``code_body.render_math``; ``[[type:id]]`` refs are split by :func:`parse.split_entity_refs` and
7
+ each rendered through :func:`render_entity_chip`.
8
+
9
+ Engine shape. ``render_markdown`` splits the source into blocks (heading / hr / fenced code /
10
+ blockquote / table / list / paragraph) on a line-oriented pass, then renders each block's text
11
+ through :func:`_render_inline`. The inline pass is escape-first: it isolates the constructs whose
12
+ interior must NOT be re-interpreted (code spans, math, links, images), and every remaining plain
13
+ run is escaped via :func:`parse.esc` before emphasis markers and entity chips are re-introduced.
14
+ Raw HTML in the source is therefore never passed through — it is escaped and shown verbatim.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import html
20
+ import re
21
+
22
+ from mostlyright.data_harness.nbrender import code_body
23
+ from mostlyright.data_harness.nbrender.parse import (
24
+ Cell,
25
+ EntityRef,
26
+ RenderContext,
27
+ esc,
28
+ heading_text,
29
+ slugify,
30
+ split_entity_refs,
31
+ )
32
+ from mostlyright.data_harness.nbrender.status import is_status_column, render_status
33
+
34
+ # F29 · the four entity types that earn a colored-dot chip; any other type renders as plain text.
35
+ _KNOWN_ENTITY_TYPES = ("station", "dataset", "run", "source")
36
+
37
+ # --- block classifiers ------------------------------------------------------------------------
38
+ # atx heading; deeper levels are capped at h4 because the type scale only defines h1-h4.
39
+ _HEADING_RE = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
40
+ # thematic break: three or more of the same -, * or _, spaces allowed between.
41
+ _HR_RE = re.compile(r"^ {0,3}([-*_])(?: *\1){2,} *$")
42
+ # fenced code opener: three or more backticks or tildes, optional info string.
43
+ _FENCE_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})\s*([^`]*)$")
44
+ # list item: optional indent, a bullet or ordered marker, then content.
45
+ _LIST_RE = re.compile(r"^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$")
46
+ # blockquote line: one > and an optional single space.
47
+ _QUOTE_RE = re.compile(r"^ {0,3}>\s?(.*)$")
48
+ # blockquote attribution -> <footer>: an em dash (U+2014) or a double hyphen, then text.
49
+ _FOOTER_RE = re.compile("^(?:\\u2014|--)\\s+(.*)$")
50
+ # a table body cell counts as numeric (right-aligned) when it matches this.
51
+ _NUMERIC_RE = re.compile(r"^[-+]?\$?\d[\d,]*(?:\.\d+)?%?$")
52
+
53
+ # --- inline scanner ---------------------------------------------------------------------------
54
+ # One alternative per construct, each a single named group with no capturing subgroups, so
55
+ # ``match.lastgroup`` names the construct. The URL alternative permits one level of balanced
56
+ # parens so ``(javascript:alert(1))`` is consumed whole and never leaks a trailing ``)``.
57
+ # Interiors are re-parsed by ``_PARTS`` below.
58
+ _URL = r"\((?:[^()]|\([^()]*\))*\)"
59
+ _INLINE_RE = re.compile(
60
+ r"(?P<disp>\$\$.+?\$\$)"
61
+ r"|(?P<imath>\$(?!\s)[^$\n]+?(?<!\s)\$)"
62
+ r"|(?P<code>`[^`\n]+`)"
63
+ r"|(?P<image>!\[[^\]]*\]" + _URL + r")"
64
+ r"|(?P<link>\[[^\]]*\]" + _URL + r")",
65
+ re.DOTALL,
66
+ )
67
+ # alt/text in group 1, the raw parenthesized target in group 2 (first token is the URL).
68
+ _PARTS = re.compile(r"^!?\[([^\]]*)\]\((.*)\)$", re.DOTALL)
69
+
70
+ # standalone-block forms of image and display math (a paragraph that is nothing else).
71
+ _STANDALONE_IMG_RE = re.compile(r"^!\[([^\]]*)\]" + _URL + r"$")
72
+ _STANDALONE_MATH_RE = re.compile(r"^\$\$(.+?)\$\$$", re.DOTALL)
73
+
74
+ # schemes an image src may carry; anything else falls back to the alt caption (no external fetch).
75
+ _SCHEME_RE = re.compile(r"^[a-z][a-z0-9+.\-]*:")
76
+
77
+
78
+ def render_markdown(cell: Cell, ctx: RenderContext) -> str:
79
+ """C13 · markdown — headings, lists, tables, blockquotes, inline code, links, images, math."""
80
+ return f'<div class="nb-md">{_render_blocks(cell.source, ctx)}</div>'
81
+
82
+
83
+ def render_entity_chip(ref: EntityRef, ctx: RenderContext) -> str:
84
+ """F29 · entity chip — inline mono chip with type-colored dot. Static: no popover.
85
+
86
+ The dot conveys the entity type; the chip text is the id. A type outside the four known
87
+ kinds carries no styling class, so it renders as plain escaped text rather than an unstyled
88
+ chip.
89
+ """
90
+ label = esc(ref.id)
91
+ if ref.type not in _KNOWN_ENTITY_TYPES:
92
+ return label
93
+ return (
94
+ f'<span class="nb-chip nb-chip--{ref.type}"><span class="nb-chip-dot"></span>{label}</span>'
95
+ )
96
+
97
+
98
+ # --- block layer ------------------------------------------------------------------------------
99
+
100
+
101
+ def _render_blocks(source: str, ctx: RenderContext) -> str:
102
+ lines = source.replace("\r\n", "\n").replace("\r", "\n").split("\n")
103
+ parts: list[str] = []
104
+ i = 0
105
+ n = len(lines)
106
+ while i < n:
107
+ line = lines[i]
108
+ if line.strip() == "":
109
+ i += 1
110
+ continue
111
+
112
+ fence = _FENCE_RE.match(line)
113
+ if fence:
114
+ i, block = _consume_fence(lines, i, fence.group(1)[0])
115
+ parts.append(block)
116
+ continue
117
+
118
+ heading = _HEADING_RE.match(line)
119
+ if heading:
120
+ level = min(len(heading.group(1)), 4)
121
+ # Emit a stable id so the A04 outline's ``#slug`` anchors resolve — the slug is derived
122
+ # from the SAME shared helpers chrome.py uses, so the two always agree.
123
+ slug = esc(slugify(heading_text(heading.group(2))))
124
+ parts.append(
125
+ f'<h{level} id="{slug}">{_render_inline(heading.group(2), ctx)}</h{level}>'
126
+ )
127
+ i += 1
128
+ continue
129
+
130
+ if _HR_RE.match(line):
131
+ parts.append("<hr>")
132
+ i += 1
133
+ continue
134
+
135
+ if _QUOTE_RE.match(line):
136
+ buf = []
137
+ while i < n and _QUOTE_RE.match(lines[i]):
138
+ buf.append(_QUOTE_RE.match(lines[i]).group(1))
139
+ i += 1
140
+ parts.append(_render_blockquote(buf, ctx))
141
+ continue
142
+
143
+ if _is_table_header(lines, i):
144
+ header, sep = lines[i], lines[i + 1]
145
+ i += 2
146
+ body = []
147
+ while (
148
+ i < n and lines[i].strip() != "" and "|" in lines[i] and not _starts_block(lines, i)
149
+ ):
150
+ body.append(lines[i])
151
+ i += 1
152
+ parts.append(_render_table(header, sep, body, ctx))
153
+ continue
154
+
155
+ if _LIST_RE.match(line):
156
+ buf = []
157
+ while i < n and lines[i].strip() != "":
158
+ if _LIST_RE.match(lines[i]) or lines[i].startswith((" ", "\t")):
159
+ buf.append(lines[i])
160
+ i += 1
161
+ else:
162
+ break
163
+ parts.append(_render_list(buf, ctx))
164
+ continue
165
+
166
+ buf = []
167
+ while i < n and lines[i].strip() != "" and not _starts_block(lines, i):
168
+ buf.append(lines[i])
169
+ i += 1
170
+ parts.append(_render_paragraph(buf, ctx))
171
+ return "".join(parts)
172
+
173
+
174
+ def _starts_block(lines: list[str], i: int) -> bool:
175
+ """True when line ``i`` opens a non-paragraph block, so paragraph/table collection stops."""
176
+ if i >= len(lines):
177
+ return False
178
+ line = lines[i]
179
+ if line.strip() == "":
180
+ return True
181
+ if (
182
+ _HEADING_RE.match(line)
183
+ or _HR_RE.match(line)
184
+ or _FENCE_RE.match(line)
185
+ or _QUOTE_RE.match(line)
186
+ or _LIST_RE.match(line)
187
+ ):
188
+ return True
189
+ return _is_table_header(lines, i)
190
+
191
+
192
+ def _consume_fence(lines: list[str], i: int, fence_char: str) -> tuple[int, str]:
193
+ close = re.compile(r"^ {0,3}" + re.escape(fence_char) + r"{3,}\s*$")
194
+ i += 1
195
+ buf: list[str] = []
196
+ while i < len(lines):
197
+ if close.match(lines[i]):
198
+ i += 1
199
+ break
200
+ buf.append(lines[i])
201
+ i += 1
202
+ # Fenced code is a paper-ground panel; its interior is escaped verbatim, never re-parsed.
203
+ return i, f'<pre class="nb-md-fence">{esc(chr(10).join(buf))}</pre>'
204
+
205
+
206
+ def _render_paragraph(lines: list[str], ctx: RenderContext) -> str:
207
+ raw = " ".join(part.strip() for part in lines).strip()
208
+ if not raw:
209
+ return ""
210
+ math = _STANDALONE_MATH_RE.match(raw)
211
+ if math:
212
+ return _call_math(math.group(1).strip(), raw, ctx, display=True)
213
+ image = _STANDALONE_IMG_RE.match(raw)
214
+ if image:
215
+ parts = _PARTS.match(raw)
216
+ return _render_image(parts.group(1), _first_url(parts.group(2)), block=True)
217
+ # A paragraph may contain a block-level inline result (a math well, since C14 renders even
218
+ # inline math as a display well). Emit those blocks between <p> runs so no block nests in a
219
+ # <p>; consecutive inline fragments group into one paragraph.
220
+ out: list[str] = []
221
+ buffer: list[str] = []
222
+
223
+ def flush() -> None:
224
+ text = "".join(buffer).strip()
225
+ buffer.clear()
226
+ if text:
227
+ out.append(f"<p>{text}</p>")
228
+
229
+ for is_block, fragment in _inline_segments(raw, ctx):
230
+ if is_block:
231
+ flush()
232
+ out.append(fragment)
233
+ else:
234
+ buffer.append(fragment)
235
+ flush()
236
+ return "".join(out)
237
+
238
+
239
+ def _render_blockquote(lines: list[str], ctx: RenderContext) -> str:
240
+ body: list[str] = []
241
+ footer: str | None = None
242
+ for line in lines:
243
+ match = _FOOTER_RE.match(line.strip())
244
+ if match is not None:
245
+ footer = match.group(1)
246
+ else:
247
+ body.append(line)
248
+ text = " ".join(part.strip() for part in body).strip()
249
+ html_out = f"<blockquote>{_render_inline(text, ctx)}"
250
+ if footer is not None:
251
+ html_out += f"<footer>{_render_inline(footer, ctx)}</footer>"
252
+ return html_out + "</blockquote>"
253
+
254
+
255
+ def _render_list(lines: list[str], ctx: RenderContext) -> str:
256
+ items: list[dict] = []
257
+ for line in lines:
258
+ match = _LIST_RE.match(line)
259
+ if match is None:
260
+ if items:
261
+ items[-1]["content"] += " " + line.strip()
262
+ continue
263
+ indent = len(match.group(1).expandtabs(4))
264
+ ordered = match.group(2)[0].isdigit()
265
+ content = match.group(3)
266
+ if indent >= 2 and items:
267
+ items[-1]["subs"].append({"ordered": ordered, "content": content})
268
+ else:
269
+ items.append({"ordered": ordered, "content": content, "subs": []})
270
+ if not items:
271
+ return ""
272
+ tag = "ol" if items[0]["ordered"] else "ul"
273
+ out = [f"<{tag}>"]
274
+ for item in items:
275
+ out.append(f"<li>{_render_inline(item['content'], ctx)}")
276
+ subs = item["subs"]
277
+ if subs:
278
+ subtag = "ol" if subs[0]["ordered"] else "ul"
279
+ out.append(f"<{subtag}>")
280
+ for sub in subs:
281
+ out.append(f"<li>{_render_inline(sub['content'], ctx)}</li>")
282
+ out.append(f"</{subtag}>")
283
+ out.append("</li>")
284
+ out.append(f"</{tag}>")
285
+ return "".join(out)
286
+
287
+
288
+ def _render_table(header: str, sep: str, body: list[str], ctx: RenderContext) -> str:
289
+ headers = _split_row(header)
290
+ ncol = len(headers)
291
+ rows = [_split_row(line) for line in body]
292
+ numeric = []
293
+ for col in range(ncol):
294
+ vals = [row[col] for row in rows if col < len(row) and row[col] != ""]
295
+ numeric.append(bool(vals) and all(_NUMERIC_RE.match(value) for value in vals))
296
+ head_cells = "".join(f"<th>{_render_inline(cell, ctx)}</th>" for cell in headers)
297
+ body_rows = []
298
+ for row in rows:
299
+ cells = []
300
+ for col in range(ncol):
301
+ value = row[col] if col < len(row) else ""
302
+ klass = ' class="nb-num"' if numeric[col] else ""
303
+ status = render_status(value) if is_status_column(headers[col]) else None
304
+ cells.append(f"<td{klass}>{status or _render_inline(value, ctx)}</td>")
305
+ body_rows.append(f"<tr>{''.join(cells)}</tr>")
306
+ return (
307
+ '<table class="nb-md-table">'
308
+ f"<thead><tr>{head_cells}</tr></thead>"
309
+ f"<tbody>{''.join(body_rows)}</tbody></table>"
310
+ )
311
+
312
+
313
+ def _is_table_header(lines: list[str], i: int) -> bool:
314
+ """A table is a ``|`` header row followed by a ``|`` delimiter row of dashes."""
315
+ if i + 1 >= len(lines):
316
+ return False
317
+ if "|" not in lines[i] or "|" not in lines[i + 1]:
318
+ return False
319
+ return _is_table_sep(lines[i + 1])
320
+
321
+
322
+ def _is_table_sep(line: str) -> bool:
323
+ cells = _split_row(line)
324
+ return bool(cells) and all(re.fullmatch(r":?-{1,}:?", cell) for cell in cells)
325
+
326
+
327
+ def _split_row(line: str) -> list[str]:
328
+ text = line.strip()
329
+ if text.startswith("|"):
330
+ text = text[1:]
331
+ if text.endswith("|"):
332
+ text = text[:-1]
333
+ cells: list[str] = []
334
+ current: list[str] = []
335
+ index = 0
336
+ while index < len(text):
337
+ character = text[index]
338
+ if character == "\\" and index + 1 < len(text) and text[index + 1] in {"\\", "|"}:
339
+ current.append(text[index + 1])
340
+ index += 2
341
+ continue
342
+ if character == "|":
343
+ cells.append("".join(current).strip())
344
+ current = []
345
+ else:
346
+ current.append(character)
347
+ index += 1
348
+ cells.append("".join(current).strip())
349
+ return cells
350
+
351
+
352
+ # --- inline layer -----------------------------------------------------------------------------
353
+
354
+
355
+ def _render_inline(text: str, ctx: RenderContext) -> str:
356
+ """Inline HTML for a flow context (heading/list/table/blockquote) — segments joined flat."""
357
+ return "".join(fragment for _, fragment in _inline_segments(text, ctx))
358
+
359
+
360
+ def _inline_segments(text: str, ctx: RenderContext) -> list[tuple[bool, str]]:
361
+ """Scan ``text`` into (is_block, html) fragments.
362
+
363
+ Only math can be a block: C14 renders it as a display well. ``is_block`` is decided from the
364
+ returned markup, so if C14 ever yields a truly inline span the fragment stays inline.
365
+ """
366
+ out: list[tuple[bool, str]] = []
367
+ pos = 0
368
+ for match in _INLINE_RE.finditer(text):
369
+ if match.start() > pos:
370
+ out.append((False, _render_text_run(text[pos : match.start()], ctx)))
371
+ kind = match.lastgroup
372
+ token = match.group()
373
+ if kind == "disp":
374
+ fragment = _call_math(token[2:-2].strip(), token, ctx, display=True)
375
+ out.append((_is_block_markup(fragment), fragment))
376
+ elif kind == "imath":
377
+ fragment = _call_math(token[1:-1].strip(), token, ctx, display=False)
378
+ out.append((_is_block_markup(fragment), fragment))
379
+ elif kind == "code":
380
+ out.append((False, f"<code>{esc(token[1:-1])}</code>"))
381
+ elif kind == "image":
382
+ parts = _PARTS.match(token)
383
+ image = _render_image(parts.group(1), _first_url(parts.group(2)), block=False)
384
+ out.append((False, image))
385
+ elif kind == "link":
386
+ parts = _PARTS.match(token)
387
+ out.append((False, _render_link(parts.group(1), _first_url(parts.group(2)), ctx)))
388
+ pos = match.end()
389
+ if pos < len(text):
390
+ out.append((False, _render_text_run(text[pos:], ctx)))
391
+ return out
392
+
393
+
394
+ _BLOCK_TAGS = ("<div", "<p", "<pre", "<table", "<blockquote", "<ul", "<ol")
395
+
396
+
397
+ def _is_block_markup(fragment: str) -> bool:
398
+ return fragment.lstrip().startswith(_BLOCK_TAGS)
399
+
400
+
401
+ def _first_url(target: str) -> str:
402
+ """The URL from a link/image target, dropping any optional ``"title"`` after whitespace."""
403
+ target = target.strip()
404
+ return target.split(None, 1)[0] if target else ""
405
+
406
+
407
+ def _render_text_run(text: str, ctx: RenderContext) -> str:
408
+ """A plain run: split out entity chips, escape the rest, then re-introduce emphasis."""
409
+ out: list[str] = []
410
+ for segment in split_entity_refs(text):
411
+ if isinstance(segment, EntityRef):
412
+ out.append(render_entity_chip(segment, ctx))
413
+ else:
414
+ out.append(_emphasize(esc(segment)))
415
+ return "".join(out)
416
+
417
+
418
+ def _emphasize(escaped: str) -> str:
419
+ """Apply bold/italic to already-escaped text (markers survive escaping; content stays safe)."""
420
+ escaped = re.sub(r"\*\*(\S.*?\S|\S)\*\*", r"<strong>\1</strong>", escaped)
421
+ escaped = re.sub(r"(?<![\w*])__(\S.*?\S|\S)__(?![\w*])", r"<strong>\1</strong>", escaped)
422
+ escaped = re.sub(r"(?<!\*)\*(?!\s)([^*]+?)(?<!\s)\*(?!\*)", r"<em>\1</em>", escaped)
423
+ escaped = re.sub(r"(?<![\w_])_(?!\s)([^_]+?)(?<!\s)_(?![\w_])", r"<em>\1</em>", escaped)
424
+ return escaped
425
+
426
+
427
+ def _render_link(text: str, url: str, ctx: RenderContext) -> str:
428
+ inner = _render_text_run(text, ctx)
429
+ if _href_dangerous(url):
430
+ # Neutralized: drop the anchor, keep the visible (escaped) text.
431
+ return inner
432
+ return f'<a href="{esc(url.strip())}">{inner}</a>'
433
+
434
+
435
+ def _href_dangerous(url: str) -> bool:
436
+ """True for script-bearing hrefs, after unwrapping entity obfuscation and whitespace.
437
+
438
+ Order matters: decode HTML entities first so ``java&#09;script:`` becomes ``java<tab>script:``,
439
+ then remove the whitespace/control the browser itself strips, leaving a bare ``javascript:``.
440
+ """
441
+ decoded = html.unescape(url)
442
+ collapsed = re.sub(r"[\x00-\x20]+", "", decoded).lower()
443
+ return (
444
+ collapsed.startswith("javascript:")
445
+ or collapsed.startswith("vbscript:")
446
+ or collapsed.startswith("data:text/html")
447
+ )
448
+
449
+
450
+ def _render_image(alt: str, src: str, *, block: bool) -> str:
451
+ if _img_src_allowed(src):
452
+ return f'<img src="{esc(src.strip())}" alt="{esc(alt)}">'
453
+ # Blocked src (external fetch) or missing src: the alt text becomes the caption.
454
+ caption = esc(alt) if alt.strip() else "image unavailable"
455
+ tag = "div" if block else "span"
456
+ return f'<{tag} class="nb-md-caption">{caption}</{tag}>'
457
+
458
+
459
+ def _img_src_allowed(src: str) -> bool:
460
+ """Only inline ``data:image/`` blobs and relative paths render; external URLs do not."""
461
+ text = src.strip()
462
+ if not text:
463
+ return False
464
+ lowered = text.lower()
465
+ if lowered.startswith("data:image/"):
466
+ return True
467
+ if _SCHEME_RE.match(lowered) or text.startswith("//"):
468
+ return False
469
+ return True
470
+
471
+
472
+ def _call_math(tex: str, raw: str, ctx: RenderContext, *, display: bool) -> str:
473
+ """Hand off to C14, falling back to the escaped TeX source if it is unimplemented."""
474
+ try:
475
+ return code_body.render_math(tex, ctx, display=display)
476
+ except NotImplementedError:
477
+ return f"<p>{esc(raw)}</p>" if display else esc(raw)
@@ -0,0 +1,134 @@
1
+ """Group F · Mostly Right components (F27, F28): provenance callout and leakage warning.
2
+
3
+ Dispatched from ``document.render_notebook`` (the F29 entity chip is an inline markdown hook in
4
+ ``markdown_body``). Both render only when their metadata block is present: F27 from
5
+ ``notebook.metadata.mostlyright.provenance``, F28 from ``notebook.metadata.mostlyright.leakage[]``.
6
+ A leakage entry is placed by ``document`` after the cell it references. Escape every key and value
7
+ from the metadata JSON; the leakage body permits only inline ``code`` spans, nothing else.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from typing import Any
14
+
15
+ from mostlyright.data_harness.nbrender.parse import Notebook, RenderContext, esc
16
+
17
+ # Sanctioned middot separator (U+00B7), used to detect a secondary tail in a provenance value.
18
+ _MIDDOT = "·"
19
+
20
+ # Provenance fields: metadata key -> displayed label. Rendered in this order; a field
21
+ # whose value is absent or empty is skipped so the grid never shows a labelled blank.
22
+ _PROV_FIELDS: tuple[tuple[str, str], ...] = (
23
+ ("source", "Source identity"),
24
+ ("join", "Join"),
25
+ ("revision", "Revision"),
26
+ ("cache", "Cache"),
27
+ )
28
+
29
+ # A trailing parenthetical (the short sha) is the secondary part of a value; ``re.DOTALL`` off so a
30
+ # newline never leaks into the match.
31
+ _PAREN_TAIL_RE = re.compile(r"^(.*\S)\s+(\([^()]*\))\s*$")
32
+
33
+ # Inline code spans in a leakage message. Everything outside a span is escaped plain text; the span
34
+ # contents are escaped too, so the only markup this body ever introduces is ``<code>``.
35
+ _INLINE_CODE_RE = re.compile(r"`([^`]*)`")
36
+
37
+
38
+ def _prov_mark(size: int = 15) -> str:
39
+ """The 15px brand mark with every facet the yellow token (F27 header)."""
40
+ return (
41
+ f'<svg width="{size}" height="{size}" viewBox="0 0 26 26" '
42
+ 'aria-hidden="true" focusable="false">'
43
+ '<rect x="0" y="0" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
44
+ '<rect x="15" y="0" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
45
+ '<rect x="0" y="15" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
46
+ '<rect x="15" y="15" width="11" height="11" rx="2" fill="var(--nb-yellow)"></rect>'
47
+ "</svg>"
48
+ )
49
+
50
+
51
+ def _split_secondary(value: str) -> tuple[str, str]:
52
+ """Split a value into its primary text and a dimmed secondary tail (short sha or middot suffix).
53
+
54
+ ``r7 (a1b2c3d)`` -> ``("r7", "(a1b2c3d)")``; ``warm · 4m ago`` -> ``("warm", "· 4m ago")``; a
55
+ value with neither shape stays whole.
56
+ """
57
+ match = _PAREN_TAIL_RE.match(value)
58
+ if match:
59
+ return match.group(1), match.group(2)
60
+ sep = f" {_MIDDOT} "
61
+ if sep in value:
62
+ head, _, tail = value.partition(sep)
63
+ return head, f"{_MIDDOT} {tail}"
64
+ return value, ""
65
+
66
+
67
+ def _prov_field(label: str, value: Any) -> str:
68
+ """One grid cell: mono key over mono value, with any secondary tail dimmed. Classless wrapper
69
+ so key and value stack inside a single ``nb-prov-grid`` cell."""
70
+ primary, secondary = _split_secondary(str(value))
71
+ val = '<div class="nb-prov-val">' + esc(primary)
72
+ if secondary:
73
+ val += ' <span class="nb-prov-sub">' + esc(secondary) + "</span>"
74
+ val += "</div>"
75
+ return "<div>" + '<div class="nb-prov-key">' + esc(label) + "</div>" + val + "</div>"
76
+
77
+
78
+ def render_provenance(nb: Notebook, ctx: RenderContext) -> str:
79
+ """F27 · provenance callout — dark card, brand mark, source/join/revision/cache field grid.
80
+
81
+ Renders only when ``notebook.metadata.mostlyright.provenance`` is present; absent -> empty
82
+ string (no card). Fields render in ``_PROV_FIELDS`` order; each value's short-sha / timestamp
83
+ tail is dimmed via ``nb-prov-sub``. Every value is escaped at interpolation.
84
+ """
85
+ provenance = nb.provenance
86
+ if not provenance:
87
+ return ""
88
+ fields = [
89
+ _prov_field(label, provenance[key])
90
+ for key, label in _PROV_FIELDS
91
+ if provenance.get(key) not in (None, "")
92
+ ]
93
+ head = (
94
+ '<div class="nb-prov-head">'
95
+ + _prov_mark(15)
96
+ + '<span class="nb-prov-title">Provenance</span>'
97
+ "</div>"
98
+ )
99
+ grid = '<div class="nb-prov-grid">' + "".join(fields) + "</div>"
100
+ return '<div class="nb-prov">' + head + grid + "</div>"
101
+
102
+
103
+ def _leak_body(message: str) -> str:
104
+ """Escape a leakage message, promoting only backtick spans to ``<code>`` (no other markup)."""
105
+ parts: list[str] = []
106
+ cursor = 0
107
+ for match in _INLINE_CODE_RE.finditer(message):
108
+ if match.start() > cursor:
109
+ parts.append(esc(message[cursor : match.start()]))
110
+ parts.append("<code>" + esc(match.group(1)) + "</code>")
111
+ cursor = match.end()
112
+ if cursor < len(message):
113
+ parts.append(esc(message[cursor:]))
114
+ return "".join(parts)
115
+
116
+
117
+ def render_leakage(entry: dict[str, Any], ctx: RenderContext) -> str:
118
+ """F28 · leakage warning — warn-tinted callout, badge, body with inline code spans only.
119
+
120
+ ``document`` places this after the cell the entry's integer ``cell`` index names. The body is
121
+ the entry's ``message``, escaped, with backtick spans rendered as inline ``code`` and nothing
122
+ else — no headings, links, or raw HTML survive.
123
+ """
124
+ message = entry.get("message")
125
+ body = _leak_body(str(message)) if message else ""
126
+ head = (
127
+ '<div class="nb-leak-head">'
128
+ '<span class="nb-leak-badge">!</span>'
129
+ '<span class="nb-leak-title">Leakage guard</span>'
130
+ "</div>"
131
+ )
132
+ return (
133
+ '<div class="nb-leak">' + head + '<div class="nb-leak-body">' + body + "</div>" + "</div>"
134
+ )