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.
- prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
- prefix/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
- solara/__init__.py +124 -0
- solara/__main__.py +765 -0
- solara/_stores.py +297 -0
- solara/alias.py +6 -0
- solara/autorouting.py +555 -0
- solara/cache.py +305 -0
- solara/checks.html +71 -0
- solara/checks.py +227 -0
- solara/comm.py +28 -0
- solara/components/__init__.py +77 -0
- solara/components/alert.py +155 -0
- solara/components/applayout.py +397 -0
- solara/components/button.py +85 -0
- solara/components/card.py +87 -0
- solara/components/checkbox.py +50 -0
- solara/components/code_highlight_css.py +11 -0
- solara/components/code_highlight_css.vue +63 -0
- solara/components/columns.py +159 -0
- solara/components/component_vue.py +134 -0
- solara/components/cross_filter.py +335 -0
- solara/components/dataframe.py +546 -0
- solara/components/datatable.py +214 -0
- solara/components/datatable.vue +175 -0
- solara/components/details.py +56 -0
- solara/components/download.vue +35 -0
- solara/components/echarts.py +86 -0
- solara/components/echarts.vue +139 -0
- solara/components/figure_altair.py +39 -0
- solara/components/file_browser.py +181 -0
- solara/components/file_download.py +199 -0
- solara/components/file_drop.py +159 -0
- solara/components/file_drop.vue +83 -0
- solara/components/file_list_widget.vue +78 -0
- solara/components/head.py +27 -0
- solara/components/head_tag.py +49 -0
- solara/components/head_tag.vue +60 -0
- solara/components/image.py +173 -0
- solara/components/input.py +456 -0
- solara/components/input_text_area.py +86 -0
- solara/components/link.py +55 -0
- solara/components/markdown.py +441 -0
- solara/components/markdown_editor.py +33 -0
- solara/components/markdown_editor.vue +359 -0
- solara/components/matplotlib.py +74 -0
- solara/components/meta.py +47 -0
- solara/components/misc.py +333 -0
- solara/components/pivot_table.py +258 -0
- solara/components/pivot_table.vue +158 -0
- solara/components/progress.py +47 -0
- solara/components/select.py +182 -0
- solara/components/select.vue +27 -0
- solara/components/slider.py +442 -0
- solara/components/slider_date.vue +56 -0
- solara/components/spinner-solara.vue +105 -0
- solara/components/spinner.py +45 -0
- solara/components/sql_code.py +41 -0
- solara/components/sql_code.vue +125 -0
- solara/components/style.py +105 -0
- solara/components/switch.py +71 -0
- solara/components/tab_navigation.py +37 -0
- solara/components/title.py +90 -0
- solara/components/title.vue +38 -0
- solara/components/togglebuttons.py +200 -0
- solara/components/tooltip.py +61 -0
- solara/core.py +42 -0
- solara/datatypes.py +143 -0
- solara/express.py +241 -0
- solara/hooks/__init__.py +4 -0
- solara/hooks/dataframe.py +99 -0
- solara/hooks/misc.py +263 -0
- solara/hooks/use_reactive.py +151 -0
- solara/hooks/use_thread.py +129 -0
- solara/kitchensink.py +8 -0
- solara/lab/__init__.py +34 -0
- solara/lab/components/__init__.py +7 -0
- solara/lab/components/chat.py +215 -0
- solara/lab/components/confirmation_dialog.py +163 -0
- solara/lab/components/cross_filter.py +7 -0
- solara/lab/components/input_date.py +339 -0
- solara/lab/components/input_time.py +133 -0
- solara/lab/components/menu.py +181 -0
- solara/lab/components/menu.vue +38 -0
- solara/lab/components/tabs.py +274 -0
- solara/lab/components/theming.py +98 -0
- solara/lab/components/theming.vue +72 -0
- solara/lab/hooks/__init__.py +0 -0
- solara/lab/hooks/dataframe.py +2 -0
- solara/lab/toestand.py +3 -0
- solara/lab/utils/__init__.py +2 -0
- solara/lab/utils/cookies.py +5 -0
- solara/lab/utils/dataframe.py +165 -0
- solara/lab/utils/headers.py +5 -0
- solara/layout.py +44 -0
- solara/lifecycle.py +46 -0
- solara/minisettings.py +141 -0
- solara/py.typed +0 -0
- solara/reactive.py +99 -0
- solara/routing.py +268 -0
- solara/scope/__init__.py +88 -0
- solara/scope/types.py +55 -0
- solara/server/__init__.py +0 -0
- solara/server/app.py +527 -0
- solara/server/assets/custom.css +1 -0
- solara/server/assets/custom.js +1 -0
- solara/server/assets/favicon.png +0 -0
- solara/server/assets/favicon.svg +5 -0
- solara/server/assets/style.css +1681 -0
- solara/server/assets/theme-dark.css +437 -0
- solara/server/assets/theme-light.css +420 -0
- solara/server/assets/theme.js +3 -0
- solara/server/cdn_helper.py +91 -0
- solara/server/esm.py +71 -0
- solara/server/fastapi.py +5 -0
- solara/server/flask.py +297 -0
- solara/server/jupyter/__init__.py +2 -0
- solara/server/jupyter/cdn_handler.py +28 -0
- solara/server/jupyter/server_extension.py +40 -0
- solara/server/jupyter/solara.py +91 -0
- solara/server/jupytertools.py +46 -0
- solara/server/kernel.py +388 -0
- solara/server/kernel_context.py +467 -0
- solara/server/patch.py +564 -0
- solara/server/pyinstaller/__init__.py +9 -0
- solara/server/pyinstaller/hook-ipyreact.py +5 -0
- solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
- solara/server/pyinstaller/hook-solara.py +9 -0
- solara/server/qt.py +113 -0
- solara/server/reload.py +251 -0
- solara/server/server.py +484 -0
- solara/server/settings.py +249 -0
- solara/server/shell.py +269 -0
- solara/server/starlette.py +770 -0
- solara/server/static/ansi.js +270 -0
- solara/server/static/highlight-dark.css +82 -0
- solara/server/static/highlight.css +43 -0
- solara/server/static/main-vuetify.js +272 -0
- solara/server/static/main.js +163 -0
- solara/server/static/solara_bootstrap.py +129 -0
- solara/server/static/sun.svg +23 -0
- solara/server/static/webworker.js +42 -0
- solara/server/telemetry.py +212 -0
- solara/server/templates/index.html.j2 +1 -0
- solara/server/templates/loader-plain.css +11 -0
- solara/server/templates/loader-plain.html +20 -0
- solara/server/templates/loader-solara.css +111 -0
- solara/server/templates/loader-solara.html +40 -0
- solara/server/templates/plain.html +82 -0
- solara/server/templates/solara.html.j2 +486 -0
- solara/server/threaded.py +84 -0
- solara/server/utils.py +44 -0
- solara/server/websocket.py +45 -0
- solara/settings.py +86 -0
- solara/tasks.py +893 -0
- solara/template/button.py +16 -0
- solara/template/markdown.py +42 -0
- solara/template/portal/.flake8 +6 -0
- solara/template/portal/.pre-commit-config.yaml +28 -0
- solara/template/portal/LICENSE +21 -0
- solara/template/portal/Procfile +7 -0
- solara/template/portal/mypy.ini +3 -0
- solara/template/portal/pyproject.toml +26 -0
- solara/template/portal/solara_portal/__init__.py +4 -0
- solara/template/portal/solara_portal/components/__init__.py +2 -0
- solara/template/portal/solara_portal/components/article.py +28 -0
- solara/template/portal/solara_portal/components/data.py +28 -0
- solara/template/portal/solara_portal/components/header.py +6 -0
- solara/template/portal/solara_portal/components/layout.py +6 -0
- solara/template/portal/solara_portal/content/articles/equis-in-vidi.md +85 -0
- solara/template/portal/solara_portal/content/articles/substiterat-vati.md +70 -0
- solara/template/portal/solara_portal/data.py +60 -0
- solara/template/portal/solara_portal/pages/__init__.py +67 -0
- solara/template/portal/solara_portal/pages/article/__init__.py +26 -0
- solara/template/portal/solara_portal/pages/tabular.py +29 -0
- solara/template/portal/solara_portal/pages/viz/__init__.py +70 -0
- solara/template/portal/solara_portal/pages/viz/overview.py +14 -0
- solara/test/__init__.py +0 -0
- solara/test/pytest_plugin.py +783 -0
- solara/toestand.py +998 -0
- solara/util.py +348 -0
- solara/validate_hooks.py +258 -0
- solara/website/__init__.py +0 -0
- solara/website/assets/custom.css +444 -0
- solara/website/assets/images/logo-small.png +0 -0
- solara/website/assets/images/logo.svg +17 -0
- solara/website/assets/images/logo_white.svg +50 -0
- solara/website/assets/theme.js +8 -0
- solara/website/components/__init__.py +5 -0
- solara/website/components/algolia.py +6 -0
- solara/website/components/algolia.vue +24 -0
- solara/website/components/algolia_api.vue +202 -0
- solara/website/components/breadcrumbs.py +28 -0
- solara/website/components/contact.py +144 -0
- solara/website/components/docs.py +143 -0
- solara/website/components/header.py +75 -0
- solara/website/components/mailchimp.py +12 -0
- solara/website/components/mailchimp.vue +47 -0
- solara/website/components/markdown.py +99 -0
- solara/website/components/markdown_nav.vue +34 -0
- solara/website/components/notebook.py +171 -0
- solara/website/components/sidebar.py +105 -0
- solara/website/pages/__init__.py +370 -0
- solara/website/pages/about/__init__.py +9 -0
- solara/website/pages/about/about.md +3 -0
- solara/website/pages/apps/__init__.py +16 -0
- solara/website/pages/apps/authorization/__init__.py +119 -0
- solara/website/pages/apps/authorization/admin.py +12 -0
- solara/website/pages/apps/authorization/users.py +12 -0
- solara/website/pages/apps/jupyter-dashboard-1.py +116 -0
- solara/website/pages/apps/layout-demo.py +40 -0
- solara/website/pages/apps/multipage/__init__.py +38 -0
- solara/website/pages/apps/multipage/page1.py +26 -0
- solara/website/pages/apps/multipage/page2.py +34 -0
- solara/website/pages/apps/scatter.py +136 -0
- solara/website/pages/apps/scrolling.py +63 -0
- solara/website/pages/apps/tutorial-streamlit.py +18 -0
- solara/website/pages/careers/__init__.py +27 -0
- solara/website/pages/changelog/__init__.py +10 -0
- solara/website/pages/changelog/changelog.md +372 -0
- solara/website/pages/contact/__init__.py +34 -0
- solara/website/pages/doc_use_download.py +85 -0
- solara/website/pages/documentation/__init__.py +90 -0
- solara/website/pages/documentation/advanced/__init__.py +9 -0
- solara/website/pages/documentation/advanced/content/00-overview.md +1 -0
- solara/website/pages/documentation/advanced/content/10-howto/00-overview.md +6 -0
- solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md +196 -0
- solara/website/pages/documentation/advanced/content/10-howto/20-layout.md +125 -0
- solara/website/pages/documentation/advanced/content/10-howto/30-testing.md +417 -0
- solara/website/pages/documentation/advanced/content/10-howto/31-debugging.md +69 -0
- solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +50 -0
- solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md +124 -0
- solara/website/pages/documentation/advanced/content/15-reference/00-overview.md +3 -0
- solara/website/pages/documentation/advanced/content/15-reference/40-static_files.md +31 -0
- solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +72 -0
- solara/website/pages/documentation/advanced/content/15-reference/60-static-site-generation.md +59 -0
- solara/website/pages/documentation/advanced/content/15-reference/70-search.md +34 -0
- solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md +34 -0
- solara/website/pages/documentation/advanced/content/15-reference/90-notebook-support.md +7 -0
- solara/website/pages/documentation/advanced/content/15-reference/95-caching.md +148 -0
- solara/website/pages/documentation/advanced/content/20-understanding/00-introduction.md +10 -0
- solara/website/pages/documentation/advanced/content/20-understanding/05-ipywidgets.md +35 -0
- solara/website/pages/documentation/advanced/content/20-understanding/06-ipyvuetify.md +42 -0
- solara/website/pages/documentation/advanced/content/20-understanding/10-reacton.md +28 -0
- solara/website/pages/documentation/advanced/content/20-understanding/12-reacton-basics.md +108 -0
- solara/website/pages/documentation/advanced/content/20-understanding/15-anatomy.md +23 -0
- solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md +192 -0
- solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md +166 -0
- solara/website/pages/documentation/advanced/content/20-understanding/20-solara.md +18 -0
- solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +256 -0
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +108 -0
- solara/website/pages/documentation/advanced/content/20-understanding/60-voila.md +12 -0
- solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +7 -0
- solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +187 -0
- solara/website/pages/documentation/advanced/content/40-development/00-overview.md +0 -0
- solara/website/pages/documentation/advanced/content/40-development/01-contribute.md +45 -0
- solara/website/pages/documentation/advanced/content/40-development/10-setup.md +76 -0
- solara/website/pages/documentation/api/__init__.py +19 -0
- solara/website/pages/documentation/api/cross_filter/__init__.py +9 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +22 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +20 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +20 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +20 -0
- solara/website/pages/documentation/api/hooks/__init__.py +9 -0
- solara/website/pages/documentation/api/hooks/use_cross_filter.py +23 -0
- solara/website/pages/documentation/api/hooks/use_dark_effective.py +12 -0
- solara/website/pages/documentation/api/hooks/use_effect.md +43 -0
- solara/website/pages/documentation/api/hooks/use_effect.py +9 -0
- solara/website/pages/documentation/api/hooks/use_exception.py +31 -0
- solara/website/pages/documentation/api/hooks/use_memo.md +16 -0
- solara/website/pages/documentation/api/hooks/use_memo.py +9 -0
- solara/website/pages/documentation/api/hooks/use_previous.py +30 -0
- solara/website/pages/documentation/api/hooks/use_reactive.py +16 -0
- solara/website/pages/documentation/api/hooks/use_state.py +10 -0
- solara/website/pages/documentation/api/hooks/use_state_or_update.py +66 -0
- solara/website/pages/documentation/api/hooks/use_thread.md +64 -0
- solara/website/pages/documentation/api/hooks/use_thread.py +42 -0
- solara/website/pages/documentation/api/hooks/use_trait_observe.py +12 -0
- solara/website/pages/documentation/api/routing/__init__.py +9 -0
- solara/website/pages/documentation/api/routing/generate_routes.py +10 -0
- solara/website/pages/documentation/api/routing/generate_routes_directory.py +10 -0
- solara/website/pages/documentation/api/routing/resolve_path.py +35 -0
- solara/website/pages/documentation/api/routing/route.py +29 -0
- solara/website/pages/documentation/api/routing/use_route.py +76 -0
- solara/website/pages/documentation/api/routing/use_router.py +16 -0
- solara/website/pages/documentation/api/utilities/__init__.py +9 -0
- solara/website/pages/documentation/api/utilities/component_vue.py +10 -0
- solara/website/pages/documentation/api/utilities/computed.py +16 -0
- solara/website/pages/documentation/api/utilities/display.py +16 -0
- solara/website/pages/documentation/api/utilities/get_kernel_id.py +16 -0
- solara/website/pages/documentation/api/utilities/get_session_id.py +16 -0
- solara/website/pages/documentation/api/utilities/memoize.py +35 -0
- solara/website/pages/documentation/api/utilities/on_kernel_start.py +44 -0
- solara/website/pages/documentation/api/utilities/reactive.py +16 -0
- solara/website/pages/documentation/api/utilities/widget.py +104 -0
- solara/website/pages/documentation/components/__init__.py +12 -0
- solara/website/pages/documentation/components/advanced/__init__.py +9 -0
- solara/website/pages/documentation/components/advanced/link.py +25 -0
- solara/website/pages/documentation/components/advanced/meta.py +17 -0
- solara/website/pages/documentation/components/advanced/style.py +43 -0
- solara/website/pages/documentation/components/common.py +9 -0
- solara/website/pages/documentation/components/data/__init__.py +9 -0
- solara/website/pages/documentation/components/data/dataframe.py +44 -0
- solara/website/pages/documentation/components/data/pivot_table.py +81 -0
- solara/website/pages/documentation/components/enterprise/__init__.py +9 -0
- solara/website/pages/documentation/components/enterprise/avatar.py +24 -0
- solara/website/pages/documentation/components/enterprise/avatar_menu.py +25 -0
- solara/website/pages/documentation/components/input/__init__.py +9 -0
- solara/website/pages/documentation/components/input/button.py +23 -0
- solara/website/pages/documentation/components/input/checkbox.py +10 -0
- solara/website/pages/documentation/components/input/file_browser.py +30 -0
- solara/website/pages/documentation/components/input/file_drop.py +76 -0
- solara/website/pages/documentation/components/input/input.py +43 -0
- solara/website/pages/documentation/components/input/select.py +22 -0
- solara/website/pages/documentation/components/input/slider.py +29 -0
- solara/website/pages/documentation/components/input/switch.py +10 -0
- solara/website/pages/documentation/components/input/togglebuttons.py +21 -0
- solara/website/pages/documentation/components/lab/__init__.py +9 -0
- solara/website/pages/documentation/components/lab/chat.py +109 -0
- solara/website/pages/documentation/components/lab/confirmation_dialog.py +55 -0
- solara/website/pages/documentation/components/lab/cookies_headers.py +48 -0
- solara/website/pages/documentation/components/lab/input_date.py +20 -0
- solara/website/pages/documentation/components/lab/input_time.py +15 -0
- solara/website/pages/documentation/components/lab/menu.py +22 -0
- solara/website/pages/documentation/components/lab/tab.py +25 -0
- solara/website/pages/documentation/components/lab/tabs.py +45 -0
- solara/website/pages/documentation/components/lab/task.py +11 -0
- solara/website/pages/documentation/components/lab/theming.py +74 -0
- solara/website/pages/documentation/components/lab/use_task.py +11 -0
- solara/website/pages/documentation/components/layout/__init__.py +9 -0
- solara/website/pages/documentation/components/layout/app_bar.py +16 -0
- solara/website/pages/documentation/components/layout/app_bar_title.py +16 -0
- solara/website/pages/documentation/components/layout/app_layout.py +24 -0
- solara/website/pages/documentation/components/layout/card.py +15 -0
- solara/website/pages/documentation/components/layout/card_actions.py +16 -0
- solara/website/pages/documentation/components/layout/column.py +30 -0
- solara/website/pages/documentation/components/layout/columns.py +27 -0
- solara/website/pages/documentation/components/layout/columns_responsive.py +66 -0
- solara/website/pages/documentation/components/layout/details.py +13 -0
- solara/website/pages/documentation/components/layout/griddraggable.py +62 -0
- solara/website/pages/documentation/components/layout/gridfixed.py +19 -0
- solara/website/pages/documentation/components/layout/hbox.py +18 -0
- solara/website/pages/documentation/components/layout/row.py +30 -0
- solara/website/pages/documentation/components/layout/sidebar.py +24 -0
- solara/website/pages/documentation/components/layout/vbox.py +19 -0
- solara/website/pages/documentation/components/output/__init__.py +9 -0
- solara/website/pages/documentation/components/output/file_download.py +11 -0
- solara/website/pages/documentation/components/output/html.py +19 -0
- solara/website/pages/documentation/components/output/image.py +11 -0
- solara/website/pages/documentation/components/output/markdown.py +57 -0
- solara/website/pages/documentation/components/output/markdown_editor.py +51 -0
- solara/website/pages/documentation/components/output/sql_code.py +83 -0
- solara/website/pages/documentation/components/output/tooltip.py +11 -0
- solara/website/pages/documentation/components/page/__init__.py +9 -0
- solara/website/pages/documentation/components/page/head.py +15 -0
- solara/website/pages/documentation/components/page/title.py +25 -0
- solara/website/pages/documentation/components/status/__init__.py +9 -0
- solara/website/pages/documentation/components/status/error.py +39 -0
- solara/website/pages/documentation/components/status/info.py +39 -0
- solara/website/pages/documentation/components/status/progress.py +10 -0
- solara/website/pages/documentation/components/status/spinner.py +11 -0
- solara/website/pages/documentation/components/status/success.py +40 -0
- solara/website/pages/documentation/components/status/warning.py +47 -0
- solara/website/pages/documentation/components/viz/__init__.py +9 -0
- solara/website/pages/documentation/components/viz/altair.py +42 -0
- solara/website/pages/documentation/components/viz/echarts.py +77 -0
- solara/website/pages/documentation/components/viz/matplotlib.py +30 -0
- solara/website/pages/documentation/components/viz/plotly.py +63 -0
- solara/website/pages/documentation/components/viz/plotly_express.py +41 -0
- solara/website/pages/documentation/examples/__init__.py +54 -0
- solara/website/pages/documentation/examples/ai/__init__.py +11 -0
- solara/website/pages/documentation/examples/ai/chatbot.py +113 -0
- solara/website/pages/documentation/examples/ai/tokenizer.py +107 -0
- solara/website/pages/documentation/examples/basics/__init__.py +10 -0
- solara/website/pages/documentation/examples/basics/sine.py +28 -0
- solara/website/pages/documentation/examples/fullscreen/__init__.py +10 -0
- solara/website/pages/documentation/examples/fullscreen/authorization.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/layout_demo.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/multipage.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/scatter.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/scrolling.py +3 -0
- solara/website/pages/documentation/examples/fullscreen/tutorial_streamlit.py +3 -0
- solara/website/pages/documentation/examples/general/__init__.py +10 -0
- solara/website/pages/documentation/examples/general/custom_storage.py +70 -0
- solara/website/pages/documentation/examples/general/deploy_model.py +115 -0
- solara/website/pages/documentation/examples/general/live_update.py +32 -0
- solara/website/pages/documentation/examples/general/login_oauth.py +81 -0
- solara/website/pages/documentation/examples/general/mycard.vue +58 -0
- solara/website/pages/documentation/examples/general/pokemon_search.py +51 -0
- solara/website/pages/documentation/examples/general/vue_component.py +50 -0
- solara/website/pages/documentation/examples/ipycanvas.py +49 -0
- solara/website/pages/documentation/examples/libraries/__init__.py +10 -0
- solara/website/pages/documentation/examples/libraries/altair.py +65 -0
- solara/website/pages/documentation/examples/libraries/bqplot.py +39 -0
- solara/website/pages/documentation/examples/libraries/ipyleaflet.py +33 -0
- solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +66 -0
- solara/website/pages/documentation/examples/utilities/__init__.py +10 -0
- solara/website/pages/documentation/examples/utilities/calculator.py +157 -0
- solara/website/pages/documentation/examples/utilities/countdown_timer.py +62 -0
- solara/website/pages/documentation/examples/utilities/todo.py +196 -0
- solara/website/pages/documentation/examples/visualization/__init__.py +6 -0
- solara/website/pages/documentation/examples/visualization/annotator.py +67 -0
- solara/website/pages/documentation/examples/visualization/linked_views.py +81 -0
- solara/website/pages/documentation/examples/visualization/plotly.py +44 -0
- solara/website/pages/documentation/faq/__init__.py +12 -0
- solara/website/pages/documentation/faq/content/99-faq.md +112 -0
- solara/website/pages/documentation/getting_started/__init__.py +9 -0
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +107 -0
- solara/website/pages/documentation/getting_started/content/01-introduction.md +125 -0
- solara/website/pages/documentation/getting_started/content/02-installing.md +134 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/00-overview.md +14 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/10_data_science.py +13 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/20-web-app.md +89 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md +124 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md +146 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md +144 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/60-jupyter-dashboard-part1.py +65 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/SF_crime_sample.csv.gz +0 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/_data_science.ipynb +445 -0
- solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +1021 -0
- solara/website/pages/documentation/getting_started/content/05-fundamentals/00-overview.md +11 -0
- solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +228 -0
- solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +278 -0
- solara/website/pages/documentation/getting_started/content/07-deploying/00-overview.md +7 -0
- solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +305 -0
- solara/website/pages/documentation/getting_started/content/07-deploying/20-cloud-hosted.md +80 -0
- solara/website/pages/documentation/getting_started/content/80-what-is-lab.md +7 -0
- solara/website/pages/documentation/getting_started/content/90-troubleshoot.md +26 -0
- solara/website/pages/docutils.py +38 -0
- solara/website/pages/home.vue +1199 -0
- solara/website/pages/our_team/__init__.py +83 -0
- solara/website/pages/pricing/__init__.py +31 -0
- solara/website/pages/roadmap/__init__.py +11 -0
- solara/website/pages/roadmap/roadmap.md +47 -0
- solara/website/pages/scale_ipywidgets.py +45 -0
- solara/website/pages/showcase/__init__.py +105 -0
- solara/website/pages/showcase/domino_code_assist.py +60 -0
- solara/website/pages/showcase/planeto_tessa.py +19 -0
- solara/website/pages/showcase/solara_dev.py +54 -0
- solara/website/pages/showcase/solarathon_2023_team_2.py +22 -0
- solara/website/pages/showcase/solarathon_2023_team_4.py +22 -0
- solara/website/pages/showcase/solarathon_2023_team_5.py +23 -0
- solara/website/pages/showcase/solarathon_2023_team_6.py +34 -0
- solara/website/pages/showcase/wanderlust.py +27 -0
- solara/website/public/beach.jpeg +0 -0
- solara/website/public/logo.svg +6 -0
- solara/website/public/social/discord.svg +1 -0
- solara/website/public/social/github.svg +1 -0
- solara/website/public/social/twitter.svg +3 -0
- solara/website/public/success.html +25 -0
- solara/website/templates/index.html.j2 +117 -0
- solara/website/utils.py +51 -0
- solara/widgets/__init__.py +1 -0
- solara/widgets/vue/gridlayout.vue +107 -0
- solara/widgets/vue/html.vue +4 -0
- solara/widgets/vue/navigator.vue +134 -0
- solara/widgets/vue/vegalite.vue +130 -0
- solara/widgets/widgets.py +74 -0
- solara_ui-1.45.0.data/data/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
- solara_ui-1.45.0.data/data/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
- solara_ui-1.45.0.dist-info/METADATA +162 -0
- solara_ui-1.45.0.dist-info/RECORD +464 -0
- solara_ui-1.45.0.dist-info/WHEEL +4 -0
- 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)
|