solara-ui 1.31.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +734 -0
- solara/alias.py +6 -0
- solara/autorouting.py +546 -0
- solara/cache.py +303 -0
- solara/checks.html +71 -0
- solara/checks.py +224 -0
- solara/comm.py +28 -0
- solara/components/__init__.py +59 -0
- solara/components/alert.py +155 -0
- solara/components/applayout.py +393 -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 +110 -0
- solara/components/cross_filter.py +335 -0
- solara/components/dataframe.py +546 -0
- solara/components/datatable.py +221 -0
- solara/components/datatable.vue +175 -0
- solara/components/details.py +21 -0
- solara/components/download.vue +35 -0
- solara/components/echarts.py +75 -0
- solara/components/echarts.vue +128 -0
- solara/components/figure_altair.py +39 -0
- solara/components/file_browser.py +182 -0
- solara/components/file_download.py +199 -0
- solara/components/file_drop.py +139 -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 +436 -0
- solara/components/link.py +55 -0
- solara/components/markdown.py +378 -0
- solara/components/markdown_editor.py +25 -0
- solara/components/markdown_editor.vue +362 -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 +30 -0
- solara/components/sql_code.py +33 -0
- solara/components/sql_code.vue +128 -0
- solara/components/style.py +105 -0
- solara/components/switch.py +68 -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/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 +129 -0
- solara/hooks/use_thread.py +129 -0
- solara/kitchensink.py +8 -0
- solara/lab/__init__.py +34 -0
- solara/lab/components/__init__.py +6 -0
- solara/lab/components/chat.py +203 -0
- solara/lab/components/confirmation_dialog.py +163 -0
- solara/lab/components/cross_filter.py +7 -0
- solara/lab/components/input_date.py +298 -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 +12 -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 +115 -0
- solara/lab/utils/headers.py +5 -0
- solara/layout.py +44 -0
- solara/lifecycle.py +46 -0
- solara/minisettings.py +133 -0
- solara/py.typed +0 -0
- solara/reactive.py +93 -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 +491 -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 +1665 -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 +77 -0
- solara/server/esm.py +69 -0
- solara/server/fastapi.py +5 -0
- solara/server/flask.py +286 -0
- solara/server/jupyter/__init__.py +2 -0
- solara/server/jupyter/cdn_handler.py +28 -0
- solara/server/jupyter/server_extension.py +29 -0
- solara/server/jupytertools.py +46 -0
- solara/server/kernel.py +338 -0
- solara/server/kernel_context.py +357 -0
- solara/server/patch.py +552 -0
- solara/server/reload.py +242 -0
- solara/server/server.py +456 -0
- solara/server/settings.py +215 -0
- solara/server/shell.py +251 -0
- solara/server/starlette.py +601 -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 +260 -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 +446 -0
- solara/server/threaded.py +75 -0
- solara/server/utils.py +30 -0
- solara/server/websocket.py +45 -0
- solara/settings.py +56 -0
- solara/tasks.py +837 -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 +697 -0
- solara/toestand.py +772 -0
- solara/util.py +308 -0
- solara/website/__init__.py +0 -0
- solara/website/assets/custom.css +468 -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.vue +24 -0
- solara/website/components/algolia_api.vue +187 -0
- solara/website/components/docs.py +118 -0
- solara/website/components/header.py +72 -0
- solara/website/components/hero.py +15 -0
- solara/website/components/mailchimp.py +12 -0
- solara/website/components/mailchimp.vue +47 -0
- solara/website/components/markdown.py +30 -0
- solara/website/components/notebook.py +171 -0
- solara/website/pages/__init__.py +575 -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/changelog/__init__.py +8 -0
- solara/website/pages/changelog/changelog.md +204 -0
- solara/website/pages/contact/__init__.py +8 -0
- solara/website/pages/contact/contact.md +17 -0
- solara/website/pages/doc_use_download.py +85 -0
- solara/website/pages/documentation/__init__.py +184 -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 +162 -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 +49 -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 +36 -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 +7 -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 +240 -0
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +97 -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 +1 -0
- solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +171 -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 +23 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +22 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +22 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +22 -0
- solara/website/pages/documentation/api/hooks/__init__.py +9 -0
- solara/website/pages/documentation/api/hooks/use_cross_filter.py +25 -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 +33 -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 +33 -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 +69 -0
- solara/website/pages/documentation/api/hooks/use_thread.md +58 -0
- solara/website/pages/documentation/api/hooks/use_thread.py +44 -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 +31 -0
- solara/website/pages/documentation/api/routing/use_route.py +80 -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 +27 -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 +27 -0
- solara/website/pages/documentation/components/advanced/meta.py +20 -0
- solara/website/pages/documentation/components/advanced/style.py +45 -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 +32 -0
- solara/website/pages/documentation/components/input/file_drop.py +76 -0
- solara/website/pages/documentation/components/input/input.py +19 -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/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 +72 -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 +68 -0
- solara/website/pages/documentation/components/layout/griddraggable.py +62 -0
- solara/website/pages/documentation/components/layout/gridfixed.py +21 -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 +21 -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 +85 -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 +18 -0
- solara/website/pages/documentation/components/page/title.py +27 -0
- solara/website/pages/documentation/components/status/__init__.py +9 -0
- solara/website/pages/documentation/components/status/error.py +40 -0
- solara/website/pages/documentation/components/status/info.py +40 -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 +75 -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 +52 -0
- solara/website/pages/documentation/examples/ai/__init__.py +11 -0
- solara/website/pages/documentation/examples/ai/chatbot.py +95 -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 +38 -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 +64 -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 +64 -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 +69 -0
- solara/website/pages/documentation/examples/visualization/linked_views.py +84 -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 +76 -0
- solara/website/pages/documentation/getting_started/__init__.py +9 -0
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +89 -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 +64 -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 +1000 -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 +223 -0
- solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +88 -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 +273 -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/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 +110 -0
- solara/widgets/vue/html.vue +4 -0
- solara/widgets/vue/navigator.vue +104 -0
- solara/widgets/vue/vegalite.vue +115 -0
- solara/widgets/widgets.py +66 -0
- solara_ui-1.31.0.data/data/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
- solara_ui-1.31.0.data/data/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
- solara_ui-1.31.0.dist-info/METADATA +158 -0
- solara_ui-1.31.0.dist-info/RECORD +439 -0
- solara_ui-1.31.0.dist-info/WHEEL +5 -0
- solara_ui-1.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import typing
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set, Union, cast
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
import anyio
|
|
13
|
+
import starlette.websockets
|
|
14
|
+
import uvicorn.server
|
|
15
|
+
import websockets.legacy.http
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import solara_enterprise
|
|
19
|
+
|
|
20
|
+
del solara_enterprise
|
|
21
|
+
has_solara_enterprise = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
has_solara_enterprise = False
|
|
24
|
+
if has_solara_enterprise and sys.version_info[:2] > (3, 6):
|
|
25
|
+
has_auth_support = True
|
|
26
|
+
from solara_enterprise.auth.middleware import MutateDetectSessionMiddleware
|
|
27
|
+
from solara_enterprise.auth.starlette import (
|
|
28
|
+
AuthBackend,
|
|
29
|
+
authorize,
|
|
30
|
+
get_user,
|
|
31
|
+
login,
|
|
32
|
+
logout,
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
has_auth_support = False
|
|
36
|
+
|
|
37
|
+
from starlette.applications import Starlette
|
|
38
|
+
from starlette.exceptions import HTTPException
|
|
39
|
+
from starlette.middleware import Middleware
|
|
40
|
+
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
41
|
+
from starlette.middleware.gzip import GZipMiddleware
|
|
42
|
+
from starlette.requests import HTTPConnection, Request
|
|
43
|
+
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
|
|
44
|
+
from starlette.routing import Mount, Route, WebSocketRoute
|
|
45
|
+
from starlette.staticfiles import StaticFiles
|
|
46
|
+
from starlette.types import Receive, Scope, Send
|
|
47
|
+
|
|
48
|
+
import solara
|
|
49
|
+
import solara.settings
|
|
50
|
+
from solara.server.threaded import ServerBase
|
|
51
|
+
|
|
52
|
+
from . import app as appmod
|
|
53
|
+
from . import kernel_context, server, settings, telemetry, websocket
|
|
54
|
+
from .cdn_helper import cdn_url_path, get_path
|
|
55
|
+
|
|
56
|
+
os.environ["SERVER_SOFTWARE"] = "solara/" + str(solara.__version__)
|
|
57
|
+
limiter: Optional[anyio.CapacityLimiter] = None
|
|
58
|
+
lock = threading.Lock()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ensure_limiter():
|
|
62
|
+
# in older anyios (<4) the limiter can only be created in an async context
|
|
63
|
+
# so we call this in a starlette handler
|
|
64
|
+
global limiter
|
|
65
|
+
if limiter is None:
|
|
66
|
+
with lock:
|
|
67
|
+
if limiter is None:
|
|
68
|
+
limiter = anyio.CapacityLimiter(settings.kernel.max_count if settings.kernel.max_count is not None else math.inf)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
logger = logging.getLogger("solara.server.fastapi")
|
|
72
|
+
# if we add these to the router, the server_test does not run (404's)
|
|
73
|
+
prefix = ""
|
|
74
|
+
|
|
75
|
+
# The limit for starlette's http traffic should come from h11's DEFAULT_MAX_INCOMPLETE_EVENT_SIZE=16kb
|
|
76
|
+
# In practice, testing with 132kb cookies (server_test.py:test_large_cookie) seems to work fine.
|
|
77
|
+
# For the websocket, the limit is set to 4kb till 10.4, see
|
|
78
|
+
# * https://github.com/aaugustin/websockets/blob/10.4/src/websockets/legacy/http.py#L14
|
|
79
|
+
# Later releases should set this to 8kb. See
|
|
80
|
+
# * https://github.com/aaugustin/websockets/commit/8ce4739b7efed3ac78b287da7fb5e537f78e72aa
|
|
81
|
+
# * https://github.com/aaugustin/websockets/issues/743
|
|
82
|
+
# Since starlette seems to accept really large values for http, lets do the same for websockets
|
|
83
|
+
# An arbitrarily large value we settled on for now is 32kb
|
|
84
|
+
# If we don't do this, users with many cookies will fail to get a websocket connection.
|
|
85
|
+
websockets.legacy.http.MAX_LINE = 1024 * 32
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class WebsocketDebugInfo:
|
|
89
|
+
lock = threading.Lock()
|
|
90
|
+
attempts = 0
|
|
91
|
+
connecting = 0
|
|
92
|
+
open = 0
|
|
93
|
+
closed = 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class WebsocketWrapper(websocket.WebsocketWrapper):
|
|
97
|
+
ws: starlette.websockets.WebSocket
|
|
98
|
+
|
|
99
|
+
def __init__(self, ws: starlette.websockets.WebSocket, portal: Optional[anyio.from_thread.BlockingPortal]) -> None:
|
|
100
|
+
self.ws = ws
|
|
101
|
+
self.portal = portal
|
|
102
|
+
self.to_send: List[Union[str, bytes]] = []
|
|
103
|
+
# following https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task
|
|
104
|
+
# we store a strong reference
|
|
105
|
+
self.tasks: Set[asyncio.Task] = set()
|
|
106
|
+
self.event_loop = asyncio.get_event_loop()
|
|
107
|
+
if settings.main.experimental_performance:
|
|
108
|
+
self.task = asyncio.ensure_future(self.process_messages_task())
|
|
109
|
+
|
|
110
|
+
async def process_messages_task(self):
|
|
111
|
+
while True:
|
|
112
|
+
await asyncio.sleep(0.05)
|
|
113
|
+
while len(self.to_send) > 0:
|
|
114
|
+
first = self.to_send.pop(0)
|
|
115
|
+
if isinstance(first, bytes):
|
|
116
|
+
await self.ws.send_bytes(first)
|
|
117
|
+
else:
|
|
118
|
+
await self.ws.send_text(first)
|
|
119
|
+
|
|
120
|
+
def close(self):
|
|
121
|
+
if self.portal is None:
|
|
122
|
+
asyncio.ensure_future(self.ws.close())
|
|
123
|
+
else:
|
|
124
|
+
self.portal.call(self.ws.close) # type: ignore
|
|
125
|
+
|
|
126
|
+
def send_text(self, data: str) -> None:
|
|
127
|
+
if self.portal is None:
|
|
128
|
+
task = self.event_loop.create_task(self.ws.send_text(data))
|
|
129
|
+
self.tasks.add(task)
|
|
130
|
+
task.add_done_callback(self.tasks.discard)
|
|
131
|
+
else:
|
|
132
|
+
if settings.main.experimental_performance:
|
|
133
|
+
self.to_send.append(data)
|
|
134
|
+
else:
|
|
135
|
+
self.portal.call(self.ws.send_bytes, data) # type: ignore
|
|
136
|
+
|
|
137
|
+
def send_bytes(self, data: bytes) -> None:
|
|
138
|
+
if self.portal is None:
|
|
139
|
+
task = self.event_loop.create_task(self.ws.send_bytes(data))
|
|
140
|
+
self.tasks.add(task)
|
|
141
|
+
task.add_done_callback(self.tasks.discard)
|
|
142
|
+
else:
|
|
143
|
+
if settings.main.experimental_performance:
|
|
144
|
+
self.to_send.append(data)
|
|
145
|
+
else:
|
|
146
|
+
self.portal.call(self.ws.send_bytes, data) # type: ignore
|
|
147
|
+
|
|
148
|
+
async def receive(self):
|
|
149
|
+
if self.portal is None:
|
|
150
|
+
message = await asyncio.ensure_future(self.ws.receive())
|
|
151
|
+
else:
|
|
152
|
+
if hasattr(self.portal, "start_task_soon"):
|
|
153
|
+
# version 3+
|
|
154
|
+
fut = self.portal.start_task_soon(self.ws.receive) # type: ignore
|
|
155
|
+
else:
|
|
156
|
+
fut = self.portal.spawn_task(self.ws.receive) # type: ignore
|
|
157
|
+
|
|
158
|
+
message = await asyncio.wrap_future(fut)
|
|
159
|
+
if "text" in message:
|
|
160
|
+
return message["text"]
|
|
161
|
+
elif "bytes" in message:
|
|
162
|
+
return message["bytes"]
|
|
163
|
+
elif message.get("type") == "websocket.disconnect":
|
|
164
|
+
raise websocket.WebSocketDisconnect()
|
|
165
|
+
else:
|
|
166
|
+
raise RuntimeError(f"Unknown message type {message}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ServerStarlette(ServerBase):
|
|
170
|
+
server: uvicorn.server.Server
|
|
171
|
+
name = "starlette"
|
|
172
|
+
|
|
173
|
+
def __init__(self, port: int, host: str = "localhost", starlette_app=None, **kwargs):
|
|
174
|
+
super().__init__(port, host, **kwargs)
|
|
175
|
+
self.app = starlette_app or app
|
|
176
|
+
|
|
177
|
+
def has_started(self):
|
|
178
|
+
return self.server.started
|
|
179
|
+
|
|
180
|
+
def signal_stop(self):
|
|
181
|
+
self.server.should_exit = True
|
|
182
|
+
# this cause uvicorn to not wait for background tasks, e.g.:
|
|
183
|
+
# <Task pending name='Task-55'
|
|
184
|
+
# coro=<WebSocketProtocol.run_asgi() running at
|
|
185
|
+
# /.../uvicorn/protocols/websockets/websockets_impl.py:184>
|
|
186
|
+
# wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x16896aa00>()]>
|
|
187
|
+
# cb=[WebSocketProtocol.on_task_complete()]>
|
|
188
|
+
self.server.force_exit = True
|
|
189
|
+
self.server.lifespan.should_exit = True
|
|
190
|
+
|
|
191
|
+
def serve(self):
|
|
192
|
+
from uvicorn.config import Config
|
|
193
|
+
from uvicorn.server import Server
|
|
194
|
+
|
|
195
|
+
if sys.version_info[:2] < (3, 7):
|
|
196
|
+
# make python 3.6 work
|
|
197
|
+
import asyncio
|
|
198
|
+
|
|
199
|
+
loop = asyncio.new_event_loop()
|
|
200
|
+
asyncio.set_event_loop(loop)
|
|
201
|
+
|
|
202
|
+
# uvloop will trigger a: RuntimeError: There is no current event loop in thread 'fastapi-thread'
|
|
203
|
+
config = Config(self.app, host=self.host, port=self.port, **self.kwargs, access_log=False, loop="asyncio")
|
|
204
|
+
self.server = Server(config=config)
|
|
205
|
+
self.started.set()
|
|
206
|
+
self.server.run()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def kernels(id):
|
|
210
|
+
return JSONResponse({"name": "lala", "id": "dsa"})
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def kernel_connection(ws: starlette.websockets.WebSocket):
|
|
214
|
+
_ensure_limiter()
|
|
215
|
+
try:
|
|
216
|
+
with WebsocketDebugInfo.lock:
|
|
217
|
+
WebsocketDebugInfo.attempts += 1
|
|
218
|
+
WebsocketDebugInfo.connecting += 1
|
|
219
|
+
await _kernel_connection(ws)
|
|
220
|
+
finally:
|
|
221
|
+
with WebsocketDebugInfo.lock:
|
|
222
|
+
WebsocketDebugInfo.closed += 1
|
|
223
|
+
WebsocketDebugInfo.open -= 1
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def _kernel_connection(ws: starlette.websockets.WebSocket):
|
|
227
|
+
session_id = ws.cookies.get(server.COOKIE_KEY_SESSION_ID)
|
|
228
|
+
|
|
229
|
+
if settings.oauth.private and not has_auth_support:
|
|
230
|
+
raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
|
|
231
|
+
if has_auth_support and "session" in ws.scope:
|
|
232
|
+
user = get_user(ws)
|
|
233
|
+
if user is None and settings.oauth.private:
|
|
234
|
+
await ws.accept()
|
|
235
|
+
logger.error("app is private, requires login")
|
|
236
|
+
await ws.close(code=1008, reason="app is private, requires login")
|
|
237
|
+
return
|
|
238
|
+
else:
|
|
239
|
+
user = None
|
|
240
|
+
|
|
241
|
+
if not session_id:
|
|
242
|
+
logger.warning("no session cookie")
|
|
243
|
+
session_id = "session-id-cookie-unavailable:" + str(uuid4())
|
|
244
|
+
# we use the jupyter session_id query parameter as the key/id
|
|
245
|
+
# for a page scope.
|
|
246
|
+
page_id = ws.query_params["session_id"]
|
|
247
|
+
if not page_id:
|
|
248
|
+
logger.error("no page_id")
|
|
249
|
+
kernel_id = ws.path_params["kernel_id"]
|
|
250
|
+
if not kernel_id:
|
|
251
|
+
logger.error("no kernel_id")
|
|
252
|
+
await ws.close()
|
|
253
|
+
return
|
|
254
|
+
logger.info("Solara kernel requested for session_id=%s kernel_id=%s", session_id, kernel_id)
|
|
255
|
+
await ws.accept()
|
|
256
|
+
with WebsocketDebugInfo.lock:
|
|
257
|
+
WebsocketDebugInfo.connecting -= 1
|
|
258
|
+
WebsocketDebugInfo.open += 1
|
|
259
|
+
|
|
260
|
+
async def run(ws_wrapper: WebsocketWrapper):
|
|
261
|
+
if kernel_context.async_context_id is not None:
|
|
262
|
+
kernel_context.async_context_id.set(uuid4().hex)
|
|
263
|
+
assert session_id is not None
|
|
264
|
+
assert kernel_id is not None
|
|
265
|
+
telemetry.connection_open(session_id)
|
|
266
|
+
headers_dict: Dict[str, List[str]] = {}
|
|
267
|
+
for k, v in ws.headers.items():
|
|
268
|
+
if k not in headers_dict.keys():
|
|
269
|
+
headers_dict[k] = [v]
|
|
270
|
+
else:
|
|
271
|
+
headers_dict[k].append(v)
|
|
272
|
+
await server.app_loop(ws_wrapper, ws.cookies, headers_dict, session_id, kernel_id, page_id, user)
|
|
273
|
+
|
|
274
|
+
def websocket_thread_runner(ws_wrapper: WebsocketWrapper, portal: anyio.from_thread.BlockingPortal):
|
|
275
|
+
async def run_wrapper():
|
|
276
|
+
try:
|
|
277
|
+
await run(ws_wrapper)
|
|
278
|
+
except: # noqa
|
|
279
|
+
if portal is not None:
|
|
280
|
+
await portal.stop(cancel_remaining=True)
|
|
281
|
+
raise
|
|
282
|
+
finally:
|
|
283
|
+
telemetry.connection_close(session_id)
|
|
284
|
+
|
|
285
|
+
# sometimes throws: RuntimeError: Already running asyncio in this thread
|
|
286
|
+
anyio.run(run_wrapper) # type: ignore
|
|
287
|
+
|
|
288
|
+
# this portal allows us to sync call the websocket calls from this current event loop we are in
|
|
289
|
+
# each websocket however, is handled from a separate thread
|
|
290
|
+
try:
|
|
291
|
+
if settings.kernel.threaded:
|
|
292
|
+
async with anyio.from_thread.BlockingPortal() as portal:
|
|
293
|
+
ws_wrapper = WebsocketWrapper(ws, portal)
|
|
294
|
+
thread_return = anyio.to_thread.run_sync(websocket_thread_runner, ws_wrapper, portal, limiter=limiter) # type: ignore
|
|
295
|
+
await thread_return
|
|
296
|
+
else:
|
|
297
|
+
ws_wrapper = WebsocketWrapper(ws, None)
|
|
298
|
+
await run(ws_wrapper)
|
|
299
|
+
finally:
|
|
300
|
+
if settings.main.experimental_performance:
|
|
301
|
+
try:
|
|
302
|
+
ws_wrapper.task.cancel()
|
|
303
|
+
except: # noqa
|
|
304
|
+
logger.exception("error cancelling websocket task")
|
|
305
|
+
try:
|
|
306
|
+
await ws.close()
|
|
307
|
+
except: # noqa
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def close(request: Request):
|
|
312
|
+
kernel_id = request.path_params["kernel_id"]
|
|
313
|
+
page_id = request.query_params["session_id"]
|
|
314
|
+
if kernel_id in kernel_context.contexts:
|
|
315
|
+
context = kernel_context.contexts[kernel_id]
|
|
316
|
+
context.page_close(page_id)
|
|
317
|
+
response = HTMLResponse(content="", status_code=200)
|
|
318
|
+
return response
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async def root(request: Request, fullpath: str = ""):
|
|
322
|
+
if settings.oauth.private and not has_auth_support:
|
|
323
|
+
raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
|
|
324
|
+
root_path = settings.main.root_path or ""
|
|
325
|
+
if not settings.main.base_url:
|
|
326
|
+
settings.main.base_url = str(request.base_url)
|
|
327
|
+
# if not explicltly set,
|
|
328
|
+
if settings.main.root_path is None:
|
|
329
|
+
# use the default root path from the app, which seems to also include the path
|
|
330
|
+
# if we are mounted under a path
|
|
331
|
+
scope = request.scope
|
|
332
|
+
root_path = scope.get("route_root_path", scope.get("root_path", ""))
|
|
333
|
+
logger.debug("root_path: %s", root_path)
|
|
334
|
+
# or use the script-name header, for instance when running under a reverse proxy
|
|
335
|
+
script_name = request.headers.get("script-name")
|
|
336
|
+
if script_name:
|
|
337
|
+
logger.debug("override root_path using script-name header from %s to %s", root_path, script_name)
|
|
338
|
+
root_path = script_name
|
|
339
|
+
script_name = request.headers.get("x-script-name")
|
|
340
|
+
if script_name:
|
|
341
|
+
logger.debug("override root_path using x-script-name header from %s to %s", root_path, script_name)
|
|
342
|
+
root_path = script_name
|
|
343
|
+
settings.main.root_path = root_path
|
|
344
|
+
|
|
345
|
+
request_path = request.url.path
|
|
346
|
+
if request_path.startswith(root_path):
|
|
347
|
+
request_path = request_path[len(root_path) :]
|
|
348
|
+
if request_path in server._redirects.keys():
|
|
349
|
+
return RedirectResponse(server._redirects[request_path])
|
|
350
|
+
|
|
351
|
+
content = server.read_root(request_path, root_path)
|
|
352
|
+
if content is None:
|
|
353
|
+
if settings.oauth.private and not request.user.is_authenticated:
|
|
354
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
355
|
+
return HTMLResponse(content="Page not found by Solara router", status_code=404)
|
|
356
|
+
|
|
357
|
+
if settings.oauth.private and not request.user.is_authenticated:
|
|
358
|
+
from solara_enterprise.auth.starlette import login
|
|
359
|
+
|
|
360
|
+
return await login(request)
|
|
361
|
+
|
|
362
|
+
response = HTMLResponse(content=content)
|
|
363
|
+
session_id = request.cookies.get(server.COOKIE_KEY_SESSION_ID) or str(uuid4())
|
|
364
|
+
samesite = "lax"
|
|
365
|
+
secure = False
|
|
366
|
+
# we want samesite, so we can set a cookie when embedded in an iframe, such as on huggingface
|
|
367
|
+
# however, samesite=none requires Secure https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
|
368
|
+
# when hosted on the localhost domain we can always set the Secure flag
|
|
369
|
+
# to allow samesite https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
|
|
370
|
+
if request.headers.get("x-forwarded-proto", "http") == "https" or request.base_url.hostname == "localhost":
|
|
371
|
+
samesite = "none"
|
|
372
|
+
secure = True
|
|
373
|
+
response.set_cookie(
|
|
374
|
+
server.COOKIE_KEY_SESSION_ID,
|
|
375
|
+
value=session_id,
|
|
376
|
+
expires="Fri, 01 Jan 2038 00:00:00 GMT",
|
|
377
|
+
samesite=samesite, # type: ignore
|
|
378
|
+
secure=secure, # type: ignore
|
|
379
|
+
) # type: ignore
|
|
380
|
+
return response
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class StaticFilesOptionalAuth(StaticFiles):
|
|
384
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
385
|
+
conn = HTTPConnection(scope)
|
|
386
|
+
if settings.oauth.private and not has_auth_support:
|
|
387
|
+
raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
|
|
388
|
+
if has_auth_support and settings.oauth.private and not conn.user.is_authenticated:
|
|
389
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
390
|
+
await super().__call__(scope, receive, send)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class StaticNbFiles(StaticFilesOptionalAuth):
|
|
394
|
+
def get_directories(
|
|
395
|
+
self,
|
|
396
|
+
directory: Union[str, "os.PathLike[str]", None] = None,
|
|
397
|
+
packages=None, # type: ignore
|
|
398
|
+
) -> List[Union[str, "os.PathLike[str]"]]:
|
|
399
|
+
return cast(List[Union[str, "os.PathLike[str]"]], server.nbextensions_directories)
|
|
400
|
+
|
|
401
|
+
# follow symlinks
|
|
402
|
+
# from https://github.com/encode/starlette/pull/1377/files
|
|
403
|
+
def lookup_path(self, path: str) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
|
|
404
|
+
for directory in self.all_directories:
|
|
405
|
+
original_path = os.path.join(directory, path)
|
|
406
|
+
full_path = os.path.realpath(original_path)
|
|
407
|
+
directory = os.path.realpath(directory)
|
|
408
|
+
try:
|
|
409
|
+
return full_path, os.stat(full_path)
|
|
410
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
411
|
+
continue
|
|
412
|
+
return "", None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class StaticPublic(StaticFilesOptionalAuth):
|
|
416
|
+
def lookup_path(self, *args, **kwargs):
|
|
417
|
+
self.all_directories = self.get_directories(None, None)
|
|
418
|
+
return super().lookup_path(*args, **kwargs)
|
|
419
|
+
|
|
420
|
+
def get_directories(
|
|
421
|
+
self,
|
|
422
|
+
directory: Union[str, "os.PathLike[str]", None] = None,
|
|
423
|
+
packages=None, # type: ignore
|
|
424
|
+
) -> List[Union[str, "os.PathLike[str]"]]:
|
|
425
|
+
# we only know the .directory at runtime (after startup)
|
|
426
|
+
# which means we cannot pass the directory to the StaticFiles constructor
|
|
427
|
+
return cast(List[Union[str, "os.PathLike[str]"]], [app.directory.parent / "public" for app in appmod.apps.values()])
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class StaticAssets(StaticFilesOptionalAuth):
|
|
431
|
+
def lookup_path(self, *args, **kwargs):
|
|
432
|
+
self.all_directories = self.get_directories(None, None)
|
|
433
|
+
return super().lookup_path(*args, **kwargs)
|
|
434
|
+
|
|
435
|
+
def get_directories(
|
|
436
|
+
self,
|
|
437
|
+
directory: Union[str, "os.PathLike[str]", None] = None,
|
|
438
|
+
packages=None, # type: ignore
|
|
439
|
+
) -> List[Union[str, "os.PathLike[str]"]]:
|
|
440
|
+
# we only know the .directory at runtime (after startup)
|
|
441
|
+
# which means we cannot pass the directory to the StaticFiles constructor
|
|
442
|
+
overrides = [app.directory.parent / "assets" for app in appmod.apps.values()]
|
|
443
|
+
default = server.solara_static.parent / "assets"
|
|
444
|
+
return cast(List[Union[str, "os.PathLike[str]"]], [*overrides, default])
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class StaticCdn(StaticFilesOptionalAuth):
|
|
448
|
+
def lookup_path(self, path: str) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
|
|
449
|
+
try:
|
|
450
|
+
full_path = str(get_path(settings.assets.proxy_cache_dir, path))
|
|
451
|
+
except Exception:
|
|
452
|
+
return "", None
|
|
453
|
+
|
|
454
|
+
return full_path, os.stat(full_path)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def on_startup():
|
|
458
|
+
# TODO: configure and set max number of threads
|
|
459
|
+
# see https://github.com/encode/starlette/issues/1724
|
|
460
|
+
telemetry.server_start()
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def on_shutdown():
|
|
464
|
+
telemetry.server_stop()
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def readyz(request: Request):
|
|
468
|
+
json, status = server.readyz()
|
|
469
|
+
return JSONResponse(json, status_code=status)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
async def resourcez(request: Request):
|
|
473
|
+
_ensure_limiter()
|
|
474
|
+
assert limiter is not None
|
|
475
|
+
data: Dict[str, Any] = {}
|
|
476
|
+
verbose = request.query_params.get("verbose", None) is not None
|
|
477
|
+
data["websockets"] = {
|
|
478
|
+
"attempts": WebsocketDebugInfo.attempts,
|
|
479
|
+
"connecting": WebsocketDebugInfo.connecting,
|
|
480
|
+
"open": WebsocketDebugInfo.open,
|
|
481
|
+
"closed": WebsocketDebugInfo.closed,
|
|
482
|
+
}
|
|
483
|
+
from . import patch
|
|
484
|
+
|
|
485
|
+
data["threads"] = {
|
|
486
|
+
"created": patch.ThreadDebugInfo.created,
|
|
487
|
+
"running": patch.ThreadDebugInfo.running,
|
|
488
|
+
"stopped": patch.ThreadDebugInfo.stopped,
|
|
489
|
+
"active": threading.active_count(),
|
|
490
|
+
}
|
|
491
|
+
contexts = list(kernel_context.contexts.values())
|
|
492
|
+
data["kernels"] = {
|
|
493
|
+
"total": len(contexts),
|
|
494
|
+
"has_connected": len([k for k in contexts if kernel_context.PageStatus.CONNECTED in k.page_status.values()]),
|
|
495
|
+
"has_disconnected": len([k for k in contexts if kernel_context.PageStatus.DISCONNECTED in k.page_status.values()]),
|
|
496
|
+
"has_closed": len([k for k in contexts if kernel_context.PageStatus.CLOSED in k.page_status.values()]),
|
|
497
|
+
"limiter": {
|
|
498
|
+
"total_tokens": limiter.total_tokens,
|
|
499
|
+
"borrowed_tokens": limiter.borrowed_tokens,
|
|
500
|
+
"available_tokens": limiter.available_tokens,
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
default_limiter = anyio.to_thread.current_default_thread_limiter()
|
|
504
|
+
data["anyio.to_thread.limiter"] = {
|
|
505
|
+
"total_tokens": default_limiter.total_tokens,
|
|
506
|
+
"borrowed_tokens": default_limiter.borrowed_tokens,
|
|
507
|
+
"available_tokens": default_limiter.available_tokens,
|
|
508
|
+
}
|
|
509
|
+
if verbose:
|
|
510
|
+
try:
|
|
511
|
+
import psutil
|
|
512
|
+
|
|
513
|
+
def expand(named_tuple):
|
|
514
|
+
return {key: getattr(named_tuple, key) for key in named_tuple._fields}
|
|
515
|
+
|
|
516
|
+
data["cpu"] = {}
|
|
517
|
+
try:
|
|
518
|
+
data["cpu"]["percent"] = psutil.cpu_percent()
|
|
519
|
+
except Exception as e:
|
|
520
|
+
data["cpu"]["percent"] = str(e)
|
|
521
|
+
try:
|
|
522
|
+
data["cpu"]["count"] = psutil.cpu_count()
|
|
523
|
+
except Exception as e:
|
|
524
|
+
data["cpu"]["count"] = str(e)
|
|
525
|
+
try:
|
|
526
|
+
data["cpu"]["times"] = expand(psutil.cpu_times())
|
|
527
|
+
data["cpu"]["times"]["per_cpu"] = [expand(x) for x in psutil.cpu_times(percpu=True)]
|
|
528
|
+
except Exception as e:
|
|
529
|
+
data["cpu"]["times"] = str(e)
|
|
530
|
+
try:
|
|
531
|
+
data["cpu"]["times_percent"] = expand(psutil.cpu_times_percent())
|
|
532
|
+
data["cpu"]["times_percent"]["per_cpu"] = [expand(x) for x in psutil.cpu_times_percent(percpu=True)]
|
|
533
|
+
except Exception as e:
|
|
534
|
+
data["cpu"]["times_percent"] = str(e)
|
|
535
|
+
try:
|
|
536
|
+
memory = psutil.virtual_memory()
|
|
537
|
+
except Exception as e:
|
|
538
|
+
data["memory"] = str(e)
|
|
539
|
+
else:
|
|
540
|
+
data["memory"] = {
|
|
541
|
+
"bytes": expand(memory),
|
|
542
|
+
"GB": {key: getattr(memory, key) / 1024**3 for key in memory._fields},
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
except ModuleNotFoundError:
|
|
546
|
+
pass
|
|
547
|
+
|
|
548
|
+
json_string = json.dumps(data, indent=2)
|
|
549
|
+
return Response(content=json_string, media_type="application/json")
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
middleware = [
|
|
553
|
+
Middleware(GZipMiddleware, minimum_size=1000),
|
|
554
|
+
]
|
|
555
|
+
|
|
556
|
+
if has_auth_support:
|
|
557
|
+
middleware = [
|
|
558
|
+
*middleware,
|
|
559
|
+
Middleware(
|
|
560
|
+
MutateDetectSessionMiddleware,
|
|
561
|
+
secret_key=settings.session.secret_key, # type: ignore
|
|
562
|
+
session_cookie="solara-session", # type: ignore
|
|
563
|
+
https_only=settings.session.https_only, # type: ignore
|
|
564
|
+
same_site=settings.session.same_site, # type: ignore
|
|
565
|
+
),
|
|
566
|
+
Middleware(AuthenticationMiddleware, backend=AuthBackend()),
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
routes_auth = []
|
|
570
|
+
if has_auth_support:
|
|
571
|
+
routes_auth = [
|
|
572
|
+
Route("/_solara/auth/authorize", endpoint=authorize), #
|
|
573
|
+
Route("/_solara/auth/logout", endpoint=logout),
|
|
574
|
+
Route("/_solara/auth/login", endpoint=login),
|
|
575
|
+
]
|
|
576
|
+
routes = [
|
|
577
|
+
Route("/readyz", endpoint=readyz),
|
|
578
|
+
Route("/resourcez", endpoint=resourcez),
|
|
579
|
+
*routes_auth,
|
|
580
|
+
Route("/jupyter/api/kernels/{id}", endpoint=kernels),
|
|
581
|
+
WebSocketRoute("/jupyter/api/kernels/{kernel_id}/{name}", endpoint=kernel_connection),
|
|
582
|
+
Route("/", endpoint=root),
|
|
583
|
+
Route("/{fullpath}", endpoint=root),
|
|
584
|
+
Route("/_solara/api/close/{kernel_id}", endpoint=close, methods=["POST"]),
|
|
585
|
+
# only enable when the proxy is turned on, otherwise if the directory does not exists we will get an exception
|
|
586
|
+
*([Mount(f"/{cdn_url_path}", app=StaticCdn(directory=settings.assets.proxy_cache_dir))] if solara.settings.assets.proxy else []),
|
|
587
|
+
Mount(f"{prefix}/static/public", app=StaticPublic()),
|
|
588
|
+
Mount(f"{prefix}/static/assets", app=StaticAssets()),
|
|
589
|
+
Mount(f"{prefix}/static/nbextensions", app=StaticNbFiles()),
|
|
590
|
+
Mount(f"{prefix}/static", app=StaticFilesOptionalAuth(directory=server.solara_static)),
|
|
591
|
+
Route("/{fullpath:path}", endpoint=root),
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
app = Starlette(routes=routes, on_startup=[on_startup], on_shutdown=[on_shutdown], middleware=middleware)
|
|
595
|
+
|
|
596
|
+
# Uncomment the lines below to test solara mouted under a subpath
|
|
597
|
+
# def myroot(request: Request):
|
|
598
|
+
# return JSONResponse({"framework": "solara"})
|
|
599
|
+
|
|
600
|
+
# routes_test_sub = [Route("/", endpoint=myroot), Mount("/foo/", routes=routes)]
|
|
601
|
+
# app = Starlette(routes=routes_test_sub, on_startup=[on_startup], on_shutdown=[on_shutdown], middleware=middleware)
|