solara-ui 1.45.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 (464) 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 +765 -0
  5. solara/_stores.py +297 -0
  6. solara/alias.py +6 -0
  7. solara/autorouting.py +555 -0
  8. solara/cache.py +305 -0
  9. solara/checks.html +71 -0
  10. solara/checks.py +227 -0
  11. solara/comm.py +28 -0
  12. solara/components/__init__.py +77 -0
  13. solara/components/alert.py +155 -0
  14. solara/components/applayout.py +397 -0
  15. solara/components/button.py +85 -0
  16. solara/components/card.py +87 -0
  17. solara/components/checkbox.py +50 -0
  18. solara/components/code_highlight_css.py +11 -0
  19. solara/components/code_highlight_css.vue +63 -0
  20. solara/components/columns.py +159 -0
  21. solara/components/component_vue.py +134 -0
  22. solara/components/cross_filter.py +335 -0
  23. solara/components/dataframe.py +546 -0
  24. solara/components/datatable.py +214 -0
  25. solara/components/datatable.vue +175 -0
  26. solara/components/details.py +56 -0
  27. solara/components/download.vue +35 -0
  28. solara/components/echarts.py +86 -0
  29. solara/components/echarts.vue +139 -0
  30. solara/components/figure_altair.py +39 -0
  31. solara/components/file_browser.py +181 -0
  32. solara/components/file_download.py +199 -0
  33. solara/components/file_drop.py +159 -0
  34. solara/components/file_drop.vue +83 -0
  35. solara/components/file_list_widget.vue +78 -0
  36. solara/components/head.py +27 -0
  37. solara/components/head_tag.py +49 -0
  38. solara/components/head_tag.vue +60 -0
  39. solara/components/image.py +173 -0
  40. solara/components/input.py +456 -0
  41. solara/components/input_text_area.py +86 -0
  42. solara/components/link.py +55 -0
  43. solara/components/markdown.py +441 -0
  44. solara/components/markdown_editor.py +33 -0
  45. solara/components/markdown_editor.vue +359 -0
  46. solara/components/matplotlib.py +74 -0
  47. solara/components/meta.py +47 -0
  48. solara/components/misc.py +333 -0
  49. solara/components/pivot_table.py +258 -0
  50. solara/components/pivot_table.vue +158 -0
  51. solara/components/progress.py +47 -0
  52. solara/components/select.py +182 -0
  53. solara/components/select.vue +27 -0
  54. solara/components/slider.py +442 -0
  55. solara/components/slider_date.vue +56 -0
  56. solara/components/spinner-solara.vue +105 -0
  57. solara/components/spinner.py +45 -0
  58. solara/components/sql_code.py +41 -0
  59. solara/components/sql_code.vue +125 -0
  60. solara/components/style.py +105 -0
  61. solara/components/switch.py +71 -0
  62. solara/components/tab_navigation.py +37 -0
  63. solara/components/title.py +90 -0
  64. solara/components/title.vue +38 -0
  65. solara/components/togglebuttons.py +200 -0
  66. solara/components/tooltip.py +61 -0
  67. solara/core.py +42 -0
  68. solara/datatypes.py +143 -0
  69. solara/express.py +241 -0
  70. solara/hooks/__init__.py +4 -0
  71. solara/hooks/dataframe.py +99 -0
  72. solara/hooks/misc.py +263 -0
  73. solara/hooks/use_reactive.py +151 -0
  74. solara/hooks/use_thread.py +129 -0
  75. solara/kitchensink.py +8 -0
  76. solara/lab/__init__.py +34 -0
  77. solara/lab/components/__init__.py +7 -0
  78. solara/lab/components/chat.py +215 -0
  79. solara/lab/components/confirmation_dialog.py +163 -0
  80. solara/lab/components/cross_filter.py +7 -0
  81. solara/lab/components/input_date.py +339 -0
  82. solara/lab/components/input_time.py +133 -0
  83. solara/lab/components/menu.py +181 -0
  84. solara/lab/components/menu.vue +38 -0
  85. solara/lab/components/tabs.py +274 -0
  86. solara/lab/components/theming.py +98 -0
  87. solara/lab/components/theming.vue +72 -0
  88. solara/lab/hooks/__init__.py +0 -0
  89. solara/lab/hooks/dataframe.py +2 -0
  90. solara/lab/toestand.py +3 -0
  91. solara/lab/utils/__init__.py +2 -0
  92. solara/lab/utils/cookies.py +5 -0
  93. solara/lab/utils/dataframe.py +165 -0
  94. solara/lab/utils/headers.py +5 -0
  95. solara/layout.py +44 -0
  96. solara/lifecycle.py +46 -0
  97. solara/minisettings.py +141 -0
  98. solara/py.typed +0 -0
  99. solara/reactive.py +99 -0
  100. solara/routing.py +268 -0
  101. solara/scope/__init__.py +88 -0
  102. solara/scope/types.py +55 -0
  103. solara/server/__init__.py +0 -0
  104. solara/server/app.py +527 -0
  105. solara/server/assets/custom.css +1 -0
  106. solara/server/assets/custom.js +1 -0
  107. solara/server/assets/favicon.png +0 -0
  108. solara/server/assets/favicon.svg +5 -0
  109. solara/server/assets/style.css +1681 -0
  110. solara/server/assets/theme-dark.css +437 -0
  111. solara/server/assets/theme-light.css +420 -0
  112. solara/server/assets/theme.js +3 -0
  113. solara/server/cdn_helper.py +91 -0
  114. solara/server/esm.py +71 -0
  115. solara/server/fastapi.py +5 -0
  116. solara/server/flask.py +297 -0
  117. solara/server/jupyter/__init__.py +2 -0
  118. solara/server/jupyter/cdn_handler.py +28 -0
  119. solara/server/jupyter/server_extension.py +40 -0
  120. solara/server/jupyter/solara.py +91 -0
  121. solara/server/jupytertools.py +46 -0
  122. solara/server/kernel.py +388 -0
  123. solara/server/kernel_context.py +467 -0
  124. solara/server/patch.py +564 -0
  125. solara/server/pyinstaller/__init__.py +9 -0
  126. solara/server/pyinstaller/hook-ipyreact.py +5 -0
  127. solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
  128. solara/server/pyinstaller/hook-solara.py +9 -0
  129. solara/server/qt.py +113 -0
  130. solara/server/reload.py +251 -0
  131. solara/server/server.py +484 -0
  132. solara/server/settings.py +249 -0
  133. solara/server/shell.py +269 -0
  134. solara/server/starlette.py +770 -0
  135. solara/server/static/ansi.js +270 -0
  136. solara/server/static/highlight-dark.css +82 -0
  137. solara/server/static/highlight.css +43 -0
  138. solara/server/static/main-vuetify.js +272 -0
  139. solara/server/static/main.js +163 -0
  140. solara/server/static/solara_bootstrap.py +129 -0
  141. solara/server/static/sun.svg +23 -0
  142. solara/server/static/webworker.js +42 -0
  143. solara/server/telemetry.py +212 -0
  144. solara/server/templates/index.html.j2 +1 -0
  145. solara/server/templates/loader-plain.css +11 -0
  146. solara/server/templates/loader-plain.html +20 -0
  147. solara/server/templates/loader-solara.css +111 -0
  148. solara/server/templates/loader-solara.html +40 -0
  149. solara/server/templates/plain.html +82 -0
  150. solara/server/templates/solara.html.j2 +486 -0
  151. solara/server/threaded.py +84 -0
  152. solara/server/utils.py +44 -0
  153. solara/server/websocket.py +45 -0
  154. solara/settings.py +86 -0
  155. solara/tasks.py +893 -0
  156. solara/template/button.py +16 -0
  157. solara/template/markdown.py +42 -0
  158. solara/template/portal/.flake8 +6 -0
  159. solara/template/portal/.pre-commit-config.yaml +28 -0
  160. solara/template/portal/LICENSE +21 -0
  161. solara/template/portal/Procfile +7 -0
  162. solara/template/portal/mypy.ini +3 -0
  163. solara/template/portal/pyproject.toml +26 -0
  164. solara/template/portal/solara_portal/__init__.py +4 -0
  165. solara/template/portal/solara_portal/components/__init__.py +2 -0
  166. solara/template/portal/solara_portal/components/article.py +28 -0
  167. solara/template/portal/solara_portal/components/data.py +28 -0
  168. solara/template/portal/solara_portal/components/header.py +6 -0
  169. solara/template/portal/solara_portal/components/layout.py +6 -0
  170. solara/template/portal/solara_portal/content/articles/equis-in-vidi.md +85 -0
  171. solara/template/portal/solara_portal/content/articles/substiterat-vati.md +70 -0
  172. solara/template/portal/solara_portal/data.py +60 -0
  173. solara/template/portal/solara_portal/pages/__init__.py +67 -0
  174. solara/template/portal/solara_portal/pages/article/__init__.py +26 -0
  175. solara/template/portal/solara_portal/pages/tabular.py +29 -0
  176. solara/template/portal/solara_portal/pages/viz/__init__.py +70 -0
  177. solara/template/portal/solara_portal/pages/viz/overview.py +14 -0
  178. solara/test/__init__.py +0 -0
  179. solara/test/pytest_plugin.py +783 -0
  180. solara/toestand.py +998 -0
  181. solara/util.py +348 -0
  182. solara/validate_hooks.py +258 -0
  183. solara/website/__init__.py +0 -0
  184. solara/website/assets/custom.css +444 -0
  185. solara/website/assets/images/logo-small.png +0 -0
  186. solara/website/assets/images/logo.svg +17 -0
  187. solara/website/assets/images/logo_white.svg +50 -0
  188. solara/website/assets/theme.js +8 -0
  189. solara/website/components/__init__.py +5 -0
  190. solara/website/components/algolia.py +6 -0
  191. solara/website/components/algolia.vue +24 -0
  192. solara/website/components/algolia_api.vue +202 -0
  193. solara/website/components/breadcrumbs.py +28 -0
  194. solara/website/components/contact.py +144 -0
  195. solara/website/components/docs.py +143 -0
  196. solara/website/components/header.py +75 -0
  197. solara/website/components/mailchimp.py +12 -0
  198. solara/website/components/mailchimp.vue +47 -0
  199. solara/website/components/markdown.py +99 -0
  200. solara/website/components/markdown_nav.vue +34 -0
  201. solara/website/components/notebook.py +171 -0
  202. solara/website/components/sidebar.py +105 -0
  203. solara/website/pages/__init__.py +370 -0
  204. solara/website/pages/about/__init__.py +9 -0
  205. solara/website/pages/about/about.md +3 -0
  206. solara/website/pages/apps/__init__.py +16 -0
  207. solara/website/pages/apps/authorization/__init__.py +119 -0
  208. solara/website/pages/apps/authorization/admin.py +12 -0
  209. solara/website/pages/apps/authorization/users.py +12 -0
  210. solara/website/pages/apps/jupyter-dashboard-1.py +116 -0
  211. solara/website/pages/apps/layout-demo.py +40 -0
  212. solara/website/pages/apps/multipage/__init__.py +38 -0
  213. solara/website/pages/apps/multipage/page1.py +26 -0
  214. solara/website/pages/apps/multipage/page2.py +34 -0
  215. solara/website/pages/apps/scatter.py +136 -0
  216. solara/website/pages/apps/scrolling.py +63 -0
  217. solara/website/pages/apps/tutorial-streamlit.py +18 -0
  218. solara/website/pages/careers/__init__.py +27 -0
  219. solara/website/pages/changelog/__init__.py +10 -0
  220. solara/website/pages/changelog/changelog.md +372 -0
  221. solara/website/pages/contact/__init__.py +34 -0
  222. solara/website/pages/doc_use_download.py +85 -0
  223. solara/website/pages/documentation/__init__.py +90 -0
  224. solara/website/pages/documentation/advanced/__init__.py +9 -0
  225. solara/website/pages/documentation/advanced/content/00-overview.md +1 -0
  226. solara/website/pages/documentation/advanced/content/10-howto/00-overview.md +6 -0
  227. solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md +196 -0
  228. solara/website/pages/documentation/advanced/content/10-howto/20-layout.md +125 -0
  229. solara/website/pages/documentation/advanced/content/10-howto/30-testing.md +417 -0
  230. solara/website/pages/documentation/advanced/content/10-howto/31-debugging.md +69 -0
  231. solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +50 -0
  232. solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md +124 -0
  233. solara/website/pages/documentation/advanced/content/15-reference/00-overview.md +3 -0
  234. solara/website/pages/documentation/advanced/content/15-reference/40-static_files.md +31 -0
  235. solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +72 -0
  236. solara/website/pages/documentation/advanced/content/15-reference/60-static-site-generation.md +59 -0
  237. solara/website/pages/documentation/advanced/content/15-reference/70-search.md +34 -0
  238. solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md +34 -0
  239. solara/website/pages/documentation/advanced/content/15-reference/90-notebook-support.md +7 -0
  240. solara/website/pages/documentation/advanced/content/15-reference/95-caching.md +148 -0
  241. solara/website/pages/documentation/advanced/content/20-understanding/00-introduction.md +10 -0
  242. solara/website/pages/documentation/advanced/content/20-understanding/05-ipywidgets.md +35 -0
  243. solara/website/pages/documentation/advanced/content/20-understanding/06-ipyvuetify.md +42 -0
  244. solara/website/pages/documentation/advanced/content/20-understanding/10-reacton.md +28 -0
  245. solara/website/pages/documentation/advanced/content/20-understanding/12-reacton-basics.md +108 -0
  246. solara/website/pages/documentation/advanced/content/20-understanding/15-anatomy.md +23 -0
  247. solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md +192 -0
  248. solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md +166 -0
  249. solara/website/pages/documentation/advanced/content/20-understanding/20-solara.md +18 -0
  250. solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +256 -0
  251. solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +108 -0
  252. solara/website/pages/documentation/advanced/content/20-understanding/60-voila.md +12 -0
  253. solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +7 -0
  254. solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +187 -0
  255. solara/website/pages/documentation/advanced/content/40-development/00-overview.md +0 -0
  256. solara/website/pages/documentation/advanced/content/40-development/01-contribute.md +45 -0
  257. solara/website/pages/documentation/advanced/content/40-development/10-setup.md +76 -0
  258. solara/website/pages/documentation/api/__init__.py +19 -0
  259. solara/website/pages/documentation/api/cross_filter/__init__.py +9 -0
  260. solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +22 -0
  261. solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +20 -0
  262. solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +20 -0
  263. solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +20 -0
  264. solara/website/pages/documentation/api/hooks/__init__.py +9 -0
  265. solara/website/pages/documentation/api/hooks/use_cross_filter.py +23 -0
  266. solara/website/pages/documentation/api/hooks/use_dark_effective.py +12 -0
  267. solara/website/pages/documentation/api/hooks/use_effect.md +43 -0
  268. solara/website/pages/documentation/api/hooks/use_effect.py +9 -0
  269. solara/website/pages/documentation/api/hooks/use_exception.py +31 -0
  270. solara/website/pages/documentation/api/hooks/use_memo.md +16 -0
  271. solara/website/pages/documentation/api/hooks/use_memo.py +9 -0
  272. solara/website/pages/documentation/api/hooks/use_previous.py +30 -0
  273. solara/website/pages/documentation/api/hooks/use_reactive.py +16 -0
  274. solara/website/pages/documentation/api/hooks/use_state.py +10 -0
  275. solara/website/pages/documentation/api/hooks/use_state_or_update.py +66 -0
  276. solara/website/pages/documentation/api/hooks/use_thread.md +64 -0
  277. solara/website/pages/documentation/api/hooks/use_thread.py +42 -0
  278. solara/website/pages/documentation/api/hooks/use_trait_observe.py +12 -0
  279. solara/website/pages/documentation/api/routing/__init__.py +9 -0
  280. solara/website/pages/documentation/api/routing/generate_routes.py +10 -0
  281. solara/website/pages/documentation/api/routing/generate_routes_directory.py +10 -0
  282. solara/website/pages/documentation/api/routing/resolve_path.py +35 -0
  283. solara/website/pages/documentation/api/routing/route.py +29 -0
  284. solara/website/pages/documentation/api/routing/use_route.py +76 -0
  285. solara/website/pages/documentation/api/routing/use_router.py +16 -0
  286. solara/website/pages/documentation/api/utilities/__init__.py +9 -0
  287. solara/website/pages/documentation/api/utilities/component_vue.py +10 -0
  288. solara/website/pages/documentation/api/utilities/computed.py +16 -0
  289. solara/website/pages/documentation/api/utilities/display.py +16 -0
  290. solara/website/pages/documentation/api/utilities/get_kernel_id.py +16 -0
  291. solara/website/pages/documentation/api/utilities/get_session_id.py +16 -0
  292. solara/website/pages/documentation/api/utilities/memoize.py +35 -0
  293. solara/website/pages/documentation/api/utilities/on_kernel_start.py +44 -0
  294. solara/website/pages/documentation/api/utilities/reactive.py +16 -0
  295. solara/website/pages/documentation/api/utilities/widget.py +104 -0
  296. solara/website/pages/documentation/components/__init__.py +12 -0
  297. solara/website/pages/documentation/components/advanced/__init__.py +9 -0
  298. solara/website/pages/documentation/components/advanced/link.py +25 -0
  299. solara/website/pages/documentation/components/advanced/meta.py +17 -0
  300. solara/website/pages/documentation/components/advanced/style.py +43 -0
  301. solara/website/pages/documentation/components/common.py +9 -0
  302. solara/website/pages/documentation/components/data/__init__.py +9 -0
  303. solara/website/pages/documentation/components/data/dataframe.py +44 -0
  304. solara/website/pages/documentation/components/data/pivot_table.py +81 -0
  305. solara/website/pages/documentation/components/enterprise/__init__.py +9 -0
  306. solara/website/pages/documentation/components/enterprise/avatar.py +24 -0
  307. solara/website/pages/documentation/components/enterprise/avatar_menu.py +25 -0
  308. solara/website/pages/documentation/components/input/__init__.py +9 -0
  309. solara/website/pages/documentation/components/input/button.py +23 -0
  310. solara/website/pages/documentation/components/input/checkbox.py +10 -0
  311. solara/website/pages/documentation/components/input/file_browser.py +30 -0
  312. solara/website/pages/documentation/components/input/file_drop.py +76 -0
  313. solara/website/pages/documentation/components/input/input.py +43 -0
  314. solara/website/pages/documentation/components/input/select.py +22 -0
  315. solara/website/pages/documentation/components/input/slider.py +29 -0
  316. solara/website/pages/documentation/components/input/switch.py +10 -0
  317. solara/website/pages/documentation/components/input/togglebuttons.py +21 -0
  318. solara/website/pages/documentation/components/lab/__init__.py +9 -0
  319. solara/website/pages/documentation/components/lab/chat.py +109 -0
  320. solara/website/pages/documentation/components/lab/confirmation_dialog.py +55 -0
  321. solara/website/pages/documentation/components/lab/cookies_headers.py +48 -0
  322. solara/website/pages/documentation/components/lab/input_date.py +20 -0
  323. solara/website/pages/documentation/components/lab/input_time.py +15 -0
  324. solara/website/pages/documentation/components/lab/menu.py +22 -0
  325. solara/website/pages/documentation/components/lab/tab.py +25 -0
  326. solara/website/pages/documentation/components/lab/tabs.py +45 -0
  327. solara/website/pages/documentation/components/lab/task.py +11 -0
  328. solara/website/pages/documentation/components/lab/theming.py +74 -0
  329. solara/website/pages/documentation/components/lab/use_task.py +11 -0
  330. solara/website/pages/documentation/components/layout/__init__.py +9 -0
  331. solara/website/pages/documentation/components/layout/app_bar.py +16 -0
  332. solara/website/pages/documentation/components/layout/app_bar_title.py +16 -0
  333. solara/website/pages/documentation/components/layout/app_layout.py +24 -0
  334. solara/website/pages/documentation/components/layout/card.py +15 -0
  335. solara/website/pages/documentation/components/layout/card_actions.py +16 -0
  336. solara/website/pages/documentation/components/layout/column.py +30 -0
  337. solara/website/pages/documentation/components/layout/columns.py +27 -0
  338. solara/website/pages/documentation/components/layout/columns_responsive.py +66 -0
  339. solara/website/pages/documentation/components/layout/details.py +13 -0
  340. solara/website/pages/documentation/components/layout/griddraggable.py +62 -0
  341. solara/website/pages/documentation/components/layout/gridfixed.py +19 -0
  342. solara/website/pages/documentation/components/layout/hbox.py +18 -0
  343. solara/website/pages/documentation/components/layout/row.py +30 -0
  344. solara/website/pages/documentation/components/layout/sidebar.py +24 -0
  345. solara/website/pages/documentation/components/layout/vbox.py +19 -0
  346. solara/website/pages/documentation/components/output/__init__.py +9 -0
  347. solara/website/pages/documentation/components/output/file_download.py +11 -0
  348. solara/website/pages/documentation/components/output/html.py +19 -0
  349. solara/website/pages/documentation/components/output/image.py +11 -0
  350. solara/website/pages/documentation/components/output/markdown.py +57 -0
  351. solara/website/pages/documentation/components/output/markdown_editor.py +51 -0
  352. solara/website/pages/documentation/components/output/sql_code.py +83 -0
  353. solara/website/pages/documentation/components/output/tooltip.py +11 -0
  354. solara/website/pages/documentation/components/page/__init__.py +9 -0
  355. solara/website/pages/documentation/components/page/head.py +15 -0
  356. solara/website/pages/documentation/components/page/title.py +25 -0
  357. solara/website/pages/documentation/components/status/__init__.py +9 -0
  358. solara/website/pages/documentation/components/status/error.py +39 -0
  359. solara/website/pages/documentation/components/status/info.py +39 -0
  360. solara/website/pages/documentation/components/status/progress.py +10 -0
  361. solara/website/pages/documentation/components/status/spinner.py +11 -0
  362. solara/website/pages/documentation/components/status/success.py +40 -0
  363. solara/website/pages/documentation/components/status/warning.py +47 -0
  364. solara/website/pages/documentation/components/viz/__init__.py +9 -0
  365. solara/website/pages/documentation/components/viz/altair.py +42 -0
  366. solara/website/pages/documentation/components/viz/echarts.py +77 -0
  367. solara/website/pages/documentation/components/viz/matplotlib.py +30 -0
  368. solara/website/pages/documentation/components/viz/plotly.py +63 -0
  369. solara/website/pages/documentation/components/viz/plotly_express.py +41 -0
  370. solara/website/pages/documentation/examples/__init__.py +54 -0
  371. solara/website/pages/documentation/examples/ai/__init__.py +11 -0
  372. solara/website/pages/documentation/examples/ai/chatbot.py +113 -0
  373. solara/website/pages/documentation/examples/ai/tokenizer.py +107 -0
  374. solara/website/pages/documentation/examples/basics/__init__.py +10 -0
  375. solara/website/pages/documentation/examples/basics/sine.py +28 -0
  376. solara/website/pages/documentation/examples/fullscreen/__init__.py +10 -0
  377. solara/website/pages/documentation/examples/fullscreen/authorization.py +3 -0
  378. solara/website/pages/documentation/examples/fullscreen/layout_demo.py +3 -0
  379. solara/website/pages/documentation/examples/fullscreen/multipage.py +3 -0
  380. solara/website/pages/documentation/examples/fullscreen/scatter.py +3 -0
  381. solara/website/pages/documentation/examples/fullscreen/scrolling.py +3 -0
  382. solara/website/pages/documentation/examples/fullscreen/tutorial_streamlit.py +3 -0
  383. solara/website/pages/documentation/examples/general/__init__.py +10 -0
  384. solara/website/pages/documentation/examples/general/custom_storage.py +70 -0
  385. solara/website/pages/documentation/examples/general/deploy_model.py +115 -0
  386. solara/website/pages/documentation/examples/general/live_update.py +32 -0
  387. solara/website/pages/documentation/examples/general/login_oauth.py +81 -0
  388. solara/website/pages/documentation/examples/general/mycard.vue +58 -0
  389. solara/website/pages/documentation/examples/general/pokemon_search.py +51 -0
  390. solara/website/pages/documentation/examples/general/vue_component.py +50 -0
  391. solara/website/pages/documentation/examples/ipycanvas.py +49 -0
  392. solara/website/pages/documentation/examples/libraries/__init__.py +10 -0
  393. solara/website/pages/documentation/examples/libraries/altair.py +65 -0
  394. solara/website/pages/documentation/examples/libraries/bqplot.py +39 -0
  395. solara/website/pages/documentation/examples/libraries/ipyleaflet.py +33 -0
  396. solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +66 -0
  397. solara/website/pages/documentation/examples/utilities/__init__.py +10 -0
  398. solara/website/pages/documentation/examples/utilities/calculator.py +157 -0
  399. solara/website/pages/documentation/examples/utilities/countdown_timer.py +62 -0
  400. solara/website/pages/documentation/examples/utilities/todo.py +196 -0
  401. solara/website/pages/documentation/examples/visualization/__init__.py +6 -0
  402. solara/website/pages/documentation/examples/visualization/annotator.py +67 -0
  403. solara/website/pages/documentation/examples/visualization/linked_views.py +81 -0
  404. solara/website/pages/documentation/examples/visualization/plotly.py +44 -0
  405. solara/website/pages/documentation/faq/__init__.py +12 -0
  406. solara/website/pages/documentation/faq/content/99-faq.md +112 -0
  407. solara/website/pages/documentation/getting_started/__init__.py +9 -0
  408. solara/website/pages/documentation/getting_started/content/00-quickstart.md +107 -0
  409. solara/website/pages/documentation/getting_started/content/01-introduction.md +125 -0
  410. solara/website/pages/documentation/getting_started/content/02-installing.md +134 -0
  411. solara/website/pages/documentation/getting_started/content/04-tutorials/00-overview.md +14 -0
  412. solara/website/pages/documentation/getting_started/content/04-tutorials/10_data_science.py +13 -0
  413. solara/website/pages/documentation/getting_started/content/04-tutorials/20-web-app.md +89 -0
  414. solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md +124 -0
  415. solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md +146 -0
  416. solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md +144 -0
  417. solara/website/pages/documentation/getting_started/content/04-tutorials/60-jupyter-dashboard-part1.py +65 -0
  418. solara/website/pages/documentation/getting_started/content/04-tutorials/SF_crime_sample.csv.gz +0 -0
  419. solara/website/pages/documentation/getting_started/content/04-tutorials/_data_science.ipynb +445 -0
  420. solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +1021 -0
  421. solara/website/pages/documentation/getting_started/content/05-fundamentals/00-overview.md +11 -0
  422. solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +228 -0
  423. solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +278 -0
  424. solara/website/pages/documentation/getting_started/content/07-deploying/00-overview.md +7 -0
  425. solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +305 -0
  426. solara/website/pages/documentation/getting_started/content/07-deploying/20-cloud-hosted.md +80 -0
  427. solara/website/pages/documentation/getting_started/content/80-what-is-lab.md +7 -0
  428. solara/website/pages/documentation/getting_started/content/90-troubleshoot.md +26 -0
  429. solara/website/pages/docutils.py +38 -0
  430. solara/website/pages/home.vue +1199 -0
  431. solara/website/pages/our_team/__init__.py +83 -0
  432. solara/website/pages/pricing/__init__.py +31 -0
  433. solara/website/pages/roadmap/__init__.py +11 -0
  434. solara/website/pages/roadmap/roadmap.md +47 -0
  435. solara/website/pages/scale_ipywidgets.py +45 -0
  436. solara/website/pages/showcase/__init__.py +105 -0
  437. solara/website/pages/showcase/domino_code_assist.py +60 -0
  438. solara/website/pages/showcase/planeto_tessa.py +19 -0
  439. solara/website/pages/showcase/solara_dev.py +54 -0
  440. solara/website/pages/showcase/solarathon_2023_team_2.py +22 -0
  441. solara/website/pages/showcase/solarathon_2023_team_4.py +22 -0
  442. solara/website/pages/showcase/solarathon_2023_team_5.py +23 -0
  443. solara/website/pages/showcase/solarathon_2023_team_6.py +34 -0
  444. solara/website/pages/showcase/wanderlust.py +27 -0
  445. solara/website/public/beach.jpeg +0 -0
  446. solara/website/public/logo.svg +6 -0
  447. solara/website/public/social/discord.svg +1 -0
  448. solara/website/public/social/github.svg +1 -0
  449. solara/website/public/social/twitter.svg +3 -0
  450. solara/website/public/success.html +25 -0
  451. solara/website/templates/index.html.j2 +117 -0
  452. solara/website/utils.py +51 -0
  453. solara/widgets/__init__.py +1 -0
  454. solara/widgets/vue/gridlayout.vue +107 -0
  455. solara/widgets/vue/html.vue +4 -0
  456. solara/widgets/vue/navigator.vue +134 -0
  457. solara/widgets/vue/vegalite.vue +130 -0
  458. solara/widgets/widgets.py +74 -0
  459. solara_ui-1.45.0.data/data/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
  460. solara_ui-1.45.0.data/data/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
  461. solara_ui-1.45.0.dist-info/METADATA +162 -0
  462. solara_ui-1.45.0.dist-info/RECORD +464 -0
  463. solara_ui-1.45.0.dist-info/WHEEL +4 -0
  464. solara_ui-1.45.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,770 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import math
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+ import threading
9
+ import typing
10
+ from typing import Any, Dict, List, Optional, Set, Union, cast
11
+ from uuid import uuid4
12
+ import warnings
13
+
14
+ import anyio
15
+ import starlette.websockets
16
+ import uvicorn.server
17
+ import websockets.legacy.http
18
+ import websockets.exceptions
19
+
20
+ from solara.server.utils import path_is_child_of
21
+
22
+ try:
23
+ import solara_enterprise
24
+
25
+ del solara_enterprise
26
+ has_solara_enterprise = True
27
+ except ImportError:
28
+ has_solara_enterprise = False
29
+ if has_solara_enterprise and sys.version_info[:2] > (3, 6):
30
+ has_auth_support = True
31
+ from solara_enterprise.auth.middleware import MutateDetectSessionMiddleware
32
+ from solara_enterprise.auth.starlette import (
33
+ AuthBackend,
34
+ authorize,
35
+ get_user,
36
+ login,
37
+ logout,
38
+ )
39
+ else:
40
+ has_auth_support = False
41
+
42
+ from starlette.applications import Starlette
43
+ from starlette.exceptions import HTTPException
44
+ from starlette.middleware import Middleware
45
+ from starlette.middleware.authentication import AuthenticationMiddleware
46
+ from starlette.middleware.gzip import GZipMiddleware
47
+ from starlette.requests import HTTPConnection, Request
48
+ from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
49
+ from starlette.routing import Mount, Route, WebSocketRoute
50
+ from starlette.staticfiles import StaticFiles
51
+ from starlette.types import Receive, Scope, Send
52
+
53
+ import solara
54
+ import solara.settings
55
+ from solara.server.threaded import ServerBase
56
+
57
+ from . import app as appmod
58
+ from . import kernel_context, server, settings, telemetry, websocket
59
+ from .cdn_helper import cdn_url_path, get_path
60
+
61
+ os.environ["SERVER_SOFTWARE"] = "solara/" + str(solara.__version__)
62
+ limiter: Optional[anyio.CapacityLimiter] = None
63
+ lock = threading.Lock()
64
+
65
+
66
+ def _ensure_limiter():
67
+ # in older anyios (<4) the limiter can only be created in an async context
68
+ # so we call this in a starlette handler
69
+ global limiter
70
+ if limiter is None:
71
+ with lock:
72
+ if limiter is None:
73
+ limiter = anyio.CapacityLimiter(settings.kernel.max_count if settings.kernel.max_count is not None else math.inf)
74
+
75
+
76
+ logger = logging.getLogger("solara.server.fastapi")
77
+ # if we add these to the router, the server_test does not run (404's)
78
+ prefix = ""
79
+
80
+ # The limit for starlette's http traffic should come from h11's DEFAULT_MAX_INCOMPLETE_EVENT_SIZE=16kb
81
+ # In practice, testing with 132kb cookies (server_test.py:test_large_cookie) seems to work fine.
82
+ # For the websocket, the limit is set to 4kb till 10.4, see
83
+ # * https://github.com/aaugustin/websockets/blob/10.4/src/websockets/legacy/http.py#L14
84
+ # Later releases should set this to 8kb. See
85
+ # * https://github.com/aaugustin/websockets/commit/8ce4739b7efed3ac78b287da7fb5e537f78e72aa
86
+ # * https://github.com/aaugustin/websockets/issues/743
87
+ # Since starlette seems to accept really large values for http, lets do the same for websockets
88
+ # An arbitrarily large value we settled on for now is 32kb
89
+ # If we don't do this, users with many cookies will fail to get a websocket connection.
90
+ ws_major_version = int(websockets.__version__.split(".")[0])
91
+ if ws_major_version >= 13:
92
+ websockets.legacy.http.MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", str(1024 * 32))) # type: ignore
93
+ else:
94
+ websockets.legacy.http.MAX_LINE = 1024 * 32 # type: ignore
95
+
96
+
97
+ class WebsocketDebugInfo:
98
+ lock = threading.Lock()
99
+ attempts = 0
100
+ connecting = 0
101
+ open = 0
102
+ closed = 0
103
+
104
+
105
+ class WebsocketWrapper(websocket.WebsocketWrapper):
106
+ ws: starlette.websockets.WebSocket
107
+
108
+ def __init__(self, ws: starlette.websockets.WebSocket, portal: Optional[anyio.from_thread.BlockingPortal]) -> None:
109
+ self.ws = ws
110
+ self.portal = portal
111
+ self.to_send: List[Union[str, bytes]] = []
112
+ # following https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
113
+ # we store a strong reference
114
+ self.tasks: Set[asyncio.Task] = set()
115
+ self.event_loop = asyncio.get_event_loop()
116
+ self._thread_id = threading.get_ident()
117
+ if settings.main.experimental_performance:
118
+ self.task = asyncio.ensure_future(self.process_messages_task())
119
+
120
+ async def process_messages_task(self):
121
+ while True:
122
+ await asyncio.sleep(0.05)
123
+ while len(self.to_send) > 0:
124
+ first = self.to_send.pop(0)
125
+ if isinstance(first, bytes):
126
+ await self._send_bytes_exc(first)
127
+ else:
128
+ await self._send_text_exc(first)
129
+
130
+ async def _send_bytes_exc(self, data: bytes):
131
+ # make sures we catch the starlette/websockets specific exception
132
+ # and re-raise it as a websocket.WebSocketDisconnect
133
+ try:
134
+ await self.ws.send_bytes(data)
135
+ except RuntimeError as e:
136
+ # starlette throws a RuntimeError once you call send after the connection is closed
137
+ # or RuntimeError: Unexpected ASGI message 'websocket.send', after sending 'websocket.close' or response already completed.
138
+ # from uvicorn.protocols.websockets.websockets_impl.py
139
+ if "close message" in repr(e) or "websocket.close" in repr(e):
140
+ raise websocket.WebSocketDisconnect() from e
141
+ else:
142
+ raise
143
+ except (websockets.exceptions.ConnectionClosed, starlette.websockets.WebSocketDisconnect, RuntimeError) as e:
144
+ raise websocket.WebSocketDisconnect() from e
145
+
146
+ async def _send_text_exc(self, data: str):
147
+ # make sures we catch the starlette/websockets specific exception
148
+ # and re-raise it as a websocket.WebSocketDisconnect
149
+ try:
150
+ await self.ws.send_text(data)
151
+ except RuntimeError as e:
152
+ if "close message" in repr(e) or "websocket.close" in repr(e):
153
+ raise websocket.WebSocketDisconnect() from e
154
+ else:
155
+ raise
156
+ except (websockets.exceptions.ConnectionClosed, starlette.websockets.WebSocketDisconnect, RuntimeError) as e:
157
+ raise websocket.WebSocketDisconnect() from e
158
+
159
+ def close(self):
160
+ async def _close_exc():
161
+ try:
162
+ await self.ws.close()
163
+ except RuntimeError as e:
164
+ if "close message" in repr(e) or "websocket.close" in repr(e):
165
+ raise websocket.WebSocketDisconnect() from e
166
+ else:
167
+ raise
168
+ except (websockets.exceptions.ConnectionClosed, starlette.websockets.WebSocketDisconnect, RuntimeError) as e:
169
+ raise websocket.WebSocketDisconnect() from e
170
+
171
+ if self.portal is None:
172
+ asyncio.ensure_future(_close_exc())
173
+ else:
174
+ self.portal.call(_close_exc)
175
+
176
+ def send_text(self, data: str) -> None:
177
+ if self.portal is None:
178
+ task = self.event_loop.create_task(self._send_text_exc(data))
179
+ self.tasks.add(task)
180
+ task.add_done_callback(self.tasks.discard)
181
+ else:
182
+ if settings.main.experimental_performance:
183
+ self.to_send.append(data)
184
+ else:
185
+ if self._thread_id == threading.get_ident():
186
+ warnings.warn("""You are triggering a websocket send from the event loop thread.
187
+ Support for this is experimental, and to avoid this message, make sure you trigger updates
188
+ that trigger this from a different thread, e.g.:
189
+
190
+ from anyio import to_thread
191
+ await to_thread.run_sync(my_update)
192
+ """)
193
+ task = self.event_loop.create_task(self._send_text_exc(data))
194
+ self.tasks.add(task)
195
+ task.add_done_callback(self.tasks.discard)
196
+ else:
197
+ self.portal.call(self._send_text_exc, data)
198
+
199
+ def send_bytes(self, data: bytes) -> None:
200
+ if self.portal is None:
201
+ task = self.event_loop.create_task(self._send_bytes_exc(data))
202
+ self.tasks.add(task)
203
+ task.add_done_callback(self.tasks.discard)
204
+ else:
205
+ if settings.main.experimental_performance:
206
+ self.to_send.append(data)
207
+ else:
208
+ if self._thread_id == threading.get_ident():
209
+ warnings.warn("""You are triggering a websocket send from the event loop thread.
210
+ Support for this is experimental, and to avoid this message, make sure you trigger updates
211
+ that trigger this from a different thread, e.g.:
212
+
213
+ from anyio import to_thread
214
+ await to_thread.run_sync(my_update)
215
+ """)
216
+ task = self.event_loop.create_task(self._send_bytes_exc(data))
217
+ self.tasks.add(task)
218
+ task.add_done_callback(self.tasks.discard)
219
+
220
+ self.portal.call(self._send_bytes_exc, data)
221
+
222
+ async def receive(self):
223
+ if self.portal is None:
224
+ message = await asyncio.ensure_future(self.ws.receive())
225
+ else:
226
+ if hasattr(self.portal, "start_task_soon"):
227
+ # version 3+
228
+ fut = self.portal.start_task_soon(self.ws.receive)
229
+ else:
230
+ fut = self.portal.spawn_task(self.ws.receive)
231
+
232
+ message = await asyncio.wrap_future(fut)
233
+ if "text" in message:
234
+ return message["text"]
235
+ elif "bytes" in message:
236
+ return message["bytes"]
237
+ elif message.get("type") == "websocket.disconnect":
238
+ raise websocket.WebSocketDisconnect()
239
+ else:
240
+ raise RuntimeError(f"Unknown message type {message}")
241
+
242
+
243
+ class ServerStarlette(ServerBase):
244
+ server: uvicorn.server.Server
245
+ name = "starlette"
246
+
247
+ def __init__(self, port: int, host: str = "localhost", starlette_app=None, **kwargs):
248
+ super().__init__(port, host, **kwargs)
249
+ self.app = starlette_app or app
250
+
251
+ def has_started(self):
252
+ return self.server.started
253
+
254
+ def signal_stop(self):
255
+ self.server.should_exit = True
256
+ # this cause uvicorn to not wait for background tasks, e.g.:
257
+ # <Task pending name='Task-55'
258
+ # coro=<WebSocketProtocol.run_asgi() running at
259
+ # /.../uvicorn/protocols/websockets/websockets_impl.py:184>
260
+ # wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x16896aa00>()]>
261
+ # cb=[WebSocketProtocol.on_task_complete()]>
262
+ self.server.force_exit = True
263
+ self.server.lifespan.should_exit = True
264
+
265
+ def serve(self):
266
+ from uvicorn.config import Config
267
+ from uvicorn.server import Server
268
+
269
+ if sys.version_info[:2] < (3, 7):
270
+ # make python 3.6 work
271
+ import asyncio
272
+
273
+ loop = asyncio.new_event_loop()
274
+ asyncio.set_event_loop(loop)
275
+
276
+ # uvloop will trigger a: RuntimeError: There is no current event loop in thread 'fastapi-thread'
277
+ config = Config(self.app, host=self.host, port=self.port, **self.kwargs, access_log=False, loop="asyncio")
278
+ self.server = Server(config=config)
279
+ self.started.set()
280
+ self.server.run()
281
+
282
+
283
+ async def kernels(id):
284
+ return JSONResponse({"name": "lala", "id": "dsa"})
285
+
286
+
287
+ async def kernel_connection(ws: starlette.websockets.WebSocket):
288
+ _ensure_limiter()
289
+ try:
290
+ with WebsocketDebugInfo.lock:
291
+ WebsocketDebugInfo.attempts += 1
292
+ WebsocketDebugInfo.connecting += 1
293
+ await _kernel_connection(ws)
294
+ finally:
295
+ with WebsocketDebugInfo.lock:
296
+ WebsocketDebugInfo.closed += 1
297
+ WebsocketDebugInfo.open -= 1
298
+
299
+
300
+ async def _kernel_connection(ws: starlette.websockets.WebSocket):
301
+ session_id = ws.cookies.get(server.COOKIE_KEY_SESSION_ID)
302
+
303
+ if settings.oauth.private and not has_auth_support:
304
+ raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
305
+ if has_auth_support and "session" in ws.scope:
306
+ user = get_user(ws)
307
+ if user is None and settings.oauth.private:
308
+ await ws.accept()
309
+ logger.error("app is private, requires login")
310
+ await ws.close(code=1008, reason="app is private, requires login")
311
+ return
312
+ else:
313
+ user = None
314
+
315
+ if not session_id:
316
+ logger.warning("no session cookie")
317
+ session_id = "session-id-cookie-unavailable:" + str(uuid4())
318
+ # we use the jupyter session_id query parameter as the key/id
319
+ # for a page scope.
320
+ page_id = ws.query_params["session_id"]
321
+ if not page_id:
322
+ logger.error("no page_id")
323
+ kernel_id = ws.path_params["kernel_id"]
324
+ if not kernel_id:
325
+ logger.error("no kernel_id")
326
+ await ws.close()
327
+ return
328
+ logger.info("Solara kernel requested for session_id=%s kernel_id=%s", session_id, kernel_id)
329
+ await ws.accept()
330
+ with WebsocketDebugInfo.lock:
331
+ WebsocketDebugInfo.connecting -= 1
332
+ WebsocketDebugInfo.open += 1
333
+
334
+ async def run(ws_wrapper: WebsocketWrapper):
335
+ if kernel_context.async_context_id is not None:
336
+ kernel_context.async_context_id.set(uuid4().hex)
337
+ assert session_id is not None
338
+ assert kernel_id is not None
339
+ telemetry.connection_open(session_id)
340
+ headers_dict: Dict[str, List[str]] = {}
341
+ for k, v in ws.headers.items():
342
+ if k not in headers_dict.keys():
343
+ headers_dict[k] = [v]
344
+ else:
345
+ headers_dict[k].append(v)
346
+ await server.app_loop(ws_wrapper, ws.cookies, headers_dict, session_id, kernel_id, page_id, user)
347
+
348
+ def websocket_thread_runner(ws_wrapper: WebsocketWrapper, portal: anyio.from_thread.BlockingPortal):
349
+ async def run_wrapper():
350
+ try:
351
+ await run(ws_wrapper)
352
+ except: # noqa
353
+ if portal is not None:
354
+ await portal.stop(cancel_remaining=True)
355
+ raise
356
+ finally:
357
+ telemetry.connection_close(session_id)
358
+
359
+ # sometimes throws: RuntimeError: Already running asyncio in this thread
360
+ anyio.run(run_wrapper) # type: ignore
361
+
362
+ # this portal allows us to sync call the websocket calls from this current event loop we are in
363
+ # each websocket however, is handled from a separate thread
364
+ try:
365
+ if settings.kernel.threaded:
366
+ async with anyio.from_thread.BlockingPortal() as portal:
367
+ ws_wrapper = WebsocketWrapper(ws, portal)
368
+ thread_return = anyio.to_thread.run_sync(websocket_thread_runner, ws_wrapper, portal, limiter=limiter) # type: ignore
369
+ await thread_return
370
+ else:
371
+ ws_wrapper = WebsocketWrapper(ws, None)
372
+ await run(ws_wrapper)
373
+ finally:
374
+ if settings.main.experimental_performance:
375
+ try:
376
+ ws_wrapper.task.cancel()
377
+ except: # noqa
378
+ logger.exception("error cancelling websocket task")
379
+ try:
380
+ await ws.close()
381
+ except: # noqa
382
+ pass
383
+
384
+
385
+ def close(request: Request):
386
+ kernel_id = request.path_params["kernel_id"]
387
+ page_id = request.query_params["session_id"]
388
+ context = kernel_context.contexts.get(kernel_id, None)
389
+ if context is not None:
390
+ context.page_close(page_id)
391
+ response = HTMLResponse(content="", status_code=200)
392
+ return response
393
+
394
+
395
+ async def root(request: Request, fullpath: str = ""):
396
+ forwarded_host = request.headers.get("x-forwarded-host")
397
+ forwarded_proto = request.headers.get("x-forwarded-proto")
398
+ host = request.headers.get("host")
399
+ if forwarded_proto and forwarded_proto != request.scope["scheme"]:
400
+ warnings.warn(f"""Header x-forwarded-proto={forwarded_proto!r} does not match scheme={request.scope["scheme"]!r} as given by the asgi framework (probably uvicorn)
401
+
402
+ This might be a configuration mismatch behind a reverse proxy and can cause issues with redirect urls, and auth.
403
+
404
+ Most likely, you need to trust your reverse proxy server, see:
405
+ https://solara.dev/documentation/getting_started/deploying/self-hosted
406
+
407
+ If you use uvicorn (the default when you use `solara run`), make sure you
408
+ configure the following environment variables for uvicorn correctly:
409
+ UVICORN_PROXY_HEADERS=1 # only needed for uvicorn < 0.10, since it is the default after 0.10
410
+ FORWARDED_ALLOW_IPS="127.0.0.1" # 127.0.0.1 is the default, replace this by the ip of the proxy server
411
+
412
+ Make sure you replace the IP with the correct IP of the reverse proxy server (instead of 127.0.0.1).
413
+
414
+ If you are sure that only the reverse proxy can reach the solara server, you can consider setting:
415
+ FORWARDED_ALLOW_IPS="*" # This can be a security risk, only use when you know what you are doing
416
+ """)
417
+ if settings.oauth.private and not has_auth_support:
418
+ raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
419
+ root_path = settings.main.root_path or ""
420
+ if not settings.main.base_url:
421
+ # Note:
422
+ # starlette does not respect x-forwarded-host, and therefore
423
+ # base_url and expected_origin below could be different
424
+ # x-forwarded-host should only be considered if the same criteria in
425
+ # uvicorn's ProxyHeadersMiddleware accepts x-forwarded-proto
426
+ settings.main.base_url = str(request.base_url)
427
+ # if not explicltly set,
428
+ configured_root_path = settings.main.root_path
429
+ scope = request.scope
430
+ root_path_asgi = scope.get("route_root_path", scope.get("root_path", ""))
431
+ if settings.main.root_path is None:
432
+ # use the default root path from the app, which seems to also include the path
433
+ # if we are mounted under a path
434
+ root_path = root_path_asgi
435
+ logger.debug("root_path: %s", root_path)
436
+ # or use the script-name header, for instance when running under a reverse proxy
437
+ script_name = request.headers.get("script-name")
438
+ if script_name:
439
+ logger.debug("override root_path using script-name header from %s to %s", root_path, script_name)
440
+ root_path = script_name
441
+ script_name = request.headers.get("x-script-name")
442
+ if script_name:
443
+ logger.debug("override root_path using x-script-name header from %s to %s", root_path, script_name)
444
+ root_path = script_name
445
+ settings.main.root_path = root_path
446
+
447
+ # lets be flexible about the trailing slash
448
+ # TODO: maybe we should be more strict about the trailing slash
449
+ naked_root_path = settings.main.root_path.rstrip("/")
450
+ naked_base_url = settings.main.base_url.rstrip("/")
451
+ if not naked_base_url.endswith(naked_root_path):
452
+ msg = f"""base url {naked_base_url!r} does not end with root path {naked_root_path!r}
453
+
454
+ This could be a configuration mismatch behind a reverse proxy and can cause issues with redirect urls, and auth.
455
+
456
+ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
457
+ """
458
+ if "script-name" in request.headers:
459
+ msg += f"""It looks like the reverse proxy sets the script-name header to {request.headers["script-name"]!r}
460
+ """
461
+ if "x-script-name" in request.headers:
462
+ msg += f"""It looks like the reverse proxy sets the x-script-name header to {request.headers["x-script-name"]!r}
463
+ """
464
+ if configured_root_path:
465
+ msg += f"""It looks like the root path was configured to {configured_root_path!r} in the settings
466
+ """
467
+ if root_path_asgi:
468
+ msg += f"""It looks like the root path set by the asgi framework was configured to {root_path_asgi!r}
469
+ """
470
+ warnings.warn(msg)
471
+ if host and forwarded_host and forwarded_proto:
472
+ port = request.base_url.port
473
+ ports = {"http": 80, "https": 443}
474
+ expected_origin = f"{forwarded_proto}://{forwarded_host}"
475
+ if port and port != ports[forwarded_proto]:
476
+ expected_origin += f":{port}"
477
+ starlette_origin = settings.main.base_url
478
+ # strip off trailing / because we compare to the naked root path
479
+ starlette_origin = starlette_origin.rstrip("/")
480
+ if naked_root_path:
481
+ # take off the root path
482
+ starlette_origin = starlette_origin[: -len(naked_root_path)]
483
+ if starlette_origin != expected_origin:
484
+ warnings.warn(f"""Origin as determined by starlette ({starlette_origin!r}) does not match expected origin ({expected_origin!r}) based on x-forwarded-proto ({forwarded_proto!r}) and x-forwarded-host ({forwarded_host!r}) headers.
485
+
486
+ This might be a configuration mismatch behind a reverse proxy and can cause issues with redirect urls, and auth.
487
+ Most likely your proxy server sets the host header incorrectly (value for this request was {host!r})
488
+
489
+ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
490
+ """)
491
+
492
+ request_path = request.url.path
493
+ if request_path.startswith(root_path):
494
+ request_path = request_path[len(root_path) :]
495
+ if request_path in server._redirects.keys():
496
+ return RedirectResponse(server._redirects[request_path])
497
+
498
+ content = server.read_root(request_path, root_path)
499
+ if content is None:
500
+ if settings.oauth.private and not request.user.is_authenticated:
501
+ raise HTTPException(status_code=401, detail="Unauthorized")
502
+ raise HTTPException(status_code=404, detail="Page not found by Solara router")
503
+
504
+ if settings.oauth.private and not request.user.is_authenticated:
505
+ from solara_enterprise.auth.starlette import login
506
+
507
+ return await login(request)
508
+
509
+ response = HTMLResponse(content=content)
510
+ session_id = request.cookies.get(server.COOKIE_KEY_SESSION_ID) or str(uuid4())
511
+ samesite = "lax"
512
+ secure = False
513
+ httponly = settings.session.http_only
514
+ # we want samesite, so we can set a cookie when embedded in an iframe, such as on huggingface
515
+ # however, samesite=none requires Secure https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
516
+ # when hosted on the localhost domain we can always set the Secure flag
517
+ # to allow samesite https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
518
+ if request.scope["scheme"] == "https" or request.headers.get("x-forwarded-proto", "http") == "https" or request.base_url.hostname == "localhost":
519
+ samesite = "none"
520
+ secure = True
521
+ elif request.base_url.hostname != "localhost":
522
+ warnings.warn(f"""Cookies with samesite=none require https, but according to the asgi framework, the scheme is {request.scope["scheme"]!r}
523
+ and the x-forwarded-proto header is {request.headers.get("x-forwarded-proto", "http")!r}. We will fallback to samesite=lax.
524
+
525
+ If you embed solara in an iframe, make sure you forward the x-forwarded-proto header correctly so that the session cookie can be set.
526
+
527
+ See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite for more information on samesite cookies.
528
+
529
+ Also check out the following Solara documentation:
530
+ * https://solara.dev/documentation/getting_started/deploying/self-hosted
531
+ * https://solara.dev/documentation/advanced/howto/embed
532
+ """)
533
+ response.set_cookie(
534
+ server.COOKIE_KEY_SESSION_ID,
535
+ value=session_id,
536
+ expires="Fri, 01 Jan 2038 00:00:00 GMT",
537
+ samesite=samesite, # type: ignore
538
+ secure=secure, # type: ignore
539
+ httponly=httponly, # type: ignore
540
+ ) # type: ignore
541
+ return response
542
+
543
+
544
+ class StaticFilesOptionalAuth(StaticFiles):
545
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
546
+ conn = HTTPConnection(scope)
547
+ if settings.oauth.private and not has_auth_support:
548
+ raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
549
+ if has_auth_support and settings.oauth.private and not conn.user.is_authenticated:
550
+ raise HTTPException(status_code=401, detail="Unauthorized")
551
+ await super().__call__(scope, receive, send)
552
+
553
+
554
+ class StaticNbFiles(StaticFilesOptionalAuth):
555
+ def get_directories(
556
+ self,
557
+ directory: Union[str, "os.PathLike[str]", None] = None,
558
+ packages=None, # type: ignore
559
+ ) -> List[Union[str, "os.PathLike[str]"]]:
560
+ return cast(List[Union[str, "os.PathLike[str]"]], server.nbextensions_directories)
561
+
562
+ # follow symlinks
563
+ # from https://github.com/encode/starlette/pull/1377/files
564
+ def lookup_path(self, path: str) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
565
+ for directory in self.all_directories:
566
+ directory = os.path.realpath(directory)
567
+ original_path = os.path.join(directory, path)
568
+ full_path = os.path.realpath(original_path)
569
+ # return early if someone tries to access a file outside of the directory
570
+ if not path_is_child_of(Path(original_path), Path(directory)):
571
+ return "", None
572
+ try:
573
+ return full_path, os.stat(full_path)
574
+ except (FileNotFoundError, NotADirectoryError):
575
+ continue
576
+ return "", None
577
+
578
+
579
+ class StaticPublic(StaticFilesOptionalAuth):
580
+ def lookup_path(self, *args, **kwargs):
581
+ self.all_directories = self.get_directories(None, None)
582
+ return super().lookup_path(*args, **kwargs)
583
+
584
+ def get_directories(
585
+ self,
586
+ directory: Union[str, "os.PathLike[str]", None] = None,
587
+ packages=None, # type: ignore
588
+ ) -> List[Union[str, "os.PathLike[str]"]]:
589
+ # we only know the .directory at runtime (after startup)
590
+ # which means we cannot pass the directory to the StaticFiles constructor
591
+ return cast(List[Union[str, "os.PathLike[str]"]], [app.directory.parent / "public" for app in appmod.apps.values()])
592
+
593
+
594
+ class StaticAssets(StaticFilesOptionalAuth):
595
+ def lookup_path(self, *args, **kwargs):
596
+ self.all_directories = self.get_directories(None, None)
597
+ return super().lookup_path(*args, **kwargs)
598
+
599
+ def get_directories(
600
+ self,
601
+ directory: Union[str, "os.PathLike[str]", None] = None,
602
+ packages=None, # type: ignore
603
+ ) -> List[Union[str, "os.PathLike[str]"]]:
604
+ # we only know the .directory at runtime (after startup)
605
+ # which means we cannot pass the directory to the StaticFiles constructor
606
+ directories = server.asset_directories()
607
+ return cast(List[Union[str, "os.PathLike[str]"]], directories)
608
+
609
+
610
+ class StaticCdn(StaticFilesOptionalAuth):
611
+ def lookup_path(self, path: str) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
612
+ try:
613
+ full_path = str(get_path(settings.assets.proxy_cache_dir, path))
614
+ except Exception:
615
+ return "", None
616
+ return full_path, os.stat(full_path)
617
+
618
+
619
+ def on_startup():
620
+ appmod.ensure_apps_initialized()
621
+ # TODO: configure and set max number of threads
622
+ # see https://github.com/encode/starlette/issues/1724
623
+ telemetry.server_start()
624
+
625
+
626
+ def on_shutdown():
627
+ # shutdown all kernels
628
+ for context in list(kernel_context.contexts.values()):
629
+ try:
630
+ context.close()
631
+ except: # noqa
632
+ logger.exception("error closing kernel on shutdown")
633
+ telemetry.server_stop()
634
+
635
+
636
+ def readyz(request: Request):
637
+ json, status = server.readyz()
638
+ return JSONResponse(json, status_code=status)
639
+
640
+
641
+ async def resourcez(request: Request):
642
+ _ensure_limiter()
643
+ assert limiter is not None
644
+ data: Dict[str, Any] = {}
645
+ verbose = request.query_params.get("verbose", None) is not None
646
+ data["websockets"] = {
647
+ "attempts": WebsocketDebugInfo.attempts,
648
+ "connecting": WebsocketDebugInfo.connecting,
649
+ "open": WebsocketDebugInfo.open,
650
+ "closed": WebsocketDebugInfo.closed,
651
+ }
652
+ from . import patch
653
+
654
+ data["threads"] = {
655
+ "created": patch.ThreadDebugInfo.created,
656
+ "running": patch.ThreadDebugInfo.running,
657
+ "stopped": patch.ThreadDebugInfo.stopped,
658
+ "active": threading.active_count(),
659
+ }
660
+ contexts = list(kernel_context.contexts.values())
661
+ data["kernels"] = {
662
+ "total": len(contexts),
663
+ "has_connected": len([k for k in contexts if kernel_context.PageStatus.CONNECTED in k.page_status.values()]),
664
+ "has_disconnected": len([k for k in contexts if kernel_context.PageStatus.DISCONNECTED in k.page_status.values()]),
665
+ "has_closed": len([k for k in contexts if kernel_context.PageStatus.CLOSED in k.page_status.values()]),
666
+ "limiter": {
667
+ "total_tokens": limiter.total_tokens,
668
+ "borrowed_tokens": limiter.borrowed_tokens,
669
+ "available_tokens": limiter.available_tokens,
670
+ },
671
+ }
672
+ default_limiter = anyio.to_thread.current_default_thread_limiter()
673
+ data["anyio.to_thread.limiter"] = {
674
+ "total_tokens": default_limiter.total_tokens,
675
+ "borrowed_tokens": default_limiter.borrowed_tokens,
676
+ "available_tokens": default_limiter.available_tokens,
677
+ }
678
+ if verbose:
679
+ try:
680
+ import psutil
681
+
682
+ def expand(named_tuple):
683
+ return {key: getattr(named_tuple, key) for key in named_tuple._fields}
684
+
685
+ data["cpu"] = {}
686
+ try:
687
+ data["cpu"]["percent"] = psutil.cpu_percent()
688
+ except Exception as e:
689
+ data["cpu"]["percent"] = str(e)
690
+ try:
691
+ data["cpu"]["count"] = psutil.cpu_count()
692
+ except Exception as e:
693
+ data["cpu"]["count"] = str(e)
694
+ try:
695
+ data["cpu"]["times"] = expand(psutil.cpu_times())
696
+ data["cpu"]["times"]["per_cpu"] = [expand(x) for x in psutil.cpu_times(percpu=True)]
697
+ except Exception as e:
698
+ data["cpu"]["times"] = str(e)
699
+ try:
700
+ data["cpu"]["times_percent"] = expand(psutil.cpu_times_percent())
701
+ data["cpu"]["times_percent"]["per_cpu"] = [expand(x) for x in psutil.cpu_times_percent(percpu=True)]
702
+ except Exception as e:
703
+ data["cpu"]["times_percent"] = str(e)
704
+ try:
705
+ memory = psutil.virtual_memory()
706
+ except Exception as e:
707
+ data["memory"] = str(e)
708
+ else:
709
+ data["memory"] = {
710
+ "bytes": expand(memory),
711
+ "GB": {key: getattr(memory, key) / 1024**3 for key in memory._fields},
712
+ }
713
+
714
+ except ModuleNotFoundError:
715
+ pass
716
+
717
+ json_string = json.dumps(data, indent=2)
718
+ return Response(content=json_string, media_type="application/json")
719
+
720
+
721
+ middleware = [
722
+ Middleware(GZipMiddleware, minimum_size=1000),
723
+ ]
724
+
725
+ if has_auth_support:
726
+ middleware = [
727
+ *middleware,
728
+ Middleware(
729
+ MutateDetectSessionMiddleware,
730
+ secret_key=settings.session.secret_key, # type: ignore
731
+ session_cookie="solara-session", # type: ignore
732
+ https_only=settings.session.https_only, # type: ignore
733
+ same_site=settings.session.same_site, # type: ignore
734
+ ),
735
+ Middleware(AuthenticationMiddleware, backend=AuthBackend()),
736
+ ]
737
+
738
+ routes_auth = []
739
+ if has_auth_support:
740
+ routes_auth = [
741
+ Route("/_solara/auth/authorize", endpoint=authorize), #
742
+ Route("/_solara/auth/logout", endpoint=logout),
743
+ Route("/_solara/auth/login", endpoint=login),
744
+ ]
745
+ routes = [
746
+ Route("/readyz", endpoint=readyz),
747
+ Route("/resourcez", endpoint=resourcez),
748
+ *routes_auth,
749
+ Route("/jupyter/api/kernels/{id}", endpoint=kernels),
750
+ WebSocketRoute("/jupyter/api/kernels/{kernel_id}/{name}", endpoint=kernel_connection),
751
+ Route("/", endpoint=root),
752
+ Route("/{fullpath}", endpoint=root),
753
+ Route("/_solara/api/close/{kernel_id}", endpoint=close, methods=["POST"]),
754
+ # only enable when the proxy is turned on, otherwise if the directory does not exists we will get an exception
755
+ *([Mount(f"/{cdn_url_path}", app=StaticCdn(directory=settings.assets.proxy_cache_dir))] if solara.settings.assets.proxy else []),
756
+ Mount(f"{prefix}/static/public", app=StaticPublic()),
757
+ Mount(f"{prefix}/static/assets", app=StaticAssets()),
758
+ Mount(f"{prefix}/jupyter/nbextensions", app=StaticNbFiles()),
759
+ Mount(f"{prefix}/static", app=StaticFilesOptionalAuth(directory=server.solara_static)),
760
+ Route("/{fullpath:path}", endpoint=root),
761
+ ]
762
+
763
+ app = Starlette(routes=routes, on_startup=[on_startup], on_shutdown=[on_shutdown], middleware=middleware)
764
+
765
+ # Uncomment the lines below to test solara mouted under a subpath
766
+ # def myroot(request: Request):
767
+ # return JSONResponse({"framework": "solara"})
768
+
769
+ # routes_test_sub = [Route("/", endpoint=myroot), Mount("/foo/", routes=routes)]
770
+ # app = Starlette(routes=routes_test_sub, on_startup=[on_startup], on_shutdown=[on_shutdown], middleware=middleware)