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,266 @@
1
+ """Group C · code bodies (C12, C14, C15): code input, LaTeX math, raw cell.
2
+
3
+ Dispatched from ``document`` (markdown C13 lives in ``markdown_body``). The six-color syntax theme
4
+ is driven by the stdlib ``tokenize`` module over the cell source — never a pip highlighter — and a
5
+ tokenizer error must fall back to the escaped source, never a raw dump. Math (C14) renders TeX
6
+ source, not glyphs: no formula renderer is bundled, so the display well shows escaped TeX under the
7
+ mono label ``math · TeX source``. Every rendered string originates in notebook JSON and must pass
8
+ through :func:`parse.esc`.
9
+
10
+ Reconstruction discipline — copying a code panel must yield exactly the cell source: the highlighter
11
+ rebuilds the panel text by slicing the original source between token spans, so the concatenated
12
+ text content of the ``<pre>`` is byte-identical to ``cell.source``. Only the five accent
13
+ categories are wrapped in a span; identifiers, operators, and whitespace stay bare and inherit the
14
+ base ``#CFC9BB`` from ``.nb-code pre``. Line numbers, when enabled, live in a separate
15
+ ``aria-hidden`` element that the copy selection excludes.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import tokenize
21
+ from typing import Any
22
+
23
+ from mostlyright.data_harness.nbrender.parse import Cell, RenderContext, esc
24
+
25
+ # --- C12 six-color syntax theme ---------------------------------------------------------------
26
+ # Keywords / imports / control flow -> keyword color. True/False/None are excluded here so they
27
+ # land in the number color, which covers "number, boolean, None". The ``keyword`` module is not
28
+ # on the stdlib allow-list, so the hard-keyword set is pinned locally; Python's keywords are
29
+ # stable and soft keywords (match/case/type) are intentionally left as identifiers.
30
+ _KEYWORDS = frozenset(
31
+ {
32
+ "and",
33
+ "as",
34
+ "assert",
35
+ "async",
36
+ "await",
37
+ "break",
38
+ "class",
39
+ "continue",
40
+ "def",
41
+ "del",
42
+ "elif",
43
+ "else",
44
+ "except",
45
+ "finally",
46
+ "for",
47
+ "from",
48
+ "global",
49
+ "if",
50
+ "import",
51
+ "in",
52
+ "is",
53
+ "lambda",
54
+ "nonlocal",
55
+ "not",
56
+ "or",
57
+ "pass",
58
+ "raise",
59
+ "return",
60
+ "try",
61
+ "while",
62
+ "with",
63
+ "yield",
64
+ }
65
+ )
66
+ _CONSTANTS = frozenset({"True", "False", "None"})
67
+
68
+ # STRING plus the 3.12+ f-string token trio, resolved by name so older/newer runtimes both work.
69
+ _STRING_TYPES = {tokenize.STRING}
70
+ for _name in ("FSTRING_START", "FSTRING_MIDDLE", "FSTRING_END"):
71
+ _tok = getattr(tokenize, _name, None)
72
+ if _tok is not None:
73
+ _STRING_TYPES.add(_tok)
74
+ _STRING_TYPES = frozenset(_STRING_TYPES)
75
+
76
+ _ENCODING = getattr(tokenize, "ENCODING", None)
77
+
78
+ _TOK_COMMENT = "nb-tok-com"
79
+ _TOK_STRING = "nb-tok-str"
80
+ _TOK_NUMBER = "nb-tok-num"
81
+ _TOK_KEYWORD = "nb-tok-kw"
82
+ _TOK_FN = "nb-tok-fn"
83
+
84
+ _MATH_LABEL = "math · TeX source"
85
+
86
+
87
+ def _row_slice(lines: list[str], start: tuple[int, int], end: tuple[int, int]) -> str:
88
+ """Exact source text between two ``(row, col)`` tokenize positions (rows 1-based).
89
+
90
+ Out-of-range rows (the EOF ``NEWLINE``/``ENDMARKER`` sit one row past the last line) clamp to
91
+ an empty string so reconstruction never indexes past the source.
92
+ """
93
+ (srow, scol), (erow, ecol) = start, end
94
+ count = len(lines)
95
+
96
+ def row(index: int) -> str:
97
+ return lines[index - 1] if 1 <= index <= count else ""
98
+
99
+ if srow == erow:
100
+ return row(srow)[scol:ecol]
101
+ parts = [row(srow)[scol:]]
102
+ parts.extend(row(index) for index in range(srow + 1, erow))
103
+ parts.append(row(erow)[:ecol])
104
+ return "".join(parts)
105
+
106
+
107
+ def _is_call(toks: list[Any], index: int) -> bool:
108
+ """True when the token after ``index`` is an opening paren — a function call name.
109
+
110
+ Whitespace is not a token, so the immediate successor is the syntactically adjacent one;
111
+ ``foo(`` and ``foo (`` both qualify while ``foo.bar`` and ``foo[0]`` do not.
112
+ """
113
+ nxt = toks[index + 1] if index + 1 < len(toks) else None
114
+ return nxt is not None and nxt.type == tokenize.OP and nxt.string == "("
115
+
116
+
117
+ def _readline(source: str):
118
+ """A ``readline`` callable over ``source`` without importing ``io`` (off the allow-list)."""
119
+ lines = iter(source.splitlines(keepends=True))
120
+
121
+ def readline() -> str:
122
+ return next(lines, "")
123
+
124
+ return readline
125
+
126
+
127
+ def _highlight(source: str) -> str:
128
+ """Six-color tokenize highlight of ``source``; any tokenizer fault degrades to escaped text.
129
+
130
+ The whole tokenize-and-reconstruct is guarded: a ``TokenError``, ``IndentationError``, a null
131
+ byte, or any other malformed-source failure returns ``esc(source)`` — highlighted source and
132
+ the fallback are both exactly the escaped source, so a bad cell never blanks or dumps raw.
133
+ """
134
+ if not source:
135
+ return ""
136
+ lines = source.splitlines(keepends=True)
137
+ try:
138
+ toks = list(tokenize.generate_tokens(_readline(source)))
139
+ out: list[str] = []
140
+ last = (1, 0)
141
+ at_line_start = True
142
+ in_decorator = False
143
+ for i, tok in enumerate(toks):
144
+ ttype = tok.type
145
+ tstr = tok.string
146
+ if _ENCODING is not None and ttype == _ENCODING:
147
+ continue
148
+ start, end = tok.start, tok.end
149
+ if start >= last:
150
+ gap = _row_slice(lines, last, start)
151
+ if gap:
152
+ out.append(esc(gap))
153
+ text = _row_slice(lines, start, end)
154
+
155
+ cls: str | None = None
156
+ if ttype == tokenize.COMMENT:
157
+ cls = _TOK_COMMENT
158
+ elif ttype in _STRING_TYPES:
159
+ cls = _TOK_STRING
160
+ elif ttype == tokenize.NUMBER:
161
+ cls = _TOK_NUMBER
162
+ elif ttype == tokenize.NAME:
163
+ if tstr in _KEYWORDS:
164
+ cls = _TOK_KEYWORD
165
+ elif tstr in _CONSTANTS:
166
+ cls = _TOK_NUMBER
167
+ elif in_decorator:
168
+ cls = _TOK_FN
169
+ elif _is_call(toks, i):
170
+ cls = _TOK_FN
171
+ elif ttype == tokenize.OP:
172
+ if tstr == "@" and at_line_start:
173
+ in_decorator = True
174
+ cls = _TOK_FN
175
+ elif in_decorator and tstr == ".":
176
+ cls = _TOK_FN
177
+ elif in_decorator and tstr == "(":
178
+ in_decorator = False
179
+
180
+ if text:
181
+ out.append(f'<span class="{cls}">{esc(text)}</span>' if cls else esc(text))
182
+ if end >= last:
183
+ last = end
184
+
185
+ if ttype in (tokenize.NEWLINE, tokenize.NL):
186
+ at_line_start = True
187
+ in_decorator = False
188
+ elif ttype in (tokenize.INDENT, tokenize.DEDENT, tokenize.COMMENT):
189
+ pass # leading trivia keeps the line-start flag for a following decorator
190
+ else:
191
+ at_line_start = False
192
+ return "".join(out)
193
+ except Exception:
194
+ return esc(source)
195
+
196
+
197
+ def _wants_line_numbers(cell: Cell) -> bool:
198
+ """Line numbers are off by default; a cell opts in via ``metadata.line_numbers`` (or the
199
+ ``metadata.jupyter.line_numbers`` nesting)."""
200
+ meta = cell.metadata if isinstance(cell.metadata, dict) else {}
201
+ if meta.get("line_numbers") is True:
202
+ return True
203
+ jupyter = meta.get("jupyter")
204
+ return isinstance(jupyter, dict) and jupyter.get("line_numbers") is True
205
+
206
+
207
+ def _line_number_column(source: str) -> str:
208
+ """The right-aligned, copy-excluded line-number rail for the code panel."""
209
+ count = max(1, len(source.splitlines()))
210
+ numbers = "\n".join(str(n) for n in range(1, count + 1))
211
+ return f'<pre class="nb-lineno" aria-hidden="true">{esc(numbers)}</pre>'
212
+
213
+
214
+ def render_code_input(cell: Cell, ctx: RenderContext) -> str:
215
+ """C12 · code input — dark panel, tokenize-driven six-color theme, optional line numbers.
216
+
217
+ Returns the panel's inner content (the ``<pre>``, wrapped with a line-number rail when the
218
+ cell opts in). The enclosing ``.nb-panel.nb-code`` div — including the welded/head radius and
219
+ padding modifiers — belongs to ``document``, not to this module. The ``<pre>`` is
220
+ ``tabindex=0`` with an accessible name; its text content equals ``cell.source`` exactly so a
221
+ copy yields the source with prompts and line numbers excluded.
222
+ """
223
+ source = cell.source or ""
224
+ status = "executed" if cell.execution_count is not None else "not executed"
225
+ label = f"code cell {ctx.cell_index + 1} of {ctx.cell_count}, {status}"
226
+ pre = f'<pre tabindex="0" aria-label="{esc(label)}">{_highlight(source)}</pre>'
227
+ if _wants_line_numbers(cell):
228
+ return f'<div class="nb-code-lines">{_line_number_column(source)}{pre}</div>'
229
+ return pre
230
+
231
+
232
+ def render_math(tex: str, ctx: RenderContext, *, display: bool) -> str:
233
+ """C14 · LaTeX math — escaped TeX source in the display well, labeled ``math · TeX source``.
234
+
235
+ No formula renderer (KaTeX or otherwise) is bundled, so the well shows the escaped TeX source,
236
+ never rendered glyphs and never a raw dump. ``display`` distinguishes ``$$…$$`` block math from
237
+ inline ``$…$`` for the accessible name and a ``data-math`` hook; both use the display well.
238
+ """
239
+ kind = "display" if display else "inline"
240
+ body = esc((tex or "").strip())
241
+ return (
242
+ f'<div class="nb-math" data-math="{kind}" role="math" '
243
+ f'aria-label="{kind} math, TeX source">'
244
+ f'<span class="nb-math-label">{_MATH_LABEL}</span>'
245
+ f"<code>{body}</code>"
246
+ "</div>"
247
+ )
248
+
249
+
250
+ def render_raw(cell: Cell, ctx: RenderContext) -> str:
251
+ """C15 · raw cell — receding well, mimetype label, escaped pre. Excluded from the outline.
252
+
253
+ The mimetype comes from ``metadata.raw_mimetype`` (nbformat) or ``metadata.format`` (legacy),
254
+ defaulting to ``text/plain``; a non-string value falls back to the default. The source is
255
+ emitted verbatim through :func:`parse.esc`.
256
+ """
257
+ meta = cell.metadata if isinstance(cell.metadata, dict) else {}
258
+ mimetype = meta.get("raw_mimetype") or meta.get("format") or "text/plain"
259
+ if not isinstance(mimetype, str):
260
+ mimetype = "text/plain"
261
+ return (
262
+ '<div class="nb-raw">'
263
+ f'<span class="nb-raw-label">{esc(mimetype)}</span>'
264
+ f"<pre>{esc(cell.source or '')}</pre>"
265
+ "</div>"
266
+ )
@@ -0,0 +1,407 @@
1
+ """Top-level assembly: raw nbformat JSON to one self-contained HTML string.
2
+
3
+ This module owns the shell and layout and the ``.ipynb`` -> component routing. It renders no
4
+ component itself — it dispatches through the routing below and wraps each call so a component that
5
+ raises ``NotImplementedError`` degrades to a visible pending well instead of blanking the cell. No
6
+ component in this package raises it, so that well is a defensive fallback rather than an expected
7
+ state. Unknown mimetypes and cell types resolve to the labeled unsupported well here, never a raw
8
+ dump.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from mostlyright.data_harness.nbrender import (
17
+ chrome,
18
+ code_body,
19
+ frame,
20
+ interactive,
21
+ markdown_body,
22
+ mr_components,
23
+ outputs_data,
24
+ outputs_rich,
25
+ outputs_source,
26
+ outputs_stage,
27
+ outputs_text,
28
+ tokens,
29
+ )
30
+ from mostlyright.data_harness.nbrender.parse import (
31
+ Cell,
32
+ Notebook,
33
+ Output,
34
+ RenderContext,
35
+ coalesce_stream_outputs,
36
+ detect_tqdm,
37
+ esc,
38
+ parse_notebook,
39
+ )
40
+
41
+ _WIDGET_MIME = "application/vnd.jupyter.widget-view+json"
42
+ _COLUMN_PROFILE_MIME = "application/vnd.mostlyright.column-profile.v1+json"
43
+ _STAGE_OBSERVATION_MIMES = frozenset(outputs_stage.MIMES)
44
+ _SOURCE_OBSERVATION_MIME = outputs_source.MIME
45
+ _STALE_WIDGET_MSG = "Widget state is frozen — no live kernel in this export."
46
+
47
+
48
+ def _pending_well(component_id: str) -> str:
49
+ """Placeholder for a component that reported itself unimplemented."""
50
+ return (
51
+ f'<div class="nb-pending" data-component="{esc(component_id)}">'
52
+ f"component pending: {esc(component_id)}</div>"
53
+ )
54
+
55
+
56
+ def _unsupported_well(label: str) -> str:
57
+ """Labeled well for an unknown mimetype or cell type — never a raw dump."""
58
+ return f'<div class="nb-unsupported">unsupported output · {esc(label)}</div>'
59
+
60
+
61
+ def _slot(component_id: str, fn: Callable[..., str], *args: Any, **kwargs: Any) -> str:
62
+ """Call a component, substituting a pending well if it raises ``NotImplementedError``.
63
+
64
+ Only ``NotImplementedError`` is caught; any real fault surfaces instead of hiding behind a
65
+ well. No component raises it, so this guard is a fallback, not a routine path.
66
+ """
67
+ try:
68
+ return fn(*args, **kwargs)
69
+ except NotImplementedError:
70
+ return _pending_well(component_id)
71
+
72
+
73
+ def _attr_slot(fn: Callable[..., str], *args: Any, **kwargs: Any) -> str:
74
+ """Call a component yielding an attribute/class fragment; unimplemented falls back to ``''``."""
75
+ try:
76
+ return fn(*args, **kwargs)
77
+ except NotImplementedError:
78
+ return ""
79
+
80
+
81
+ def _render_output(out: Output, ctx: RenderContext) -> str:
82
+ """Route one output to its component by kind then mimetype, else the unsupported well."""
83
+ if out.kind == "stream-stderr":
84
+ return _slot("D17", outputs_text.render_stderr, out, ctx)
85
+ if out.kind == "stream-stdout":
86
+ if detect_tqdm(out.text) is not None:
87
+ return _slot("D24", outputs_text.render_progress, out, ctx)
88
+ return _slot("D16", outputs_text.render_stdout, out, ctx)
89
+ if out.kind == "error":
90
+ return _slot("D23", outputs_text.render_traceback, out, ctx)
91
+ if out.kind in ("execute_result", "display_data"):
92
+ mimetype = out.primary_mimetype()
93
+ if mimetype is None:
94
+ return _unsupported_well("(empty output)")
95
+ if mimetype == _WIDGET_MIME:
96
+ html = ""
97
+ if ctx.mode in ("static", "embed"):
98
+ html += _slot(
99
+ "E26", interactive.render_stale_banner, ctx, message=_STALE_WIDGET_MSG
100
+ )
101
+ return html + _slot("E25", interactive.render_widget, out, ctx)
102
+ if mimetype == _COLUMN_PROFILE_MIME:
103
+ if outputs_data.is_valid_column_profiles_payload(out.data.get(_COLUMN_PROFILE_MIME)):
104
+ return _slot("MR01", outputs_data.render_column_profiles, out, ctx)
105
+ # Custom MIME is an enhancement, never a single point of failure. Strip only the bad
106
+ # representation and route the same normal Jupyter output through its portable HTML or
107
+ # text fallback. With neither fallback, show a labeled well rather than a blank cell.
108
+ fallback = Output(
109
+ kind=out.kind,
110
+ text=out.text,
111
+ data={key: value for key, value in out.data.items() if key != _COLUMN_PROFILE_MIME},
112
+ metadata=out.metadata,
113
+ execution_count=out.execution_count,
114
+ )
115
+ return (
116
+ _render_output(fallback, ctx)
117
+ if fallback.primary_mimetype() is not None
118
+ else _unsupported_well("invalid column inspector")
119
+ )
120
+ if mimetype == _SOURCE_OBSERVATION_MIME:
121
+ if outputs_source.valid_payload(out.data.get(mimetype)):
122
+ return _slot("MR03", outputs_source.render_source_observation, out, ctx)
123
+ fallback = Output(
124
+ kind=out.kind,
125
+ text=out.text,
126
+ data={key: value for key, value in out.data.items() if key != mimetype},
127
+ metadata=out.metadata,
128
+ execution_count=out.execution_count,
129
+ )
130
+ return (
131
+ _render_output(fallback, ctx)
132
+ if fallback.primary_mimetype() is not None
133
+ else _unsupported_well("invalid source evidence")
134
+ )
135
+ if mimetype in _STAGE_OBSERVATION_MIMES:
136
+ if outputs_stage.valid_payload(out.data.get(mimetype)):
137
+ return _slot(
138
+ "MR02", outputs_stage.render_stage_observation, out, ctx, mimetype=mimetype
139
+ )
140
+ fallback = Output(
141
+ kind=out.kind,
142
+ text=out.text,
143
+ data={key: value for key, value in out.data.items() if key != mimetype},
144
+ metadata=out.metadata,
145
+ execution_count=out.execution_count,
146
+ )
147
+ return (
148
+ _render_output(fallback, ctx)
149
+ if fallback.primary_mimetype() is not None
150
+ else _unsupported_well("invalid stage observation")
151
+ )
152
+ if mimetype == "text/html":
153
+ source = out.text_of("text/html")
154
+ if 'class="dataframe"' in source or "class='dataframe'" in source:
155
+ return _slot("D19", outputs_rich.render_dataframe, out, ctx)
156
+ return _slot("D21", outputs_rich.render_html, out, ctx)
157
+ if mimetype in ("image/svg+xml", "image/png"):
158
+ return _slot("D20", outputs_rich.render_figure, out, ctx)
159
+ if mimetype == "application/json":
160
+ return _slot("D22", outputs_rich.render_json, out, ctx)
161
+ if mimetype == "text/plain":
162
+ if detect_tqdm(out.text_of("text/plain")) is not None:
163
+ return _slot("D24", outputs_text.render_progress, out, ctx)
164
+ return _slot("D18", outputs_text.render_result_text, out, ctx)
165
+ return _unsupported_well(mimetype)
166
+ return _unsupported_well("(unknown output)")
167
+
168
+
169
+ def _output_prompt_variant(out: Output, cell: Cell) -> tuple[str | None, int | None]:
170
+ """Pick the B06 gutter variant + count for an output row.
171
+
172
+ ``execute_result``/``display_data`` -> ``Out [n]`` (orange, n from the output's or the cell's
173
+ execution count); a widget bundle -> ``Wgt`` (grey); a stream -> ``Out [*]`` (grey); an error ->
174
+ ``Err`` (rust). An ``unknown`` record carries no prompt.
175
+ """
176
+ if out.kind in ("stream-stdout", "stream-stderr"):
177
+ return "stream", None
178
+ if out.kind == "error":
179
+ return "err", None
180
+ if out.kind in ("execute_result", "display_data"):
181
+ if out.primary_mimetype() == _WIDGET_MIME:
182
+ return "wgt", None
183
+ count = out.execution_count if out.execution_count is not None else cell.execution_count
184
+ return "out", count
185
+ return None, None
186
+
187
+
188
+ def _io_row(prompt: str, output_html: str) -> str:
189
+ """Re-enter an output into the 62px gutter grid so its ``Out`` prompt aligns under ``In [n]``.
190
+
191
+ The ``.nb-io`` wrapper pulls back ``margin-left: -62px`` and re-establishes the
192
+ ``[62px][content]`` grid, so ``In [n]`` and ``Out [n]`` share one pixel-aligned
193
+ column across every output type. The prompt column is ``aria-hidden`` and ``user-select:
194
+ none`` (excluded from copy).
195
+ """
196
+ return (
197
+ '<div class="nb-io">'
198
+ f'<div class="nb-io-gutter" aria-hidden="true">{prompt}</div>'
199
+ f'<div class="nb-io-body">{output_html}</div>'
200
+ "</div>"
201
+ )
202
+
203
+
204
+ def _content_row(content: str, modifier: str) -> str:
205
+ """Place a document-level block in the content column with an empty gutter cell."""
206
+ return (
207
+ f'<section class="nb-cell nb-cell--{esc(modifier)}">'
208
+ '<div class="nb-gutter" aria-hidden="true"></div>'
209
+ f'<div class="nb-content">{content}</div>'
210
+ "</section>"
211
+ )
212
+
213
+
214
+ def _is_weldable_well(out: Output) -> bool:
215
+ """A first output welds flush to the code panel only when it is a stdout well (D16).
216
+
217
+ stderr (D17) carries its own dashed-separator rule and the rich containers (df/fig/json/
218
+ traceback) own a top border, so only a non-tqdm stdout stream is the ``.nb-well`` that welds
219
+ directly beneath the panel. Every other output re-enters the gutter inside ``.nb-io``, which
220
+ owns the 10px input-to-output gap and zeroes the top margin of every child of its body, so the
221
+ gap is the same for every output shape — including one that renders as several sibling roots.
222
+ The weld itself is expressed by
223
+ the ``.nb-code--welded + .nb-well`` adjacency rule in ``build_css`` — the well's own markup is
224
+ left pristine, so this is a panel-class decision here, not a rewrite of the output.
225
+ """
226
+ return out.kind == "stream-stdout" and detect_tqdm(out.text) is None
227
+
228
+
229
+ def _render_code_cell(cell: Cell, ctx: RenderContext) -> tuple[str, str]:
230
+ """Return (gutter_html, content_html) for a code cell.
231
+
232
+ Consecutive stdout/stderr stream records are coalesced (D16) before routing; a panel header is
233
+ always present (B08 copy), so the panel takes ``nb-code--head`` for the 16px 18px pre padding;
234
+ and when the first output is a stdout well the panel takes ``nb-code--welded`` so the shared
235
+ adjacency rule squares the panel's bottom and drops the well's top seam — the two read as one
236
+ dark-to-paper unit. Every other output re-enters the gutter grid (``.nb-io``) with its B06
237
+ ``Out``/``Err``/``Wgt`` prompt so ``In [n]`` and ``Out [n]`` share the one 62px column; the
238
+ welded first stdout is the single exception (it reads as a panel continuation, with no prompt).
239
+ """
240
+ activity = cell.research_activity
241
+ gutter = _slot(
242
+ "B06",
243
+ frame.render_prompt,
244
+ cell,
245
+ ctx,
246
+ variant="running" if activity == "executing" else None,
247
+ )
248
+ state = _attr_slot(
249
+ frame.cell_state_classes,
250
+ cell,
251
+ ctx,
252
+ state="running" if activity == "executing" else None,
253
+ )
254
+ panel_head = _slot("B08", frame.render_hover_toolbar, cell, ctx)
255
+ activity_chip = _slot("B10", frame.render_research_activity, cell, ctx)
256
+ if activity_chip:
257
+ panel_head = panel_head.replace(
258
+ '<div class="nb-panel-actions">',
259
+ f'{activity_chip}<div class="nb-panel-actions">',
260
+ 1,
261
+ )
262
+ code = _slot("C12", code_body.render_code_input, cell, ctx)
263
+
264
+ outputs = coalesce_stream_outputs(cell.outputs)
265
+ welded = bool(outputs) and _is_weldable_well(outputs[0])
266
+
267
+ classes = ["nb-panel", "nb-code"]
268
+ if panel_head:
269
+ classes.append("nb-code--head")
270
+ if welded:
271
+ classes.append("nb-code--welded")
272
+ if state:
273
+ classes.append(state)
274
+ panel = f'<div class="{esc(" ".join(classes))}">{panel_head}{code}</div>'
275
+
276
+ parts = [panel]
277
+ for index, out in enumerate(outputs):
278
+ rendered = _render_output(out, ctx)
279
+ if index == 0 and welded:
280
+ parts.append(rendered) # flush beneath the panel, no gutter re-entry
281
+ continue
282
+ variant, count = _output_prompt_variant(out, cell)
283
+ prompt = (
284
+ _slot("B06", frame.render_prompt, cell, ctx, variant=variant, count=count)
285
+ if variant is not None
286
+ else ""
287
+ )
288
+ parts.append(_io_row(prompt, rendered))
289
+ if cell.execution_count is not None:
290
+ metadata = _slot("B11", frame.render_exec_meta, cell, ctx)
291
+ if metadata:
292
+ parts.append(metadata.replace(">", " data-cell-exec-meta>", 1))
293
+ return gutter, "".join(parts)
294
+
295
+
296
+ def _render_markdown_cell(cell: Cell, ctx: RenderContext) -> tuple[str, str]:
297
+ gutter = _slot("B06", frame.render_prompt, cell, ctx)
298
+ tags = _slot("B10", frame.render_tags, cell, ctx) if cell.tags else ""
299
+ body = _slot("C13", markdown_body.render_markdown, cell, ctx)
300
+ activity = _slot("B10", frame.render_research_activity, cell, ctx)
301
+ return gutter, activity + tags + body
302
+
303
+
304
+ def _render_raw_cell(cell: Cell, ctx: RenderContext) -> tuple[str, str]:
305
+ gutter = _slot("B06", frame.render_prompt, cell, ctx)
306
+ return gutter, _slot("C15", code_body.render_raw, cell, ctx)
307
+
308
+
309
+ def _render_cell(cell: Cell, ctx: RenderContext) -> str:
310
+ if cell.cell_type == "code":
311
+ gutter, content = _render_code_cell(cell, ctx)
312
+ elif cell.cell_type == "markdown":
313
+ gutter, content = _render_markdown_cell(cell, ctx)
314
+ elif cell.cell_type == "raw":
315
+ gutter, content = _render_raw_cell(cell, ctx)
316
+ else:
317
+ gutter, content = "", _unsupported_well(f"cell · {cell.cell_type}")
318
+ label = f"cell {ctx.cell_index + 1} of {ctx.cell_count}"
319
+ cell_id = cell.cell_id or f"cell-{ctx.cell_index}"
320
+ activity = cell.research_activity
321
+ activity_attr = f' data-research-activity="{esc(activity)}"' if activity else ""
322
+ activity_class = f" is-{activity}" if activity else ""
323
+ return (
324
+ f'<section class="nb-cell nb-cell--{esc(cell.cell_type)}{activity_class}" '
325
+ f'data-cell-id="{esc(cell_id)}" aria-label="{esc(label)}"{activity_attr}>'
326
+ f'<div class="nb-gutter" data-cell-gutter aria-hidden="true">{gutter}</div>'
327
+ f'<div class="nb-content">{content}</div>'
328
+ "</section>"
329
+ )
330
+
331
+
332
+ def _leakage_placement(
333
+ nb: Notebook,
334
+ ) -> tuple[dict[int, list[dict[str, Any]]], list[dict[str, Any]]]:
335
+ """Split leakage entries into per-cell (by integer ``cell`` index) and trailing groups."""
336
+ by_cell: dict[int, list[dict[str, Any]]] = {}
337
+ trailing: list[dict[str, Any]] = []
338
+ for entry in nb.leakage:
339
+ index = entry.get("cell")
340
+ if isinstance(index, int) and not isinstance(index, bool):
341
+ by_cell.setdefault(index, []).append(entry)
342
+ else:
343
+ trailing.append(entry)
344
+ return by_cell, trailing
345
+
346
+
347
+ def render_notebook(nb: dict[str, Any], mode: str = "static", filename: str = "table.ipynb") -> str:
348
+ """Render a notebook to one HTML string: shell > A01 > [A04 | cells] > A05.
349
+
350
+ ``mode`` is one of static/connected/embed; embed drops all chrome. The result is always
351
+ well-formed even against a malformed notebook — the parser degrades bad records and every
352
+ component call is guarded.
353
+ """
354
+ if mode not in tokens.VALID_MODES:
355
+ raise ValueError(f"unknown render mode: {mode!r} (expected one of {tokens.VALID_MODES})")
356
+
357
+ model = parse_notebook(nb)
358
+ doc_ctx = RenderContext(mode=mode, cell_index=-1, cell_count=len(model.cells))
359
+ css = tokens.build_css(mode)
360
+
361
+ body_parts: list[str] = []
362
+ if model.provenance is not None:
363
+ body_parts.append(_slot("F27", mr_components.render_provenance, model, doc_ctx))
364
+
365
+ by_cell, trailing_leakage = _leakage_placement(model)
366
+ prev_exec: int | None = None
367
+ for index, cell in enumerate(model.cells):
368
+ ctx = RenderContext(
369
+ mode=mode,
370
+ cell_index=index,
371
+ cell_count=len(model.cells),
372
+ prev_execution_count=prev_exec,
373
+ )
374
+ body_parts.append(_render_cell(cell, ctx))
375
+ for entry in by_cell.get(index, []):
376
+ body_parts.append(
377
+ _content_row(_slot("F28", mr_components.render_leakage, entry, ctx), "leak")
378
+ )
379
+ if cell.cell_type == "code" and cell.execution_count is not None:
380
+ prev_exec = cell.execution_count
381
+ for entry in trailing_leakage:
382
+ body_parts.append(
383
+ _content_row(_slot("F28", mr_components.render_leakage, entry, doc_ctx), "leak")
384
+ )
385
+
386
+ document = f'<div class="nb-document">{"".join(body_parts)}</div>'
387
+
388
+ outline = ""
389
+ if mode != "embed":
390
+ outline = _slot("A04", chrome.render_outline, model, doc_ctx)
391
+ body = f'<div class="nb-body">{outline}{document}</div>'
392
+
393
+ chrome_top = ""
394
+ chrome_bottom = ""
395
+ if mode != "embed":
396
+ chrome_top += _slot("A01", chrome.render_header, model, doc_ctx, filename)
397
+ if mode == "connected":
398
+ chrome_top += _slot("A02", chrome.render_toolbar, model, doc_ctx)
399
+ if mode != "embed":
400
+ chrome_bottom = _slot("A05", chrome.render_footer, model, doc_ctx)
401
+
402
+ return (
403
+ f'<div class="nb-shell nb-shell--{esc(mode)}">'
404
+ f"<style>{css}</style>"
405
+ f"{chrome_top}{body}{chrome_bottom}"
406
+ "</div>"
407
+ )