solara-ui 1.31.0__py2.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 (439) hide show
  1. prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
  2. prefix/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
  3. solara/__init__.py +124 -0
  4. solara/__main__.py +734 -0
  5. solara/alias.py +6 -0
  6. solara/autorouting.py +546 -0
  7. solara/cache.py +303 -0
  8. solara/checks.html +71 -0
  9. solara/checks.py +224 -0
  10. solara/comm.py +28 -0
  11. solara/components/__init__.py +59 -0
  12. solara/components/alert.py +155 -0
  13. solara/components/applayout.py +393 -0
  14. solara/components/button.py +85 -0
  15. solara/components/card.py +87 -0
  16. solara/components/checkbox.py +50 -0
  17. solara/components/code_highlight_css.py +11 -0
  18. solara/components/code_highlight_css.vue +63 -0
  19. solara/components/columns.py +159 -0
  20. solara/components/component_vue.py +110 -0
  21. solara/components/cross_filter.py +335 -0
  22. solara/components/dataframe.py +546 -0
  23. solara/components/datatable.py +221 -0
  24. solara/components/datatable.vue +175 -0
  25. solara/components/details.py +21 -0
  26. solara/components/download.vue +35 -0
  27. solara/components/echarts.py +75 -0
  28. solara/components/echarts.vue +128 -0
  29. solara/components/figure_altair.py +39 -0
  30. solara/components/file_browser.py +182 -0
  31. solara/components/file_download.py +199 -0
  32. solara/components/file_drop.py +139 -0
  33. solara/components/file_drop.vue +83 -0
  34. solara/components/file_list_widget.vue +78 -0
  35. solara/components/head.py +27 -0
  36. solara/components/head_tag.py +49 -0
  37. solara/components/head_tag.vue +60 -0
  38. solara/components/image.py +173 -0
  39. solara/components/input.py +436 -0
  40. solara/components/link.py +55 -0
  41. solara/components/markdown.py +378 -0
  42. solara/components/markdown_editor.py +25 -0
  43. solara/components/markdown_editor.vue +362 -0
  44. solara/components/matplotlib.py +74 -0
  45. solara/components/meta.py +47 -0
  46. solara/components/misc.py +333 -0
  47. solara/components/pivot_table.py +258 -0
  48. solara/components/pivot_table.vue +158 -0
  49. solara/components/progress.py +47 -0
  50. solara/components/select.py +182 -0
  51. solara/components/select.vue +27 -0
  52. solara/components/slider.py +442 -0
  53. solara/components/slider_date.vue +56 -0
  54. solara/components/spinner-solara.vue +105 -0
  55. solara/components/spinner.py +30 -0
  56. solara/components/sql_code.py +33 -0
  57. solara/components/sql_code.vue +128 -0
  58. solara/components/style.py +105 -0
  59. solara/components/switch.py +68 -0
  60. solara/components/tab_navigation.py +37 -0
  61. solara/components/title.py +90 -0
  62. solara/components/title.vue +38 -0
  63. solara/components/togglebuttons.py +200 -0
  64. solara/components/tooltip.py +61 -0
  65. solara/datatypes.py +143 -0
  66. solara/express.py +241 -0
  67. solara/hooks/__init__.py +4 -0
  68. solara/hooks/dataframe.py +99 -0
  69. solara/hooks/misc.py +263 -0
  70. solara/hooks/use_reactive.py +129 -0
  71. solara/hooks/use_thread.py +129 -0
  72. solara/kitchensink.py +8 -0
  73. solara/lab/__init__.py +34 -0
  74. solara/lab/components/__init__.py +6 -0
  75. solara/lab/components/chat.py +203 -0
  76. solara/lab/components/confirmation_dialog.py +163 -0
  77. solara/lab/components/cross_filter.py +7 -0
  78. solara/lab/components/input_date.py +298 -0
  79. solara/lab/components/menu.py +181 -0
  80. solara/lab/components/menu.vue +38 -0
  81. solara/lab/components/tabs.py +274 -0
  82. solara/lab/components/theming.py +98 -0
  83. solara/lab/components/theming.vue +72 -0
  84. solara/lab/hooks/__init__.py +0 -0
  85. solara/lab/hooks/dataframe.py +12 -0
  86. solara/lab/toestand.py +3 -0
  87. solara/lab/utils/__init__.py +2 -0
  88. solara/lab/utils/cookies.py +5 -0
  89. solara/lab/utils/dataframe.py +115 -0
  90. solara/lab/utils/headers.py +5 -0
  91. solara/layout.py +44 -0
  92. solara/lifecycle.py +46 -0
  93. solara/minisettings.py +133 -0
  94. solara/py.typed +0 -0
  95. solara/reactive.py +93 -0
  96. solara/routing.py +268 -0
  97. solara/scope/__init__.py +88 -0
  98. solara/scope/types.py +55 -0
  99. solara/server/__init__.py +0 -0
  100. solara/server/app.py +491 -0
  101. solara/server/assets/custom.css +1 -0
  102. solara/server/assets/custom.js +1 -0
  103. solara/server/assets/favicon.png +0 -0
  104. solara/server/assets/favicon.svg +5 -0
  105. solara/server/assets/style.css +1665 -0
  106. solara/server/assets/theme-dark.css +437 -0
  107. solara/server/assets/theme-light.css +420 -0
  108. solara/server/assets/theme.js +3 -0
  109. solara/server/cdn_helper.py +77 -0
  110. solara/server/esm.py +69 -0
  111. solara/server/fastapi.py +5 -0
  112. solara/server/flask.py +286 -0
  113. solara/server/jupyter/__init__.py +2 -0
  114. solara/server/jupyter/cdn_handler.py +28 -0
  115. solara/server/jupyter/server_extension.py +29 -0
  116. solara/server/jupytertools.py +46 -0
  117. solara/server/kernel.py +338 -0
  118. solara/server/kernel_context.py +357 -0
  119. solara/server/patch.py +552 -0
  120. solara/server/reload.py +242 -0
  121. solara/server/server.py +456 -0
  122. solara/server/settings.py +215 -0
  123. solara/server/shell.py +251 -0
  124. solara/server/starlette.py +601 -0
  125. solara/server/static/ansi.js +270 -0
  126. solara/server/static/highlight-dark.css +82 -0
  127. solara/server/static/highlight.css +43 -0
  128. solara/server/static/main-vuetify.js +260 -0
  129. solara/server/static/main.js +163 -0
  130. solara/server/static/solara_bootstrap.py +129 -0
  131. solara/server/static/sun.svg +23 -0
  132. solara/server/static/webworker.js +42 -0
  133. solara/server/telemetry.py +212 -0
  134. solara/server/templates/index.html.j2 +1 -0
  135. solara/server/templates/loader-plain.css +11 -0
  136. solara/server/templates/loader-plain.html +20 -0
  137. solara/server/templates/loader-solara.css +111 -0
  138. solara/server/templates/loader-solara.html +40 -0
  139. solara/server/templates/plain.html +82 -0
  140. solara/server/templates/solara.html.j2 +446 -0
  141. solara/server/threaded.py +75 -0
  142. solara/server/utils.py +30 -0
  143. solara/server/websocket.py +45 -0
  144. solara/settings.py +56 -0
  145. solara/tasks.py +837 -0
  146. solara/template/button.py +16 -0
  147. solara/template/markdown.py +42 -0
  148. solara/template/portal/.flake8 +6 -0
  149. solara/template/portal/.pre-commit-config.yaml +28 -0
  150. solara/template/portal/LICENSE +21 -0
  151. solara/template/portal/Procfile +7 -0
  152. solara/template/portal/mypy.ini +3 -0
  153. solara/template/portal/pyproject.toml +26 -0
  154. solara/template/portal/solara_portal/__init__.py +4 -0
  155. solara/template/portal/solara_portal/components/__init__.py +2 -0
  156. solara/template/portal/solara_portal/components/article.py +28 -0
  157. solara/template/portal/solara_portal/components/data.py +28 -0
  158. solara/template/portal/solara_portal/components/header.py +6 -0
  159. solara/template/portal/solara_portal/components/layout.py +6 -0
  160. solara/template/portal/solara_portal/content/articles/equis-in-vidi.md +85 -0
  161. solara/template/portal/solara_portal/content/articles/substiterat-vati.md +70 -0
  162. solara/template/portal/solara_portal/data.py +60 -0
  163. solara/template/portal/solara_portal/pages/__init__.py +67 -0
  164. solara/template/portal/solara_portal/pages/article/__init__.py +26 -0
  165. solara/template/portal/solara_portal/pages/tabular.py +29 -0
  166. solara/template/portal/solara_portal/pages/viz/__init__.py +70 -0
  167. solara/template/portal/solara_portal/pages/viz/overview.py +14 -0
  168. solara/test/__init__.py +0 -0
  169. solara/test/pytest_plugin.py +697 -0
  170. solara/toestand.py +772 -0
  171. solara/util.py +308 -0
  172. solara/website/__init__.py +0 -0
  173. solara/website/assets/custom.css +468 -0
  174. solara/website/assets/images/logo-small.png +0 -0
  175. solara/website/assets/images/logo.svg +17 -0
  176. solara/website/assets/images/logo_white.svg +50 -0
  177. solara/website/assets/theme.js +8 -0
  178. solara/website/components/__init__.py +5 -0
  179. solara/website/components/algolia.vue +24 -0
  180. solara/website/components/algolia_api.vue +187 -0
  181. solara/website/components/docs.py +118 -0
  182. solara/website/components/header.py +72 -0
  183. solara/website/components/hero.py +15 -0
  184. solara/website/components/mailchimp.py +12 -0
  185. solara/website/components/mailchimp.vue +47 -0
  186. solara/website/components/markdown.py +30 -0
  187. solara/website/components/notebook.py +171 -0
  188. solara/website/pages/__init__.py +575 -0
  189. solara/website/pages/apps/__init__.py +16 -0
  190. solara/website/pages/apps/authorization/__init__.py +119 -0
  191. solara/website/pages/apps/authorization/admin.py +12 -0
  192. solara/website/pages/apps/authorization/users.py +12 -0
  193. solara/website/pages/apps/jupyter-dashboard-1.py +116 -0
  194. solara/website/pages/apps/layout-demo.py +40 -0
  195. solara/website/pages/apps/multipage/__init__.py +38 -0
  196. solara/website/pages/apps/multipage/page1.py +26 -0
  197. solara/website/pages/apps/multipage/page2.py +34 -0
  198. solara/website/pages/apps/scatter.py +136 -0
  199. solara/website/pages/apps/scrolling.py +63 -0
  200. solara/website/pages/apps/tutorial-streamlit.py +18 -0
  201. solara/website/pages/changelog/__init__.py +8 -0
  202. solara/website/pages/changelog/changelog.md +204 -0
  203. solara/website/pages/contact/__init__.py +8 -0
  204. solara/website/pages/contact/contact.md +17 -0
  205. solara/website/pages/doc_use_download.py +85 -0
  206. solara/website/pages/documentation/__init__.py +184 -0
  207. solara/website/pages/documentation/advanced/__init__.py +9 -0
  208. solara/website/pages/documentation/advanced/content/00-overview.md +1 -0
  209. solara/website/pages/documentation/advanced/content/10-howto/00-overview.md +6 -0
  210. solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md +196 -0
  211. solara/website/pages/documentation/advanced/content/10-howto/20-layout.md +125 -0
  212. solara/website/pages/documentation/advanced/content/10-howto/30-testing.md +162 -0
  213. solara/website/pages/documentation/advanced/content/10-howto/31-debugging.md +69 -0
  214. solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +49 -0
  215. solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md +124 -0
  216. solara/website/pages/documentation/advanced/content/15-reference/00-overview.md +3 -0
  217. solara/website/pages/documentation/advanced/content/15-reference/40-static_files.md +31 -0
  218. solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +36 -0
  219. solara/website/pages/documentation/advanced/content/15-reference/60-static-site-generation.md +59 -0
  220. solara/website/pages/documentation/advanced/content/15-reference/70-search.md +34 -0
  221. solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md +34 -0
  222. solara/website/pages/documentation/advanced/content/15-reference/90-notebook-support.md +7 -0
  223. solara/website/pages/documentation/advanced/content/15-reference/95-caching.md +148 -0
  224. solara/website/pages/documentation/advanced/content/20-understanding/00-introduction.md +10 -0
  225. solara/website/pages/documentation/advanced/content/20-understanding/05-ipywidgets.md +35 -0
  226. solara/website/pages/documentation/advanced/content/20-understanding/06-ipyvuetify.md +42 -0
  227. solara/website/pages/documentation/advanced/content/20-understanding/10-reacton.md +28 -0
  228. solara/website/pages/documentation/advanced/content/20-understanding/12-reacton-basics.md +108 -0
  229. solara/website/pages/documentation/advanced/content/20-understanding/15-anatomy.md +23 -0
  230. solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md +7 -0
  231. solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md +166 -0
  232. solara/website/pages/documentation/advanced/content/20-understanding/20-solara.md +18 -0
  233. solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +240 -0
  234. solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +97 -0
  235. solara/website/pages/documentation/advanced/content/20-understanding/60-voila.md +12 -0
  236. solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +1 -0
  237. solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +171 -0
  238. solara/website/pages/documentation/advanced/content/40-development/00-overview.md +0 -0
  239. solara/website/pages/documentation/advanced/content/40-development/01-contribute.md +45 -0
  240. solara/website/pages/documentation/advanced/content/40-development/10-setup.md +76 -0
  241. solara/website/pages/documentation/api/__init__.py +19 -0
  242. solara/website/pages/documentation/api/cross_filter/__init__.py +9 -0
  243. solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +23 -0
  244. solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +22 -0
  245. solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +22 -0
  246. solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +22 -0
  247. solara/website/pages/documentation/api/hooks/__init__.py +9 -0
  248. solara/website/pages/documentation/api/hooks/use_cross_filter.py +25 -0
  249. solara/website/pages/documentation/api/hooks/use_dark_effective.py +12 -0
  250. solara/website/pages/documentation/api/hooks/use_effect.md +43 -0
  251. solara/website/pages/documentation/api/hooks/use_effect.py +9 -0
  252. solara/website/pages/documentation/api/hooks/use_exception.py +33 -0
  253. solara/website/pages/documentation/api/hooks/use_memo.md +16 -0
  254. solara/website/pages/documentation/api/hooks/use_memo.py +9 -0
  255. solara/website/pages/documentation/api/hooks/use_previous.py +33 -0
  256. solara/website/pages/documentation/api/hooks/use_reactive.py +16 -0
  257. solara/website/pages/documentation/api/hooks/use_state.py +10 -0
  258. solara/website/pages/documentation/api/hooks/use_state_or_update.py +69 -0
  259. solara/website/pages/documentation/api/hooks/use_thread.md +58 -0
  260. solara/website/pages/documentation/api/hooks/use_thread.py +44 -0
  261. solara/website/pages/documentation/api/hooks/use_trait_observe.py +12 -0
  262. solara/website/pages/documentation/api/routing/__init__.py +9 -0
  263. solara/website/pages/documentation/api/routing/generate_routes.py +10 -0
  264. solara/website/pages/documentation/api/routing/generate_routes_directory.py +10 -0
  265. solara/website/pages/documentation/api/routing/resolve_path.py +35 -0
  266. solara/website/pages/documentation/api/routing/route.py +31 -0
  267. solara/website/pages/documentation/api/routing/use_route.py +80 -0
  268. solara/website/pages/documentation/api/routing/use_router.py +16 -0
  269. solara/website/pages/documentation/api/utilities/__init__.py +9 -0
  270. solara/website/pages/documentation/api/utilities/component_vue.py +10 -0
  271. solara/website/pages/documentation/api/utilities/computed.py +16 -0
  272. solara/website/pages/documentation/api/utilities/display.py +16 -0
  273. solara/website/pages/documentation/api/utilities/get_kernel_id.py +16 -0
  274. solara/website/pages/documentation/api/utilities/get_session_id.py +16 -0
  275. solara/website/pages/documentation/api/utilities/memoize.py +35 -0
  276. solara/website/pages/documentation/api/utilities/on_kernel_start.py +27 -0
  277. solara/website/pages/documentation/api/utilities/reactive.py +16 -0
  278. solara/website/pages/documentation/api/utilities/widget.py +104 -0
  279. solara/website/pages/documentation/components/__init__.py +12 -0
  280. solara/website/pages/documentation/components/advanced/__init__.py +9 -0
  281. solara/website/pages/documentation/components/advanced/link.py +27 -0
  282. solara/website/pages/documentation/components/advanced/meta.py +20 -0
  283. solara/website/pages/documentation/components/advanced/style.py +45 -0
  284. solara/website/pages/documentation/components/common.py +9 -0
  285. solara/website/pages/documentation/components/data/__init__.py +9 -0
  286. solara/website/pages/documentation/components/data/dataframe.py +44 -0
  287. solara/website/pages/documentation/components/data/pivot_table.py +81 -0
  288. solara/website/pages/documentation/components/enterprise/__init__.py +9 -0
  289. solara/website/pages/documentation/components/enterprise/avatar.py +24 -0
  290. solara/website/pages/documentation/components/enterprise/avatar_menu.py +25 -0
  291. solara/website/pages/documentation/components/input/__init__.py +9 -0
  292. solara/website/pages/documentation/components/input/button.py +23 -0
  293. solara/website/pages/documentation/components/input/checkbox.py +10 -0
  294. solara/website/pages/documentation/components/input/file_browser.py +32 -0
  295. solara/website/pages/documentation/components/input/file_drop.py +76 -0
  296. solara/website/pages/documentation/components/input/input.py +19 -0
  297. solara/website/pages/documentation/components/input/select.py +22 -0
  298. solara/website/pages/documentation/components/input/slider.py +29 -0
  299. solara/website/pages/documentation/components/input/switch.py +10 -0
  300. solara/website/pages/documentation/components/input/togglebuttons.py +21 -0
  301. solara/website/pages/documentation/components/lab/__init__.py +9 -0
  302. solara/website/pages/documentation/components/lab/chat.py +109 -0
  303. solara/website/pages/documentation/components/lab/confirmation_dialog.py +55 -0
  304. solara/website/pages/documentation/components/lab/cookies_headers.py +48 -0
  305. solara/website/pages/documentation/components/lab/input_date.py +20 -0
  306. solara/website/pages/documentation/components/lab/menu.py +22 -0
  307. solara/website/pages/documentation/components/lab/tab.py +25 -0
  308. solara/website/pages/documentation/components/lab/tabs.py +45 -0
  309. solara/website/pages/documentation/components/lab/task.py +11 -0
  310. solara/website/pages/documentation/components/lab/theming.py +72 -0
  311. solara/website/pages/documentation/components/lab/use_task.py +11 -0
  312. solara/website/pages/documentation/components/layout/__init__.py +9 -0
  313. solara/website/pages/documentation/components/layout/app_bar.py +16 -0
  314. solara/website/pages/documentation/components/layout/app_bar_title.py +16 -0
  315. solara/website/pages/documentation/components/layout/app_layout.py +24 -0
  316. solara/website/pages/documentation/components/layout/card.py +15 -0
  317. solara/website/pages/documentation/components/layout/card_actions.py +16 -0
  318. solara/website/pages/documentation/components/layout/column.py +30 -0
  319. solara/website/pages/documentation/components/layout/columns.py +27 -0
  320. solara/website/pages/documentation/components/layout/columns_responsive.py +68 -0
  321. solara/website/pages/documentation/components/layout/griddraggable.py +62 -0
  322. solara/website/pages/documentation/components/layout/gridfixed.py +21 -0
  323. solara/website/pages/documentation/components/layout/hbox.py +18 -0
  324. solara/website/pages/documentation/components/layout/row.py +30 -0
  325. solara/website/pages/documentation/components/layout/sidebar.py +24 -0
  326. solara/website/pages/documentation/components/layout/vbox.py +19 -0
  327. solara/website/pages/documentation/components/output/__init__.py +9 -0
  328. solara/website/pages/documentation/components/output/file_download.py +11 -0
  329. solara/website/pages/documentation/components/output/html.py +21 -0
  330. solara/website/pages/documentation/components/output/image.py +11 -0
  331. solara/website/pages/documentation/components/output/markdown.py +57 -0
  332. solara/website/pages/documentation/components/output/markdown_editor.py +51 -0
  333. solara/website/pages/documentation/components/output/sql_code.py +85 -0
  334. solara/website/pages/documentation/components/output/tooltip.py +11 -0
  335. solara/website/pages/documentation/components/page/__init__.py +9 -0
  336. solara/website/pages/documentation/components/page/head.py +18 -0
  337. solara/website/pages/documentation/components/page/title.py +27 -0
  338. solara/website/pages/documentation/components/status/__init__.py +9 -0
  339. solara/website/pages/documentation/components/status/error.py +40 -0
  340. solara/website/pages/documentation/components/status/info.py +40 -0
  341. solara/website/pages/documentation/components/status/progress.py +10 -0
  342. solara/website/pages/documentation/components/status/spinner.py +11 -0
  343. solara/website/pages/documentation/components/status/success.py +40 -0
  344. solara/website/pages/documentation/components/status/warning.py +47 -0
  345. solara/website/pages/documentation/components/viz/__init__.py +9 -0
  346. solara/website/pages/documentation/components/viz/altair.py +42 -0
  347. solara/website/pages/documentation/components/viz/echarts.py +75 -0
  348. solara/website/pages/documentation/components/viz/matplotlib.py +30 -0
  349. solara/website/pages/documentation/components/viz/plotly.py +63 -0
  350. solara/website/pages/documentation/components/viz/plotly_express.py +41 -0
  351. solara/website/pages/documentation/examples/__init__.py +52 -0
  352. solara/website/pages/documentation/examples/ai/__init__.py +11 -0
  353. solara/website/pages/documentation/examples/ai/chatbot.py +95 -0
  354. solara/website/pages/documentation/examples/ai/tokenizer.py +107 -0
  355. solara/website/pages/documentation/examples/basics/__init__.py +10 -0
  356. solara/website/pages/documentation/examples/basics/sine.py +28 -0
  357. solara/website/pages/documentation/examples/fullscreen/__init__.py +10 -0
  358. solara/website/pages/documentation/examples/fullscreen/authorization.py +3 -0
  359. solara/website/pages/documentation/examples/fullscreen/layout_demo.py +3 -0
  360. solara/website/pages/documentation/examples/fullscreen/multipage.py +3 -0
  361. solara/website/pages/documentation/examples/fullscreen/scatter.py +3 -0
  362. solara/website/pages/documentation/examples/fullscreen/scrolling.py +3 -0
  363. solara/website/pages/documentation/examples/fullscreen/tutorial_streamlit.py +3 -0
  364. solara/website/pages/documentation/examples/general/__init__.py +10 -0
  365. solara/website/pages/documentation/examples/general/custom_storage.py +70 -0
  366. solara/website/pages/documentation/examples/general/deploy_model.py +115 -0
  367. solara/website/pages/documentation/examples/general/live_update.py +38 -0
  368. solara/website/pages/documentation/examples/general/login_oauth.py +81 -0
  369. solara/website/pages/documentation/examples/general/mycard.vue +58 -0
  370. solara/website/pages/documentation/examples/general/pokemon_search.py +51 -0
  371. solara/website/pages/documentation/examples/general/vue_component.py +50 -0
  372. solara/website/pages/documentation/examples/ipycanvas.py +49 -0
  373. solara/website/pages/documentation/examples/libraries/__init__.py +10 -0
  374. solara/website/pages/documentation/examples/libraries/altair.py +64 -0
  375. solara/website/pages/documentation/examples/libraries/bqplot.py +39 -0
  376. solara/website/pages/documentation/examples/libraries/ipyleaflet.py +33 -0
  377. solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +66 -0
  378. solara/website/pages/documentation/examples/utilities/__init__.py +10 -0
  379. solara/website/pages/documentation/examples/utilities/calculator.py +157 -0
  380. solara/website/pages/documentation/examples/utilities/countdown_timer.py +64 -0
  381. solara/website/pages/documentation/examples/utilities/todo.py +196 -0
  382. solara/website/pages/documentation/examples/visualization/__init__.py +6 -0
  383. solara/website/pages/documentation/examples/visualization/annotator.py +69 -0
  384. solara/website/pages/documentation/examples/visualization/linked_views.py +84 -0
  385. solara/website/pages/documentation/examples/visualization/plotly.py +44 -0
  386. solara/website/pages/documentation/faq/__init__.py +12 -0
  387. solara/website/pages/documentation/faq/content/99-faq.md +76 -0
  388. solara/website/pages/documentation/getting_started/__init__.py +9 -0
  389. solara/website/pages/documentation/getting_started/content/00-quickstart.md +89 -0
  390. solara/website/pages/documentation/getting_started/content/01-introduction.md +125 -0
  391. solara/website/pages/documentation/getting_started/content/02-installing.md +134 -0
  392. solara/website/pages/documentation/getting_started/content/04-tutorials/00-overview.md +14 -0
  393. solara/website/pages/documentation/getting_started/content/04-tutorials/10_data_science.py +13 -0
  394. solara/website/pages/documentation/getting_started/content/04-tutorials/20-web-app.md +89 -0
  395. solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md +124 -0
  396. solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md +146 -0
  397. solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md +144 -0
  398. solara/website/pages/documentation/getting_started/content/04-tutorials/60-jupyter-dashboard-part1.py +64 -0
  399. solara/website/pages/documentation/getting_started/content/04-tutorials/SF_crime_sample.csv.gz +0 -0
  400. solara/website/pages/documentation/getting_started/content/04-tutorials/_data_science.ipynb +445 -0
  401. solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +1000 -0
  402. solara/website/pages/documentation/getting_started/content/05-fundamentals/00-overview.md +11 -0
  403. solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +223 -0
  404. solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +88 -0
  405. solara/website/pages/documentation/getting_started/content/07-deploying/00-overview.md +7 -0
  406. solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +273 -0
  407. solara/website/pages/documentation/getting_started/content/07-deploying/20-cloud-hosted.md +80 -0
  408. solara/website/pages/documentation/getting_started/content/80-what-is-lab.md +7 -0
  409. solara/website/pages/documentation/getting_started/content/90-troubleshoot.md +26 -0
  410. solara/website/pages/docutils.py +38 -0
  411. solara/website/pages/showcase/__init__.py +105 -0
  412. solara/website/pages/showcase/domino_code_assist.py +60 -0
  413. solara/website/pages/showcase/planeto_tessa.py +19 -0
  414. solara/website/pages/showcase/solara_dev.py +54 -0
  415. solara/website/pages/showcase/solarathon_2023_team_2.py +22 -0
  416. solara/website/pages/showcase/solarathon_2023_team_4.py +22 -0
  417. solara/website/pages/showcase/solarathon_2023_team_5.py +23 -0
  418. solara/website/pages/showcase/solarathon_2023_team_6.py +34 -0
  419. solara/website/pages/showcase/wanderlust.py +27 -0
  420. solara/website/public/beach.jpeg +0 -0
  421. solara/website/public/logo.svg +6 -0
  422. solara/website/public/social/discord.svg +1 -0
  423. solara/website/public/social/github.svg +1 -0
  424. solara/website/public/social/twitter.svg +3 -0
  425. solara/website/public/success.html +25 -0
  426. solara/website/templates/index.html.j2 +117 -0
  427. solara/website/utils.py +51 -0
  428. solara/widgets/__init__.py +1 -0
  429. solara/widgets/vue/gridlayout.vue +110 -0
  430. solara/widgets/vue/html.vue +4 -0
  431. solara/widgets/vue/navigator.vue +104 -0
  432. solara/widgets/vue/vegalite.vue +115 -0
  433. solara/widgets/widgets.py +66 -0
  434. solara_ui-1.31.0.data/data/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
  435. solara_ui-1.31.0.data/data/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
  436. solara_ui-1.31.0.dist-info/METADATA +158 -0
  437. solara_ui-1.31.0.dist-info/RECORD +439 -0
  438. solara_ui-1.31.0.dist-info/WHEEL +5 -0
  439. solara_ui-1.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,338 @@
1
+ import json
2
+ import logging
3
+ import pdb
4
+ import queue
5
+ import struct
6
+ import warnings
7
+ from binascii import b2a_base64
8
+ from datetime import datetime
9
+ from typing import Set, Union
10
+
11
+ import ipykernel
12
+ import ipykernel.kernelbase
13
+ import jupyter_client.session as session
14
+ from dateutil.tz import tzlocal # type: ignore
15
+ from ipykernel.comm import CommManager
16
+ from zmq.eventloop.zmqstream import ZMQStream
17
+
18
+ import solara
19
+ from solara.server.shell import SolaraInteractiveShell
20
+
21
+ from . import settings, websocket
22
+
23
+ logger = logging.getLogger("solara.server.kernel")
24
+ ipykernel_major = int(ipykernel.__version__.split(".")[0])
25
+
26
+ jsonmodule = json
27
+
28
+
29
+ # from jupyter_client/jsonutil.py
30
+ def _ensure_tzinfo(dt: datetime) -> datetime:
31
+ """Ensure a datetime object has tzinfo
32
+ If no tzinfo is present, add tzlocal
33
+ """
34
+ if not dt.tzinfo:
35
+ # No more naïve datetime objects!
36
+ warnings.warn(
37
+ "Interpreting naive datetime as local %s. Please add timezone info to timestamps." % dt,
38
+ DeprecationWarning,
39
+ stacklevel=4,
40
+ )
41
+ dt = dt.replace(tzinfo=tzlocal())
42
+ return dt
43
+
44
+
45
+ def json_default(obj):
46
+ """default function for packing objects in JSON."""
47
+ if isinstance(obj, datetime):
48
+ obj = _ensure_tzinfo(obj)
49
+ return obj.isoformat().replace("+00:00", "Z")
50
+ elif isinstance(obj, bytes):
51
+ return b2a_base64(obj).decode("ascii")
52
+ if type(obj).__module__ == "numpy":
53
+ import numpy as np
54
+
55
+ if isinstance(obj, np.number):
56
+ return repr(obj.item())
57
+ else:
58
+ raise TypeError("%r is not JSON serializable" % obj)
59
+ else:
60
+ raise TypeError("%r is not JSON serializable" % obj)
61
+
62
+
63
+ def json_dumps(data):
64
+ try:
65
+ return jsonmodule.dumps(data)
66
+ except TypeError:
67
+ logger.warning("Invalid JSON, will try a with a more forgiving json encoder")
68
+ return jsonmodule.dumps(
69
+ data,
70
+ default=json_default,
71
+ ensure_ascii=False,
72
+ allow_nan=False,
73
+ )
74
+
75
+
76
+ ipykernel_version = tuple(map(int, ipykernel.__version__.split(".")))
77
+ if ipykernel_version >= (6, 18, 0):
78
+ import comm.base_comm
79
+
80
+ class Comm(comm.base_comm.BaseComm):
81
+ kernel: Union[ipykernel.kernelbase.Kernel, None]
82
+
83
+ def __init__(self, **kwargs) -> None:
84
+ if ipykernel.kernelbase.Kernel.initialized():
85
+ self.kernel = ipykernel.kernelbase.Kernel.instance()
86
+ else:
87
+ self.kernel = None
88
+ super().__init__(**kwargs)
89
+
90
+ def publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys):
91
+ if self.kernel is None or self.kernel.session is None:
92
+ return
93
+ data = {} if data is None else data
94
+ metadata = {} if metadata is None else metadata
95
+ content = dict(data=data, comm_id=self.comm_id, **keys)
96
+ self.kernel.session.send(
97
+ self.kernel.iopub_socket,
98
+ msg_type,
99
+ content,
100
+ metadata=metadata,
101
+ parent=self.kernel.get_parent("shell"),
102
+ ident=self.topic,
103
+ buffers=buffers,
104
+ )
105
+
106
+ comm.create_comm = Comm
107
+
108
+ def get_comm_manager():
109
+ from .kernel_context import get_current_context, has_current_context
110
+
111
+ if has_current_context():
112
+ return get_current_context().kernel.comm_manager
113
+ else:
114
+ return global_comm_manager
115
+
116
+ global_comm_manager = comm.get_comm_manager()
117
+ comm.get_comm_manager = get_comm_manager
118
+
119
+ # from notebook.base.zmqhandlers import serialize_binary_message
120
+ # this saves us a dependency on notebook/jupyter_server when e.g.
121
+ # running on pyodide
122
+
123
+
124
+ def _fix_msg(msg):
125
+ # makes sure the msg can be json serializable
126
+ # instead of using a callable like in jupyter_client (i.e. json_default)
127
+ # we replace the keys we know are problematic
128
+ # this allows us to use a faster json serializer in the future
129
+ if "header" in msg and "date" in msg["header"]:
130
+ # this is what jupyter_client.jsonutil.json_default does
131
+ msg["header"]["date"] = msg["header"]["date"].isoformat().replace("+00:00", "Z")
132
+ if "parent_header" in msg and "date" in msg["parent_header"]:
133
+ # date is already a string if it's copied from the header that is not turned into a datetime
134
+ # maybe we should do that in server.py
135
+ date = msg["parent_header"]["date"]
136
+ if isinstance(date, datetime):
137
+ msg["parent_header"]["date"] = date.isoformat().replace("+00:00", "Z")
138
+
139
+
140
+ def serialize_binary_message(msg):
141
+ """serialize a message as a binary blob
142
+
143
+ Header:
144
+
145
+ 4 bytes: number of msg parts (nbufs) as 32b int
146
+ 4 * nbufs bytes: offset for each buffer as integer as 32b int
147
+
148
+ Offsets are from the start of the buffer, including the header.
149
+
150
+ Returns
151
+ -------
152
+
153
+ The message serialized to bytes.
154
+
155
+ """
156
+ # don't modify msg or buffer list in-place
157
+ msg = msg.copy()
158
+ buffers = list(msg.pop("buffers"))
159
+ bmsg = json_dumps(msg).encode("utf8")
160
+ buffers.insert(0, bmsg)
161
+ nbufs = len(buffers)
162
+ offsets = [4 * (nbufs + 1)]
163
+ for buf in buffers[:-1]:
164
+ offsets.append(offsets[-1] + len(buf))
165
+ offsets_buf = struct.pack("!" + "I" * (nbufs + 1), nbufs, *offsets)
166
+ buffers.insert(0, offsets_buf)
167
+ return b"".join(buffers)
168
+
169
+
170
+ def deserialize_binary_message(bmsg):
171
+ """deserialize a message from a binary blog
172
+
173
+ Header:
174
+
175
+ 4 bytes: number of msg parts (nbufs) as 32b int
176
+ 4 * nbufs bytes: offset for each buffer as integer as 32b int
177
+
178
+ Offsets are from the start of the buffer, including the header.
179
+
180
+ Returns
181
+ -------
182
+ message dictionary
183
+ """
184
+ nbufs = struct.unpack("!i", bmsg[:4])[0]
185
+ offsets = list(struct.unpack("!" + "I" * nbufs, bmsg[4 : 4 * (nbufs + 1)]))
186
+ offsets.append(None)
187
+ bufs = []
188
+ for start, stop in zip(offsets[:-1], offsets[1:]):
189
+ bufs.append(bmsg[start:stop])
190
+ msg = json.loads(bufs[0].decode("utf8"))
191
+ msg["buffers"] = bufs[1:]
192
+ return msg
193
+
194
+
195
+ SESSION_KEY = b"solara"
196
+
197
+
198
+ class WebsocketStream:
199
+ def __init__(self, session, channel: str):
200
+ self.session = session
201
+ self.channel = channel
202
+
203
+
204
+ class WebsocketStreamWrapper(ZMQStream):
205
+ def __init__(self, websocket, channel):
206
+ self.websocket = websocket
207
+ self.channel = channel
208
+
209
+ def flush(self, *ignore):
210
+ pass
211
+
212
+
213
+ def send_websockets(websockets: Set[websocket.WebsocketWrapper], binary_msg):
214
+ for ws in list(websockets):
215
+ try:
216
+ ws.send(binary_msg)
217
+ except: # noqa
218
+ # in case of any issue, we simply remove it from the list
219
+ try:
220
+ # websocket can be modified by another thread
221
+ websockets.remove(ws)
222
+ except KeyError:
223
+ pass # already removed
224
+
225
+
226
+ class SessionWebsocket(session.Session):
227
+ def __init__(self, *args, **kwargs):
228
+ super().__init__(*args, **kwargs)
229
+ self.websockets: Set[websocket.WebsocketWrapper] = set() # map from .. msg id to websocket?
230
+
231
+ def close(self):
232
+ for ws in list(self.websockets):
233
+ try:
234
+ ws.close()
235
+ except: # noqa
236
+ pass
237
+
238
+ def send(
239
+ self,
240
+ stream,
241
+ msg_or_type,
242
+ content=None,
243
+ parent=None,
244
+ ident=None,
245
+ buffers=None,
246
+ track=False,
247
+ header=None,
248
+ metadata=None,
249
+ ):
250
+ try:
251
+ if isinstance(msg_or_type, dict):
252
+ msg = msg_or_type
253
+ else:
254
+ msg = self.msg(
255
+ msg_or_type,
256
+ content=content,
257
+ parent=parent,
258
+ header=header,
259
+ metadata=metadata,
260
+ )
261
+ _fix_msg(msg)
262
+ msg["channel"] = stream.channel
263
+ # not using pdb guard for performance reasons
264
+ try:
265
+ if buffers:
266
+ msg["buffers"] = [memoryview(k).cast("b") for k in buffers]
267
+ wire_message = serialize_binary_message(msg)
268
+ else:
269
+ wire_message = json_dumps(msg)
270
+ except Exception:
271
+ logger.exception("Could not serialize message: %r", msg)
272
+ if settings.main.use_pdb:
273
+ pdb.post_mortem()
274
+ raise
275
+ send_websockets(self.websockets, wire_message)
276
+ except Exception as e:
277
+ logger.exception("Error sending message: %s", e)
278
+
279
+
280
+ class Kernel(ipykernel.kernelbase.Kernel):
281
+ session: SessionWebsocket # type: ignore
282
+ # Ideally we have `session = Instance(Session, allow_none=True)`, but MyPy does not like it
283
+
284
+ implementation = "solara"
285
+ implementation_version = solara.__version__
286
+ banner = "solara"
287
+
288
+ def __init__(self):
289
+ super().__init__()
290
+ self.session = SessionWebsocket(parent=self, key=SESSION_KEY)
291
+ self.msg_queue = queue.Queue() # type: ignore
292
+ self.stream = self.iopub_socket = WebsocketStream(self.session, "iopub")
293
+ # on github action the next line gives a mypy error:
294
+ # solara/server/kernel.py:111: error: "SessionWebsocket" has no attribute "stream"
295
+ # not sure why we cannot reproduce that locally
296
+ self.session.stream = self.iopub_socket # type: ignore
297
+ if ipykernel_version >= (6, 18, 0):
298
+ # from this version on, ipykernel uses the comm package https://github.com/ipython/ipykernel/pull/973
299
+ self.comm_manager = CommManager(parent=self, kernel=self)
300
+ import ipywidgets.widgets.widget
301
+
302
+ if hasattr(ipywidgets.widgets.widget, "Comm"):
303
+ ipywidgets.widgets.widget.Comm = Comm
304
+ ipywidgets.widgets.widget.Widget.comm.klass = Comm
305
+ else:
306
+ self.comm_manager = CommManager(parent=self, kernel=self)
307
+ self.log = logging.getLogger("fake")
308
+
309
+ comm_msg_types = ["comm_open", "comm_msg", "comm_close"]
310
+ for msg_type in comm_msg_types:
311
+ self.shell_handlers[msg_type] = getattr(self.comm_manager, msg_type)
312
+ self.shell = SolaraInteractiveShell()
313
+ self.shell.display_pub.session = self.session
314
+ self.shell.display_pub.pub_socket = self.iopub_socket
315
+
316
+ async def _flush_control_queue(self):
317
+ pass
318
+
319
+ # these don't work from non-main thread, and we do not care about them I think
320
+ # TODO: it seems that if post_handler_hook is not override, the flask reload tests fails
321
+ # for unknown reason
322
+ def pre_handler_hook(self, *args):
323
+ pass
324
+
325
+ def post_handler_hook(self, *args):
326
+ pass
327
+
328
+ def set_parent(self, ident, parent, channel="shell"):
329
+ """Overridden from parent to tell the display hook and output streams
330
+ about the parent message.
331
+ """
332
+ if ipykernel_major < 6:
333
+ # the channel argument was added in 6.0
334
+ super().set_parent(ident, parent)
335
+ else:
336
+ super().set_parent(ident, parent, channel)
337
+ if channel == "shell":
338
+ self.shell.set_parent(parent)
@@ -0,0 +1,357 @@
1
+ import asyncio
2
+ import sys
3
+
4
+ try:
5
+ import contextvars
6
+ except ModuleNotFoundError:
7
+ contextvars = None # type: ignore
8
+
9
+ import dataclasses
10
+ import enum
11
+ import logging
12
+ import os
13
+ import pickle
14
+ import threading
15
+ import time
16
+ import typing
17
+ from pathlib import Path
18
+ from typing import Any, Callable, Dict, List, Optional, cast
19
+
20
+ import ipywidgets as widgets
21
+ import reacton
22
+ from ipywidgets import DOMWidget, Widget
23
+
24
+ import solara.server.settings
25
+ import solara.util
26
+
27
+ from . import kernel, kernel_context, websocket
28
+ from .. import lifecycle
29
+ from .kernel import Kernel, WebsocketStreamWrapper
30
+
31
+ WebSocket = Any
32
+ logger = logging.getLogger("solara.server.app")
33
+
34
+
35
+ class Local(threading.local):
36
+ kernel_context_stack: Optional[List[Optional["kernel_context.VirtualKernelContext"]]] = None
37
+
38
+
39
+ local = Local()
40
+
41
+
42
+ class PageStatus(enum.Enum):
43
+ CONNECTED = "connected"
44
+ DISCONNECTED = "disconnected"
45
+ CLOSED = "closed"
46
+
47
+
48
+ @dataclasses.dataclass
49
+ class VirtualKernelContext:
50
+ id: str
51
+ kernel: kernel.Kernel
52
+ # we keep track of the session id to prevent kernel hijacking
53
+ # to 'steal' a kernel, one would need to know the session id
54
+ # *and* the kernel id
55
+ session_id: str
56
+ control_sockets: List[WebSocket] = dataclasses.field(default_factory=list)
57
+ # this is the 'private' version of the normally global ipywidgets.Widgets.widget dict
58
+ # see patch.py
59
+ widgets: Dict[str, Widget] = dataclasses.field(default_factory=dict)
60
+ # same, for ipyvue templates
61
+ # see patch.py
62
+ templates: Dict[str, Widget] = dataclasses.field(default_factory=dict)
63
+ user_dicts: Dict[str, Dict] = dataclasses.field(default_factory=dict)
64
+ # anything we need to attach to the context
65
+ # e.g. for a react app the render context, so that we can store/restore the state
66
+ app_object: Optional[Any] = None
67
+ reload: Callable = lambda: None # noqa: E731
68
+ state: Any = None
69
+ container: Optional[DOMWidget] = None
70
+ # we track which pages are connected to implement kernel culling
71
+ page_status: Dict[str, PageStatus] = dataclasses.field(default_factory=dict)
72
+ # only used for testing
73
+ _last_kernel_cull_task: "Optional[asyncio.Future[None]]" = None
74
+ closed_event: threading.Event = dataclasses.field(default_factory=threading.Event)
75
+ _on_close_callbacks: List[Callable[[], None]] = dataclasses.field(default_factory=list)
76
+
77
+ def __post_init__(self):
78
+ with self:
79
+ for f, *_ in lifecycle._on_kernel_start_callbacks:
80
+ cleanup = f()
81
+ if cleanup:
82
+ self.on_close(cleanup)
83
+
84
+ def restart(self):
85
+ # should we do this, or maybe close the context and create a new one?
86
+ with self:
87
+ for f in reversed(self._on_close_callbacks):
88
+ f()
89
+ self._on_close_callbacks.clear()
90
+ self.__post_init__()
91
+
92
+ def display(self, *args):
93
+ print(args) # noqa
94
+
95
+ def on_close(self, f: Callable[[], None]):
96
+ self._on_close_callbacks.append(f)
97
+
98
+ def __enter__(self):
99
+ if local.kernel_context_stack is None:
100
+ local.kernel_context_stack = []
101
+ key = get_current_thread_key()
102
+ local.kernel_context_stack.append(current_context.get(key, None))
103
+ current_context[key] = self
104
+
105
+ def __exit__(self, *args):
106
+ key = get_current_thread_key()
107
+ assert local.kernel_context_stack is not None
108
+ current_context[key] = local.kernel_context_stack.pop()
109
+
110
+ def close(self):
111
+ if self.closed_event.is_set():
112
+ logger.error("Tried to close a kernel context that is already closed: %s", self.id)
113
+ return
114
+ logger.info("Shut down virtual kernel: %s", self.id)
115
+ with self:
116
+ for f in reversed(self._on_close_callbacks):
117
+ f()
118
+ with self:
119
+ if self.app_object is not None:
120
+ if isinstance(self.app_object, reacton.core._RenderContext):
121
+ try:
122
+ self.app_object.close()
123
+ except Exception as e:
124
+ logger.exception("Could not close render context: %s", e)
125
+ # we want to continue, so we at least close all widgets
126
+ widgets.Widget.close_all()
127
+ # what if we reference each other
128
+ # import gc
129
+ # gc.collect()
130
+ self.kernel.session.close()
131
+ if self.id in contexts:
132
+ del contexts[self.id]
133
+ self.closed_event.set()
134
+
135
+ def _state_reset(self):
136
+ state_directory = Path(".") / "states"
137
+ state_directory.mkdir(exist_ok=True)
138
+ path = state_directory / f"{self.id}.pickle"
139
+ path = path.absolute()
140
+ try:
141
+ path.unlink()
142
+ except: # noqa
143
+ pass
144
+ del contexts[self.id]
145
+ key = get_current_thread_key()
146
+ del current_context[key]
147
+
148
+ def state_save(self, state_directory: os.PathLike):
149
+ path = Path(state_directory) / f"{self.id}.pickle"
150
+ render_context = self.app_object
151
+ if render_context is not None:
152
+ render_context = cast(reacton.core._RenderContext, render_context)
153
+ state = render_context.state_get()
154
+ with path.open("wb") as f:
155
+ logger.debug("State: %r", state)
156
+ pickle.dump(state, f)
157
+
158
+ def page_connect(self, page_id: str):
159
+ logger.info("Connect page %s for kernel %s", page_id, self.id)
160
+ assert self.page_status.get(page_id) != PageStatus.CLOSED, "cannot connect with the same page_id after a close"
161
+ self.page_status[page_id] = PageStatus.CONNECTED
162
+ if self._last_kernel_cull_task:
163
+ self._last_kernel_cull_task.cancel()
164
+
165
+ def page_disconnect(self, page_id: str) -> "asyncio.Future[None]":
166
+ """Signal that a page has disconnected, and schedule a kernel cull if needed.
167
+
168
+ During the kernel reconnect window, we will keep the kernel alive, even if all pages have disconnected.
169
+
170
+ Returns a future that is set when the kernel cull is done.
171
+ The scheduled kernel cull can be cancelled when a new page connects, a new disconnect is scheduled,
172
+ or a page if explicitly closed.
173
+ """
174
+ logger.info("Disconnect page %s for kernel %s", page_id, self.id)
175
+ future: "asyncio.Future[None]" = asyncio.Future()
176
+ self.page_status[page_id] = PageStatus.DISCONNECTED
177
+ current_event_loop = asyncio.get_event_loop()
178
+
179
+ async def kernel_cull():
180
+ try:
181
+ cull_timeout_sleep_seconds = solara.util.parse_timedelta(solara.server.settings.kernel.cull_timeout)
182
+ logger.info("Scheduling kernel cull, will wait for max %s before shutting down the virtual kernel %s", cull_timeout_sleep_seconds, self.id)
183
+ await asyncio.sleep(cull_timeout_sleep_seconds)
184
+ has_connected_pages = PageStatus.CONNECTED in self.page_status.values()
185
+ if has_connected_pages:
186
+ logger.info("We have (re)connected pages, keeping the virtual kernel %s alive", self.id)
187
+ else:
188
+ logger.info("No connected pages, and timeout reached, shutting down virtual kernel %s", self.id)
189
+ self.close()
190
+ current_event_loop.call_soon_threadsafe(future.set_result, None)
191
+ except asyncio.CancelledError:
192
+ if sys.version_info >= (3, 9):
193
+ current_event_loop.call_soon_threadsafe(future.cancel, "cancelled because a new cull task was scheduled")
194
+ else:
195
+ current_event_loop.call_soon_threadsafe(future.cancel)
196
+ raise
197
+
198
+ has_connected_pages = PageStatus.CONNECTED in self.page_status.values()
199
+ if not has_connected_pages:
200
+ # when we have no connected pages, we will schedule a kernel cull
201
+ if self._last_kernel_cull_task:
202
+ self._last_kernel_cull_task.cancel()
203
+
204
+ async def create_task():
205
+ task = asyncio.create_task(kernel_cull())
206
+ # create a reference to the task so we can cancel it later
207
+ self._last_kernel_cull_task = task
208
+ await task
209
+
210
+ asyncio.run_coroutine_threadsafe(create_task(), keep_alive_event_loop)
211
+ else:
212
+ future.set_result(None)
213
+ return future
214
+
215
+ def page_close(self, page_id: str):
216
+ """Signal that a page has closed, and close the context if needed.
217
+
218
+ Closing the browser tab or a page navigation means an explicit close, which is
219
+ different from a websocket/page disconnect, which we might want to recover from.
220
+
221
+ """
222
+ self.page_status[page_id] = PageStatus.CLOSED
223
+ logger.info("Close page %s for kernel %s", page_id, self.id)
224
+ has_connected_pages = PageStatus.CONNECTED in self.page_status.values()
225
+ has_disconnected_pages = PageStatus.DISCONNECTED in self.page_status.values()
226
+ if not (has_connected_pages or has_disconnected_pages):
227
+ logger.info("No connected or disconnected pages, shutting down virtual kernel %s", self.id)
228
+ if self._last_kernel_cull_task:
229
+ self._last_kernel_cull_task.cancel()
230
+ self.close()
231
+
232
+
233
+ try:
234
+ # Normal Python
235
+ keep_alive_event_loop = asyncio.new_event_loop()
236
+
237
+ def _run():
238
+ asyncio.set_event_loop(keep_alive_event_loop)
239
+ try:
240
+ keep_alive_event_loop.run_forever()
241
+ except Exception:
242
+ logger.exception("Error in keep alive event loop")
243
+ raise
244
+
245
+ threading.Thread(target=_run, daemon=True).start()
246
+ except RuntimeError:
247
+ # Emscripten/pyodide/lite
248
+ keep_alive_event_loop = asyncio.get_event_loop()
249
+
250
+ contexts: Dict[str, VirtualKernelContext] = {}
251
+ # maps from thread key to VirtualKernelContext, if VirtualKernelContext is None, it exists, but is not set as current
252
+ current_context: Dict[str, Optional[VirtualKernelContext]] = {}
253
+
254
+
255
+ def create_dummy_context():
256
+ from . import kernel
257
+
258
+ kernel_context = VirtualKernelContext(
259
+ id="dummy",
260
+ session_id="dummy",
261
+ kernel=kernel.Kernel(),
262
+ )
263
+ return kernel_context
264
+
265
+
266
+ if contextvars is not None:
267
+ if typing.TYPE_CHECKING:
268
+ async_context_id = contextvars.ContextVar[str]("async_context_id")
269
+ else:
270
+ async_context_id = contextvars.ContextVar("async_context_id")
271
+ async_context_id.set("default")
272
+ else:
273
+ async_context_id = None
274
+
275
+
276
+ def get_current_thread_key() -> str:
277
+ if not solara.server.settings.kernel.threaded:
278
+ if async_context_id is not None:
279
+ try:
280
+ key = async_context_id.get()
281
+ except LookupError:
282
+ raise RuntimeError("no kernel context set")
283
+ else:
284
+ raise RuntimeError("No threading support, and no contextvars support (Python 3.6 is not supported for this)")
285
+ else:
286
+ thread = threading.current_thread()
287
+ key = get_thread_key(thread)
288
+ return key
289
+
290
+
291
+ def get_thread_key(thread: threading.Thread) -> str:
292
+ if not solara.server.settings.kernel.threaded:
293
+ if async_context_id is not None:
294
+ return async_context_id.get()
295
+ thread_key = thread._name + str(thread._ident) # type: ignore
296
+ return thread_key
297
+
298
+
299
+ def set_context_for_thread(context: VirtualKernelContext, thread: threading.Thread):
300
+ key = get_thread_key(thread)
301
+ current_context[key] = context
302
+
303
+
304
+ def has_current_context() -> bool:
305
+ thread_key = get_current_thread_key()
306
+ return (thread_key in current_context) and (current_context[thread_key] is not None)
307
+
308
+
309
+ def get_current_context() -> VirtualKernelContext:
310
+ thread_key = get_current_thread_key()
311
+ if thread_key not in current_context:
312
+ raise RuntimeError(
313
+ f"Tried to get the current context for thread {thread_key}, but no known context found. This might be a bug in Solara. "
314
+ f"(known contexts: {list(current_context.keys())}"
315
+ )
316
+ context = current_context[thread_key]
317
+ if context is None:
318
+ raise RuntimeError(
319
+ f"Tried to get the current context for thread {thread_key!r}, although the context is know, it was not set for this thread. "
320
+ + "This might be a bug in Solara."
321
+ )
322
+ return context
323
+
324
+
325
+ def set_current_context(context: Optional[VirtualKernelContext]):
326
+ thread_key = get_current_thread_key()
327
+ current_context[thread_key] = context
328
+
329
+
330
+ def initialize_virtual_kernel(session_id: str, kernel_id: str, websocket: websocket.WebsocketWrapper):
331
+ from solara.server import app as appmodule
332
+
333
+ if kernel_id in contexts:
334
+ logger.info("reusing virtual kernel: %s", kernel_id)
335
+ context = contexts[kernel_id]
336
+ if context.session_id != session_id:
337
+ logger.critical("Session id mismatch when reusing kernel (hack attempt?): %s != %s", context.session_id, session_id)
338
+ websocket.send_text("Session id mismatch when reusing kernel (hack attempt?)")
339
+ # to avoid very fast reconnects (we are in a thread anyway)
340
+ time.sleep(0.5)
341
+ raise ValueError("Session id mismatch")
342
+ kernel = context.kernel
343
+ else:
344
+ kernel = Kernel()
345
+ logger.info("new virtual kernel: %s", kernel_id)
346
+ context = contexts[kernel_id] = VirtualKernelContext(id=kernel_id, session_id=session_id, kernel=kernel, control_sockets=[], widgets={}, templates={})
347
+
348
+ with context:
349
+ widgets.register_comm_target(kernel)
350
+ appmodule.register_solara_comm_target(kernel)
351
+ with context:
352
+ assert has_current_context()
353
+ assert kernel is Kernel.instance()
354
+ kernel.shell_stream = WebsocketStreamWrapper(websocket, "shell")
355
+ kernel.control_stream = WebsocketStreamWrapper(websocket, "control")
356
+ kernel.session.websockets.add(websocket)
357
+ return context