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,275 @@
1
+ """Group B · cell frame (B06-B11): gutter prompt, states, hover toolbar, collapse, tags, meta.
2
+
3
+ These are the chrome that wraps a cell body; the body itself is rendered by
4
+ ``code_body``/``markdown_body`` and the outputs by the ``outputs_*`` modules, then ``document``
5
+ slots them into the 62px grid this group decorates. State is expressed as box-shadow classes
6
+ (B07), never borders, so a selection can never shift layout. ``document`` calls
7
+ :func:`render_prompt` for every cell and output row, :func:`cell_state_classes` and
8
+ :func:`render_hover_toolbar` on code cells, :func:`render_exec_meta` when a cell has an execution
9
+ count, and :func:`render_tags` on tagged markdown cells; :func:`render_collapse_bar` (B09) is a
10
+ public helper with no call site in ``document``.
11
+
12
+ Every class emitted here resolves against a rule in :func:`tokens.build_css`, including
13
+ ``.nb-panel-head-left`` (the language/tags group) and ``.nb-panel-head--static`` (the
14
+ always-visible static toolbar). No literal color, size, or font is written here — the
15
+ design-system values live only in ``tokens``. Every value interpolated from notebook JSON passes
16
+ through :func:`parse.esc` at the point of interpolation.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ from mostlyright.data_harness.nbrender.parse import Cell, RenderContext, esc
24
+
25
+ # --- B06 · gutter prompt ----------------------------------------------------------------------
26
+ # The gutter prompt variants. Key == the ``nb-prompt--<key>`` CSS modifier;
27
+ # value is the literal glyph, with ``{n}`` the sole interpolation slot (the execution count).
28
+ _PROMPT_LABEL: dict[str, str] = {
29
+ "in": "In [{n}]", # executed code cell · cobalt
30
+ "running": "In [*]", # running · orange + pulse
31
+ "idle": "In [ ]", # never run · faint
32
+ "out": "Out [{n}]", # execute_result / display_data · orange
33
+ "stream": "Out [*]", # streaming · faint
34
+ "err": "Err", # error output · rust
35
+ "wgt": "Wgt", # widget output · faint
36
+ "md": "md", # markdown cell · hairline
37
+ "raw": "raw", # raw cell · hairline
38
+ }
39
+
40
+
41
+ def _derive_prompt(cell: Cell) -> tuple[str | None, int | None]:
42
+ """Pick the gutter variant for a cell from its type and execution state.
43
+
44
+ Output prompts (out/stream/err/wgt) and the live ``running`` state are not derivable from a
45
+ frozen cell — callers request them explicitly through ``variant``.
46
+ """
47
+ if cell.cell_type == "code":
48
+ if cell.execution_count is not None:
49
+ return "in", cell.execution_count
50
+ return "idle", None
51
+ if cell.cell_type == "markdown":
52
+ return "md", None
53
+ if cell.cell_type == "raw":
54
+ return "raw", None
55
+ return None, None
56
+
57
+
58
+ def render_prompt(
59
+ cell: Cell,
60
+ ctx: RenderContext,
61
+ *,
62
+ variant: str | None = None,
63
+ count: int | None = None,
64
+ ) -> str:
65
+ """B06 · gutter prompt — one of the eight variants for the 62px column, excluded from copy.
66
+
67
+ With no ``variant`` the cell decides: executed code -> ``In [n]``, unrun code -> ``In [ ]``,
68
+ markdown -> ``md``, raw -> ``raw``. Output prompts (``out``/``stream``/``err``/``wgt``) and the
69
+ live ``running`` state are requested explicitly by ``document`` when it re-enters an output into
70
+ the gutter column. The span is ``aria-hidden`` so it stays out of the accessibility tree even
71
+ when placed outside the (already aria-hidden) gutter; copy-exclusion comes from the gutter's
72
+ ``user-select: none``. An unknown cell type yields no prompt.
73
+ """
74
+ if variant is None:
75
+ variant, count = _derive_prompt(cell)
76
+ template = _PROMPT_LABEL.get(variant) if variant is not None else None
77
+ if template is None:
78
+ return ""
79
+ if "{n}" in template:
80
+ label = template.replace("{n}", esc(count) if count is not None else " ")
81
+ else:
82
+ label = template
83
+ return f'<span class="nb-prompt nb-prompt--{variant}" aria-hidden="true">{label}</span>'
84
+
85
+
86
+ # --- B07 · cell states ------------------------------------------------------------------------
87
+ # Live UI states (selected/editing/running) map to a box-shadow class on the code panel. Only the
88
+ # error state is derivable from frozen data — a cell carrying an error output always gets it.
89
+ _STATE_CLASS: dict[str, str] = {
90
+ "selected": "is-selected",
91
+ "editing": "is-selected", # panel reads selected; the gutter bar is a cell-level concern
92
+ "running": "is-running",
93
+ }
94
+
95
+
96
+ def cell_state_classes(cell: Cell, ctx: RenderContext, *, state: str | None = None) -> str:
97
+ """B07 · cell states — space-joined panel classes; ``''`` when resting.
98
+
99
+ Never emits a ``border`` class: state is box-shadow only so a selection cannot shift layout.
100
+ ``state`` names a live UI state (selected/editing/running) for connected mode; the error ring
101
+ is data-driven and combines with any live state. Every returned token resolves against a
102
+ ``.nb-panel.is-*`` rule, so the fragment is safe to concatenate onto the panel's class list.
103
+ """
104
+ classes: list[str] = []
105
+ live = _STATE_CLASS.get(state) if state is not None else None
106
+ if live is not None:
107
+ classes.append(live)
108
+ if any(out.kind == "error" for out in cell.outputs):
109
+ classes.append("is-error")
110
+ return " ".join(classes)
111
+
112
+
113
+ # --- B08 · hover toolbar ----------------------------------------------------------------------
114
+ _COPY_BTN = '<button class="nb-panel-btn" type="button">copy</button>'
115
+ _RUN_BTN = '<button class="nb-panel-btn" type="button">run</button>'
116
+ _FOLD_BTN = '<button class="nb-panel-btn" type="button">fold</button>'
117
+ _INTERRUPT_BTN = (
118
+ '<button class="nb-panel-btn nb-panel-btn--interrupt" type="button">interrupt</button>'
119
+ )
120
+
121
+
122
+ def render_hover_toolbar(
123
+ cell: Cell,
124
+ ctx: RenderContext,
125
+ *,
126
+ running: bool = False,
127
+ language: str = "python",
128
+ ) -> str:
129
+ """B08 · hover toolbar — panel header with the language label, cell tags, and ghost actions.
130
+
131
+ Left group is the language label plus the B10 tag chips. Right group is the action set:
132
+ connected mode shows ``copy`` / ``run`` / ``fold`` (``run`` becomes ``interrupt`` while the
133
+ cell runs); static and embed modes show ``copy`` only and stay visible without a hover via the
134
+ ``--static`` modifier. Opacity/hover behaviour lives entirely in the stylesheet.
135
+ """
136
+ left = (
137
+ '<div class="nb-panel-head-left">'
138
+ f'<span class="nb-panel-lang">{esc(language)}</span>'
139
+ f"{render_tags(cell, ctx)}"
140
+ "</div>"
141
+ )
142
+ if ctx.mode == "connected":
143
+ run = _INTERRUPT_BTN if running else _RUN_BTN
144
+ actions = _COPY_BTN + run + _FOLD_BTN
145
+ head_class = "nb-panel-head"
146
+ else:
147
+ actions = _COPY_BTN
148
+ head_class = "nb-panel-head nb-panel-head--static"
149
+ right = f'<div class="nb-panel-actions">{actions}</div>'
150
+ return f'<div class="{head_class}">{left}{right}</div>'
151
+
152
+
153
+ # --- B09 · collapse affordances ---------------------------------------------------------------
154
+ def _int_or_zero(value: Any) -> int:
155
+ """Coerce a hidden-line count to a non-negative int; anything malformed reads as zero."""
156
+ if isinstance(value, bool) or not isinstance(value, int):
157
+ return 0
158
+ return max(0, value)
159
+
160
+
161
+ def render_collapse_bar(cell: Cell, ctx: RenderContext, *, hidden_lines: int, kind: str) -> str:
162
+ """B09 · collapse affordance — dashed bar for a folded input or output.
163
+
164
+ ``kind`` is ``input`` (``metadata.jupyter.source_hidden``) or ``output``
165
+ (``metadata.jupyter.outputs_hidden``); ``hidden_lines`` is the caller's line count. A folded
166
+ input reading ``hide-input`` also surfaces that tag chip. The bar is a full-width button; in
167
+ static mode it is an inert affordance.
168
+ """
169
+ count = _int_or_zero(hidden_lines)
170
+ if kind == "output":
171
+ text = f"output hidden · {count} lines"
172
+ chip = ""
173
+ else:
174
+ text = f"{count} lines hidden"
175
+ chip = (
176
+ '<span class="nb-tag nb-tag--hide-input">hide-input</span>'
177
+ if "hide-input" in cell.tags
178
+ else ""
179
+ )
180
+ return (
181
+ '<button class="nb-collapse" type="button">'
182
+ '<span class="nb-collapse-caret">▸</span>'
183
+ f"<span>{esc(text)}</span>"
184
+ f"{chip}"
185
+ '<span class="nb-collapse-spacer"></span>'
186
+ '<span class="nb-collapse-show">show</span>'
187
+ "</button>"
188
+ )
189
+
190
+
191
+ # --- B10 · cell tags --------------------------------------------------------------------------
192
+ def render_tags(cell: Cell, ctx: RenderContext) -> str:
193
+ """B10 · cell tags — the four known chips from ``cell.tags`` in display order; ``''`` when none.
194
+
195
+ ``cell.tags`` has already narrowed to the known set (``cached``/``slow``/``skip``/
196
+ ``hide-input``), so each name is a safe class modifier; the chips are returned bare for the
197
+ caller to place (the B08 left group, or before a markdown body).
198
+ """
199
+ tags = cell.tags
200
+ if not tags:
201
+ return ""
202
+ return "".join(f'<span class="nb-tag nb-tag--{tag}">{esc(tag)}</span>' for tag in tags)
203
+
204
+
205
+ def render_research_activity(cell: Cell, ctx: RenderContext) -> str:
206
+ """Render only the exact persisted activity declaration carried by a research cell."""
207
+
208
+ state = cell.research_activity
209
+ if state is None:
210
+ return ""
211
+ label = "Writing" if state == "writing" else "Executing"
212
+ caret = (
213
+ '<span class="nb-activity-caret" aria-hidden="true"></span>' if state == "writing" else ""
214
+ )
215
+ return (
216
+ f'<span class="nb-activity nb-activity--{state}" data-cell-activity>'
217
+ f"{esc(label)}{caret}</span>"
218
+ )
219
+
220
+
221
+ # --- B11 · execution meta ---------------------------------------------------------------------
222
+ def _fmt_number(value: int | float) -> str:
223
+ """Compact numeric string: integers and whole floats drop the decimal point."""
224
+ if isinstance(value, int):
225
+ return str(value)
226
+ number = float(value)
227
+ return str(int(number)) if number.is_integer() else f"{number:g}"
228
+
229
+
230
+ def _exec_meta_facts(cell: Cell) -> list[str]:
231
+ """Duration / finish-time / peak-memory facts from ``metadata.mostlyright.execution``.
232
+
233
+ Keys are ``duration_s`` (seconds), ``finished_at`` (a clock string), and ``peak_mb`` (MB).
234
+ Only well-typed fields render; the free-text ``finished_at`` is escaped.
235
+ """
236
+ block = cell.metadata.get("mostlyright")
237
+ execution = block.get("execution") if isinstance(block, dict) else None
238
+ if not isinstance(execution, dict):
239
+ return []
240
+ facts: list[str] = []
241
+ duration = execution.get("duration_s")
242
+ if isinstance(duration, (int, float)) and not isinstance(duration, bool):
243
+ facts.append(f"{float(duration):.2f} s")
244
+ finished = execution.get("finished_at")
245
+ if isinstance(finished, str) and finished.strip():
246
+ facts.append(esc(finished))
247
+ peak = execution.get("peak_mb")
248
+ if isinstance(peak, (int, float)) and not isinstance(peak, bool):
249
+ facts.append(f"peak {_fmt_number(peak)} MB")
250
+ return facts
251
+
252
+
253
+ def _is_int(value: Any) -> bool:
254
+ return isinstance(value, int) and not isinstance(value, bool)
255
+
256
+
257
+ def render_exec_meta(cell: Cell, ctx: RenderContext) -> str:
258
+ """B11 · execution meta — duration/finish/peak row plus the out-of-order chip.
259
+
260
+ Facts come from cell metadata when present. When ``execution_count`` is below the previous
261
+ code cell's (``ctx.prev_execution_count``), an orange ``ran out of order`` chip is appended.
262
+ The row is omitted entirely when there is nothing to say.
263
+ """
264
+ facts = _exec_meta_facts(cell)
265
+ out_of_order = ""
266
+ if (
267
+ _is_int(cell.execution_count)
268
+ and _is_int(ctx.prev_execution_count)
269
+ and cell.execution_count < ctx.prev_execution_count # type: ignore[operator]
270
+ ):
271
+ out_of_order = '<span class="nb-exec-ooo">↯ ran out of order</span>'
272
+ if not facts and not out_of_order:
273
+ return ""
274
+ separator = '<span class="nb-exec-mid">·</span>'
275
+ return f'<div class="nb-exec-meta">{separator.join(facts)}{out_of_order}</div>'
@@ -0,0 +1,337 @@
1
+ """Group E · interactive (E25, E26): ipywidgets frozen render and the stale-output banner.
2
+
3
+ Routed to from ``document._render_output``. There is no kernel in ``static``/``embed``: E25 renders
4
+ a frozen control state from ``application/vnd.jupyter.widget-view+json`` plus widget-state metadata
5
+ when present, otherwise it dims a placeholder and pairs it with the E26 banner. When the banner is
6
+ shown the controls take ``opacity: 0.62; pointer-events: none``. Escape every label and value from
7
+ the widget JSON.
8
+
9
+ Frozen state is resolved from what a single :class:`~parse.Output` can reach — the fixed
10
+ ``render_widget(out, ctx)`` signature does not expose notebook-level ``metadata.widgets`` — so this
11
+ module reads either an embedded ``state`` object on the widget-view bundle (a Mostly Right export
12
+ convenience) or the widget's ``text/plain`` repr (``IntSlider(value=5)``). Neither present, or an
13
+ unrecognized model, degrades to the dimmed placeholder.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import Any, NamedTuple
20
+
21
+ from mostlyright.data_harness.nbrender.parse import Output, RenderContext, esc
22
+
23
+ _WIDGET_MIME = "application/vnd.jupyter.widget-view+json"
24
+
25
+ # The E26 banner's cobalt affordance is the same element that becomes a re-run button in connected
26
+ # mode; in a no-kernel export it renders inert (a span, no action) so the banner height is stable.
27
+ _STALE_ACTION = "re-run to refresh"
28
+
29
+ # A slider with no declared bounds falls back to the ipywidgets integer defaults.
30
+ _DEFAULT_MIN = 0.0
31
+ _DEFAULT_MAX = 100.0
32
+
33
+ # A checkmark glyph is not sanctioned iconography, so the checked tick is an inline SVG stroke. The
34
+ # stroke references the paper token (near-white); the ``.nb-checkbox.is-checked svg`` rule in
35
+ # ``tokens`` wins over this presentation attribute and upgrades it to pure white.
36
+ _CHECK_TICK = (
37
+ '<svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">'
38
+ '<path d="M2.5 6.2 L5 8.7 L9.5 3.3" fill="none" stroke="var(--nb-paper)" '
39
+ 'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"></path>'
40
+ "</svg>"
41
+ )
42
+
43
+ _REPR_RE = re.compile(r"^\s*([A-Za-z_]\w*)\s*\((.*)\)\s*$", re.DOTALL)
44
+
45
+
46
+ class _WidgetState(NamedTuple):
47
+ """Normalized frozen widget state, source-agnostic (embedded object or text/plain repr)."""
48
+
49
+ kind: str | None # slider | toggle | button | checkbox | output | None (unrecognized)
50
+ label: str # model/class name, used only for the placeholder label
51
+ description: str
52
+ value: Any
53
+ vmin: float | None
54
+ vmax: float | None
55
+ options: list[str]
56
+
57
+
58
+ def _classify(name: str) -> str | None:
59
+ """Map an ipywidgets model/class name to one of the five controls drawn here, else None."""
60
+ lowered = (name or "").lower()
61
+ if "slider" in lowered:
62
+ return "slider"
63
+ if "togglebuttons" in lowered: # plural group; must precede the bare "button" test below
64
+ return "toggle"
65
+ if "checkbox" in lowered:
66
+ return "checkbox"
67
+ if "button" in lowered:
68
+ return "button"
69
+ if "output" in lowered:
70
+ return "output"
71
+ return None
72
+
73
+
74
+ def _as_num(value: Any) -> float | None:
75
+ """Coerce a numeric bound to float; reject bools and non-numerics (``None`` -> use defaults)."""
76
+ if isinstance(value, bool):
77
+ return None
78
+ if isinstance(value, (int, float)):
79
+ return float(value)
80
+ return None
81
+
82
+
83
+ def _fmt_value(value: Any) -> str:
84
+ """Human value readout: bools verbatim, floats without trailing-zero noise, else ``str``."""
85
+ if isinstance(value, bool):
86
+ return "True" if value else "False"
87
+ if isinstance(value, float):
88
+ return f"{value:g}"
89
+ if value is None:
90
+ return ""
91
+ return str(value)
92
+
93
+
94
+ def _fraction(value: Any, vmin: float | None, vmax: float | None) -> float:
95
+ """Slider fill fraction in [0, 1] from value and bounds: bounds default, range clamps."""
96
+ numeric = _as_num(value)
97
+ if numeric is None:
98
+ return 0.0
99
+ lo = vmin if vmin is not None else _DEFAULT_MIN
100
+ hi = vmax if vmax is not None else _DEFAULT_MAX
101
+ if hi == lo:
102
+ return 0.0
103
+ return max(0.0, min(1.0, (numeric - lo) / (hi - lo)))
104
+
105
+
106
+ # --- state resolution -------------------------------------------------------------------------
107
+
108
+
109
+ def _split_top_level(source: str) -> list[str]:
110
+ """Split a repr arg string on top-level commas, honoring quotes and bracket nesting."""
111
+ parts: list[str] = []
112
+ buf: list[str] = []
113
+ depth = 0
114
+ quote: str | None = None
115
+ escaped = False
116
+ for ch in source:
117
+ if escaped:
118
+ buf.append(ch)
119
+ escaped = False
120
+ continue
121
+ if quote is not None:
122
+ buf.append(ch)
123
+ if ch == "\\":
124
+ escaped = True
125
+ elif ch == quote:
126
+ quote = None
127
+ continue
128
+ if ch in "\"'":
129
+ quote = ch
130
+ buf.append(ch)
131
+ elif ch in "([{":
132
+ depth += 1
133
+ buf.append(ch)
134
+ elif ch in ")]}":
135
+ depth = max(0, depth - 1)
136
+ buf.append(ch)
137
+ elif ch == "," and depth == 0:
138
+ parts.append("".join(buf))
139
+ buf = []
140
+ else:
141
+ buf.append(ch)
142
+ if buf:
143
+ parts.append("".join(buf))
144
+ return [part.strip() for part in parts if part.strip()]
145
+
146
+
147
+ def _parse_val(token: str) -> Any:
148
+ """Parse one repr scalar/sequence: quoted string, bool, None, number, or a tuple/list."""
149
+ text = token.strip()
150
+ if len(text) >= 2 and text[0] in "\"'" and text[-1] == text[0]:
151
+ return text[1:-1]
152
+ if text == "True":
153
+ return True
154
+ if text == "False":
155
+ return False
156
+ if text == "None":
157
+ return None
158
+ if len(text) >= 2 and text[0] in "([" and text[-1] in ")]":
159
+ return [_parse_val(item) for item in _split_top_level(text[1:-1])]
160
+ if re.fullmatch(r"[+-]?\d+", text):
161
+ return int(text)
162
+ try:
163
+ return float(text)
164
+ except ValueError:
165
+ return text
166
+
167
+
168
+ def _options_of(raw: Any) -> list[str]:
169
+ """Coerce an options sequence to a list of strings; anything else yields no options."""
170
+ if isinstance(raw, list):
171
+ return [str(item) for item in raw]
172
+ return []
173
+
174
+
175
+ def _from_embedded(state: dict[str, Any]) -> _WidgetState:
176
+ """Normalize an embedded widget-state object (``_model_name`` + fields)."""
177
+ model = str(state.get("_model_name", ""))
178
+ options = _options_of(state.get("_options_labels"))
179
+ if not options:
180
+ options = _options_of(state.get("options"))
181
+ value = state.get("value")
182
+ # ToggleButtons serializes the selection as an index into the labels, not a value.
183
+ index = state.get("index")
184
+ if value is None and isinstance(index, int) and not isinstance(index, bool) and options:
185
+ if 0 <= index < len(options):
186
+ value = options[index]
187
+ label = re.sub(r"Model$", "", model) or "widget"
188
+ return _WidgetState(
189
+ kind=_classify(model),
190
+ label=label,
191
+ description=str(state.get("description", "") or ""),
192
+ value=value,
193
+ vmin=_as_num(state.get("min")),
194
+ vmax=_as_num(state.get("max")),
195
+ options=options,
196
+ )
197
+
198
+
199
+ def _from_repr(text: str) -> _WidgetState | None:
200
+ """Normalize a widget ``text/plain`` repr, e.g. ``ToggleButtons(options=('a','b'))``."""
201
+ match = _REPR_RE.match(text.strip())
202
+ if match is None:
203
+ return None
204
+ class_name = match.group(1)
205
+ kwargs: dict[str, Any] = {}
206
+ for part in _split_top_level(match.group(2)):
207
+ if "=" in part:
208
+ key, _, raw = part.partition("=")
209
+ kwargs[key.strip()] = _parse_val(raw)
210
+ return _WidgetState(
211
+ kind=_classify(class_name),
212
+ label=class_name,
213
+ description=str(kwargs.get("description", "") or ""),
214
+ value=kwargs.get("value"),
215
+ vmin=_as_num(kwargs.get("min")),
216
+ vmax=_as_num(kwargs.get("max")),
217
+ options=_options_of(kwargs.get("options")),
218
+ )
219
+
220
+
221
+ def _resolve_state(out: Output) -> _WidgetState | None:
222
+ """Frozen state from an embedded view ``state`` object, else the text/plain repr, else None."""
223
+ view = out.data.get(_WIDGET_MIME)
224
+ if isinstance(view, dict):
225
+ embedded = view.get("state")
226
+ if isinstance(embedded, dict):
227
+ return _from_embedded(embedded)
228
+ repr_text = out.text_of("text/plain")
229
+ if repr_text.strip():
230
+ return _from_repr(repr_text)
231
+ return None
232
+
233
+
234
+ # --- control rows -----------------------------------------------------------------------------
235
+
236
+
237
+ def _row(label_html: str, control_html: str) -> str:
238
+ return (
239
+ '<div class="nb-widget-row">'
240
+ '<span class="nb-widget-label">' + label_html + "</span>" + control_html + "</div>"
241
+ )
242
+
243
+
244
+ def _slider_row(state: _WidgetState) -> str:
245
+ pct = f"{_fraction(state.value, state.vmin, state.vmax) * 100:g}"
246
+ control = (
247
+ '<div class="nb-slider-track" style="position: relative">'
248
+ '<div class="nb-slider-fill" style="width: ' + pct + '%"></div>'
249
+ '<div class="nb-slider-thumb" style="position: absolute; top: 50%; left: ' + pct + "%; "
250
+ 'transform: translate(-50%, -50%)"></div>'
251
+ "</div>"
252
+ '<span class="nb-widget-value">' + esc(_fmt_value(state.value)) + "</span>"
253
+ )
254
+ return _row(esc(state.description), control)
255
+
256
+
257
+ def _toggle_row(state: _WidgetState) -> str:
258
+ selected = _fmt_value(state.value)
259
+ options = state.options if state.options else ([selected] if selected else [])
260
+ segments: list[str] = []
261
+ for option in options:
262
+ cls = "nb-toggle-seg is-active" if _fmt_value(option) == selected else "nb-toggle-seg"
263
+ segments.append('<span class="' + cls + '">' + esc(str(option)) + "</span>")
264
+ control = '<div class="nb-toggle">' + "".join(segments) + "</div>"
265
+ return _row(esc(state.description), control)
266
+
267
+
268
+ def _button_row(state: _WidgetState) -> str:
269
+ # A Button carries its own label; the left description column stays empty for column alignment.
270
+ text = state.description or "run"
271
+ control = '<button type="button" class="nb-widget-btn">' + esc(text) + "</button>"
272
+ return _row("", control)
273
+
274
+
275
+ def _checkbox_row(state: _WidgetState) -> str:
276
+ checked = bool(state.value)
277
+ cls = "nb-checkbox is-checked" if checked else "nb-checkbox"
278
+ control = '<span class="' + cls + '">' + (_CHECK_TICK if checked else "") + "</span>"
279
+ return _row(esc(state.description), control)
280
+
281
+
282
+ def _placeholder_row(state: _WidgetState | None) -> str:
283
+ """Dimmed stand-in for an absent or unrecognized widget: a labeled, empty control bar."""
284
+ label = state.label if state is not None and state.label else "widget"
285
+ control = (
286
+ '<div class="nb-slider-track" style="position: relative">'
287
+ '<div class="nb-slider-fill" style="width: 0%"></div>'
288
+ "</div>"
289
+ )
290
+ return _row(esc(label), control)
291
+
292
+
293
+ def render_widget(out: Output, ctx: RenderContext) -> str:
294
+ """E25 · ipywidgets — frozen slider/toggle/button/checkbox state, or a dimmed placeholder.
295
+
296
+ State is recovered from the output alone (embedded view ``state`` object or the text/plain
297
+ repr); an unrecognized model degrades to the placeholder. ``static``/``embed`` add
298
+ ``is-frozen`` so the shared rule dims the controls and blocks pointer events — no kernel to
299
+ drive them; ``connected`` leaves them live.
300
+ """
301
+ state = _resolve_state(out)
302
+ if state is None:
303
+ row = _placeholder_row(None)
304
+ elif state.kind == "slider":
305
+ row = _slider_row(state)
306
+ elif state.kind == "toggle":
307
+ row = _toggle_row(state)
308
+ elif state.kind == "button":
309
+ row = _button_row(state)
310
+ elif state.kind == "checkbox":
311
+ row = _checkbox_row(state)
312
+ else: # recognized-but-undrawn (output) or unrecognized model
313
+ row = _placeholder_row(state)
314
+
315
+ frozen = " is-frozen" if ctx.mode in ("static", "embed") else ""
316
+ return (
317
+ '<div class="nb-widget' + frozen + '">'
318
+ '<div class="nb-widget-controls">' + row + "</div>"
319
+ "</div>"
320
+ )
321
+
322
+
323
+ def render_stale_banner(ctx: RenderContext, *, message: str) -> str:
324
+ """E26 · stale-output banner — dot, frozen-state message, cobalt text (no action in static).
325
+
326
+ ``document`` shows this only in ``static``/``embed`` (the paths with no kernel), so the cobalt
327
+ affordance renders inert: a ``span``, never a link or button. ``message`` is caller-supplied
328
+ and escaped at interpolation.
329
+ """
330
+ return (
331
+ '<div class="nb-stale">'
332
+ '<span class="nb-stale-dot"></span>'
333
+ '<span class="nb-stale-msg">' + esc(message) + "</span>"
334
+ '<span class="nb-stale-spacer"></span>'
335
+ '<span class="nb-stale-action">' + esc(_STALE_ACTION) + "</span>"
336
+ "</div>"
337
+ )