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
solara/server/kernel.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import pdb
|
|
4
|
+
import queue
|
|
5
|
+
import struct
|
|
6
|
+
import warnings
|
|
7
|
+
from binascii import b2a_base64
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Set, Union
|
|
10
|
+
|
|
11
|
+
import ipykernel
|
|
12
|
+
import ipykernel.kernelbase
|
|
13
|
+
import jupyter_client.session as session
|
|
14
|
+
from dateutil.tz import tzlocal # type: ignore
|
|
15
|
+
from ipykernel.comm import CommManager
|
|
16
|
+
from zmq.eventloop.zmqstream import ZMQStream
|
|
17
|
+
|
|
18
|
+
import solara
|
|
19
|
+
from solara.server.shell import SolaraInteractiveShell
|
|
20
|
+
|
|
21
|
+
from . import settings, websocket
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("solara.server.kernel")
|
|
24
|
+
ipykernel_major = int(ipykernel.__version__.split(".")[0])
|
|
25
|
+
|
|
26
|
+
jsonmodule = json
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# from jupyter_client/jsonutil.py
|
|
30
|
+
def _ensure_tzinfo(dt: datetime) -> datetime:
|
|
31
|
+
"""Ensure a datetime object has tzinfo
|
|
32
|
+
If no tzinfo is present, add tzlocal
|
|
33
|
+
"""
|
|
34
|
+
if not dt.tzinfo:
|
|
35
|
+
# No more naïve datetime objects!
|
|
36
|
+
warnings.warn(
|
|
37
|
+
"Interpreting naive datetime as local %s. Please add timezone info to timestamps." % dt,
|
|
38
|
+
DeprecationWarning,
|
|
39
|
+
stacklevel=4,
|
|
40
|
+
)
|
|
41
|
+
dt = dt.replace(tzinfo=tzlocal())
|
|
42
|
+
return dt
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def json_default(obj):
|
|
46
|
+
"""default function for packing objects in JSON."""
|
|
47
|
+
if isinstance(obj, datetime):
|
|
48
|
+
obj = _ensure_tzinfo(obj)
|
|
49
|
+
return obj.isoformat().replace("+00:00", "Z")
|
|
50
|
+
elif isinstance(obj, bytes):
|
|
51
|
+
return b2a_base64(obj).decode("ascii")
|
|
52
|
+
if type(obj).__module__ == "numpy":
|
|
53
|
+
import numpy as np
|
|
54
|
+
|
|
55
|
+
if isinstance(obj, np.number):
|
|
56
|
+
return repr(obj.item())
|
|
57
|
+
else:
|
|
58
|
+
raise TypeError("%r is not JSON serializable" % obj)
|
|
59
|
+
else:
|
|
60
|
+
raise TypeError("%r is not JSON serializable" % obj)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def json_dumps(data):
|
|
64
|
+
try:
|
|
65
|
+
return jsonmodule.dumps(data)
|
|
66
|
+
except TypeError:
|
|
67
|
+
logger.warning("Invalid JSON, will try a with a more forgiving json encoder")
|
|
68
|
+
return jsonmodule.dumps(
|
|
69
|
+
data,
|
|
70
|
+
default=json_default,
|
|
71
|
+
ensure_ascii=False,
|
|
72
|
+
allow_nan=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
ipykernel_version = tuple(map(int, ipykernel.__version__.split(".")))
|
|
77
|
+
if ipykernel_version >= (6, 18, 0):
|
|
78
|
+
import comm.base_comm
|
|
79
|
+
|
|
80
|
+
class Comm(comm.base_comm.BaseComm):
|
|
81
|
+
kernel: Union[ipykernel.kernelbase.Kernel, None]
|
|
82
|
+
|
|
83
|
+
def __init__(self, **kwargs) -> None:
|
|
84
|
+
if ipykernel.kernelbase.Kernel.initialized():
|
|
85
|
+
self.kernel = ipykernel.kernelbase.Kernel.instance()
|
|
86
|
+
else:
|
|
87
|
+
self.kernel = None
|
|
88
|
+
super().__init__(**kwargs)
|
|
89
|
+
|
|
90
|
+
def publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys):
|
|
91
|
+
if self.kernel is None or self.kernel.session is None:
|
|
92
|
+
return
|
|
93
|
+
data = {} if data is None else data
|
|
94
|
+
metadata = {} if metadata is None else metadata
|
|
95
|
+
content = dict(data=data, comm_id=self.comm_id, **keys)
|
|
96
|
+
self.kernel.session.send(
|
|
97
|
+
self.kernel.iopub_socket,
|
|
98
|
+
msg_type,
|
|
99
|
+
content,
|
|
100
|
+
metadata=metadata,
|
|
101
|
+
parent=self.kernel.get_parent("shell"),
|
|
102
|
+
ident=self.topic,
|
|
103
|
+
buffers=buffers,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
comm.create_comm = Comm
|
|
107
|
+
|
|
108
|
+
def get_comm_manager():
|
|
109
|
+
from .kernel_context import get_current_context, has_current_context
|
|
110
|
+
|
|
111
|
+
if has_current_context():
|
|
112
|
+
return get_current_context().kernel.comm_manager
|
|
113
|
+
else:
|
|
114
|
+
return global_comm_manager
|
|
115
|
+
|
|
116
|
+
global_comm_manager = comm.get_comm_manager()
|
|
117
|
+
comm.get_comm_manager = get_comm_manager
|
|
118
|
+
|
|
119
|
+
# from notebook.base.zmqhandlers import serialize_binary_message
|
|
120
|
+
# this saves us a dependency on notebook/jupyter_server when e.g.
|
|
121
|
+
# running on pyodide
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _fix_msg(msg):
|
|
125
|
+
# makes sure the msg can be json serializable
|
|
126
|
+
# instead of using a callable like in jupyter_client (i.e. json_default)
|
|
127
|
+
# we replace the keys we know are problematic
|
|
128
|
+
# this allows us to use a faster json serializer in the future
|
|
129
|
+
if "header" in msg and "date" in msg["header"]:
|
|
130
|
+
# this is what jupyter_client.jsonutil.json_default does
|
|
131
|
+
msg["header"]["date"] = msg["header"]["date"].isoformat().replace("+00:00", "Z")
|
|
132
|
+
if "parent_header" in msg and "date" in msg["parent_header"]:
|
|
133
|
+
# date is already a string if it's copied from the header that is not turned into a datetime
|
|
134
|
+
# maybe we should do that in server.py
|
|
135
|
+
date = msg["parent_header"]["date"]
|
|
136
|
+
if isinstance(date, datetime):
|
|
137
|
+
msg["parent_header"]["date"] = date.isoformat().replace("+00:00", "Z")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def serialize_binary_message(msg):
|
|
141
|
+
"""serialize a message as a binary blob
|
|
142
|
+
|
|
143
|
+
Header:
|
|
144
|
+
|
|
145
|
+
4 bytes: number of msg parts (nbufs) as 32b int
|
|
146
|
+
4 * nbufs bytes: offset for each buffer as integer as 32b int
|
|
147
|
+
|
|
148
|
+
Offsets are from the start of the buffer, including the header.
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
|
|
153
|
+
The message serialized to bytes.
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
# don't modify msg or buffer list in-place
|
|
157
|
+
msg = msg.copy()
|
|
158
|
+
buffers = list(msg.pop("buffers"))
|
|
159
|
+
bmsg = json_dumps(msg).encode("utf8")
|
|
160
|
+
buffers.insert(0, bmsg)
|
|
161
|
+
nbufs = len(buffers)
|
|
162
|
+
offsets = [4 * (nbufs + 1)]
|
|
163
|
+
for buf in buffers[:-1]:
|
|
164
|
+
offsets.append(offsets[-1] + len(buf))
|
|
165
|
+
offsets_buf = struct.pack("!" + "I" * (nbufs + 1), nbufs, *offsets)
|
|
166
|
+
buffers.insert(0, offsets_buf)
|
|
167
|
+
return b"".join(buffers)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def deserialize_binary_message(bmsg):
|
|
171
|
+
"""deserialize a message from a binary blog
|
|
172
|
+
|
|
173
|
+
Header:
|
|
174
|
+
|
|
175
|
+
4 bytes: number of msg parts (nbufs) as 32b int
|
|
176
|
+
4 * nbufs bytes: offset for each buffer as integer as 32b int
|
|
177
|
+
|
|
178
|
+
Offsets are from the start of the buffer, including the header.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
message dictionary
|
|
183
|
+
"""
|
|
184
|
+
nbufs = struct.unpack("!i", bmsg[:4])[0]
|
|
185
|
+
offsets = list(struct.unpack("!" + "I" * nbufs, bmsg[4 : 4 * (nbufs + 1)]))
|
|
186
|
+
offsets.append(None)
|
|
187
|
+
bufs = []
|
|
188
|
+
for start, stop in zip(offsets[:-1], offsets[1:]):
|
|
189
|
+
bufs.append(bmsg[start:stop])
|
|
190
|
+
msg = json.loads(bufs[0].decode("utf8"))
|
|
191
|
+
msg["buffers"] = bufs[1:]
|
|
192
|
+
return msg
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
SESSION_KEY = b"solara"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class WebsocketStream:
|
|
199
|
+
def __init__(self, session, channel: str):
|
|
200
|
+
self.session = session
|
|
201
|
+
self.channel = channel
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class WebsocketStreamWrapper(ZMQStream):
|
|
205
|
+
def __init__(self, websocket, channel):
|
|
206
|
+
self.websocket = websocket
|
|
207
|
+
self.channel = channel
|
|
208
|
+
|
|
209
|
+
def flush(self, *ignore):
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def send_websockets(websockets: Set[websocket.WebsocketWrapper], binary_msg):
|
|
214
|
+
for ws in list(websockets):
|
|
215
|
+
try:
|
|
216
|
+
ws.send(binary_msg)
|
|
217
|
+
except: # noqa
|
|
218
|
+
# in case of any issue, we simply remove it from the list
|
|
219
|
+
try:
|
|
220
|
+
# websocket can be modified by another thread
|
|
221
|
+
websockets.remove(ws)
|
|
222
|
+
except KeyError:
|
|
223
|
+
pass # already removed
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class SessionWebsocket(session.Session):
|
|
227
|
+
def __init__(self, *args, **kwargs):
|
|
228
|
+
super().__init__(*args, **kwargs)
|
|
229
|
+
self.websockets: Set[websocket.WebsocketWrapper] = set() # map from .. msg id to websocket?
|
|
230
|
+
|
|
231
|
+
def close(self):
|
|
232
|
+
for ws in list(self.websockets):
|
|
233
|
+
try:
|
|
234
|
+
ws.close()
|
|
235
|
+
except: # noqa
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
def send(
|
|
239
|
+
self,
|
|
240
|
+
stream,
|
|
241
|
+
msg_or_type,
|
|
242
|
+
content=None,
|
|
243
|
+
parent=None,
|
|
244
|
+
ident=None,
|
|
245
|
+
buffers=None,
|
|
246
|
+
track=False,
|
|
247
|
+
header=None,
|
|
248
|
+
metadata=None,
|
|
249
|
+
):
|
|
250
|
+
try:
|
|
251
|
+
if isinstance(msg_or_type, dict):
|
|
252
|
+
msg = msg_or_type
|
|
253
|
+
else:
|
|
254
|
+
msg = self.msg(
|
|
255
|
+
msg_or_type,
|
|
256
|
+
content=content,
|
|
257
|
+
parent=parent,
|
|
258
|
+
header=header,
|
|
259
|
+
metadata=metadata,
|
|
260
|
+
)
|
|
261
|
+
_fix_msg(msg)
|
|
262
|
+
msg["channel"] = stream.channel
|
|
263
|
+
# not using pdb guard for performance reasons
|
|
264
|
+
try:
|
|
265
|
+
if buffers:
|
|
266
|
+
msg["buffers"] = [memoryview(k).cast("b") for k in buffers]
|
|
267
|
+
wire_message = serialize_binary_message(msg)
|
|
268
|
+
else:
|
|
269
|
+
wire_message = json_dumps(msg)
|
|
270
|
+
except Exception:
|
|
271
|
+
logger.exception("Could not serialize message: %r", msg)
|
|
272
|
+
if settings.main.use_pdb:
|
|
273
|
+
pdb.post_mortem()
|
|
274
|
+
raise
|
|
275
|
+
send_websockets(self.websockets, wire_message)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.exception("Error sending message: %s", e)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class Kernel(ipykernel.kernelbase.Kernel):
|
|
281
|
+
session: SessionWebsocket # type: ignore
|
|
282
|
+
# Ideally we have `session = Instance(Session, allow_none=True)`, but MyPy does not like it
|
|
283
|
+
|
|
284
|
+
implementation = "solara"
|
|
285
|
+
implementation_version = solara.__version__
|
|
286
|
+
banner = "solara"
|
|
287
|
+
|
|
288
|
+
def __init__(self):
|
|
289
|
+
super().__init__()
|
|
290
|
+
self.session = SessionWebsocket(parent=self, key=SESSION_KEY)
|
|
291
|
+
self.msg_queue = queue.Queue() # type: ignore
|
|
292
|
+
self.stream = self.iopub_socket = WebsocketStream(self.session, "iopub")
|
|
293
|
+
# on github action the next line gives a mypy error:
|
|
294
|
+
# solara/server/kernel.py:111: error: "SessionWebsocket" has no attribute "stream"
|
|
295
|
+
# not sure why we cannot reproduce that locally
|
|
296
|
+
self.session.stream = self.iopub_socket # type: ignore
|
|
297
|
+
if ipykernel_version >= (6, 18, 0):
|
|
298
|
+
# from this version on, ipykernel uses the comm package https://github.com/ipython/ipykernel/pull/973
|
|
299
|
+
self.comm_manager = CommManager(parent=self, kernel=self)
|
|
300
|
+
import ipywidgets.widgets.widget
|
|
301
|
+
|
|
302
|
+
if hasattr(ipywidgets.widgets.widget, "Comm"):
|
|
303
|
+
ipywidgets.widgets.widget.Comm = Comm
|
|
304
|
+
ipywidgets.widgets.widget.Widget.comm.klass = Comm
|
|
305
|
+
else:
|
|
306
|
+
self.comm_manager = CommManager(parent=self, kernel=self)
|
|
307
|
+
self.log = logging.getLogger("fake")
|
|
308
|
+
|
|
309
|
+
comm_msg_types = ["comm_open", "comm_msg", "comm_close"]
|
|
310
|
+
for msg_type in comm_msg_types:
|
|
311
|
+
self.shell_handlers[msg_type] = getattr(self.comm_manager, msg_type)
|
|
312
|
+
self.shell = SolaraInteractiveShell()
|
|
313
|
+
self.shell.display_pub.session = self.session
|
|
314
|
+
self.shell.display_pub.pub_socket = self.iopub_socket
|
|
315
|
+
|
|
316
|
+
async def _flush_control_queue(self):
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
# these don't work from non-main thread, and we do not care about them I think
|
|
320
|
+
# TODO: it seems that if post_handler_hook is not override, the flask reload tests fails
|
|
321
|
+
# for unknown reason
|
|
322
|
+
def pre_handler_hook(self, *args):
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
def post_handler_hook(self, *args):
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
def set_parent(self, ident, parent, channel="shell"):
|
|
329
|
+
"""Overridden from parent to tell the display hook and output streams
|
|
330
|
+
about the parent message.
|
|
331
|
+
"""
|
|
332
|
+
if ipykernel_major < 6:
|
|
333
|
+
# the channel argument was added in 6.0
|
|
334
|
+
super().set_parent(ident, parent)
|
|
335
|
+
else:
|
|
336
|
+
super().set_parent(ident, parent, channel)
|
|
337
|
+
if channel == "shell":
|
|
338
|
+
self.shell.set_parent(parent)
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import contextvars
|
|
6
|
+
except ModuleNotFoundError:
|
|
7
|
+
contextvars = None # type: ignore
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
import enum
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import pickle
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import typing
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Dict, List, Optional, cast
|
|
19
|
+
|
|
20
|
+
import ipywidgets as widgets
|
|
21
|
+
import reacton
|
|
22
|
+
from ipywidgets import DOMWidget, Widget
|
|
23
|
+
|
|
24
|
+
import solara.server.settings
|
|
25
|
+
import solara.util
|
|
26
|
+
|
|
27
|
+
from . import kernel, kernel_context, websocket
|
|
28
|
+
from .. import lifecycle
|
|
29
|
+
from .kernel import Kernel, WebsocketStreamWrapper
|
|
30
|
+
|
|
31
|
+
WebSocket = Any
|
|
32
|
+
logger = logging.getLogger("solara.server.app")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Local(threading.local):
|
|
36
|
+
kernel_context_stack: Optional[List[Optional["kernel_context.VirtualKernelContext"]]] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
local = Local()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PageStatus(enum.Enum):
|
|
43
|
+
CONNECTED = "connected"
|
|
44
|
+
DISCONNECTED = "disconnected"
|
|
45
|
+
CLOSED = "closed"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclasses.dataclass
|
|
49
|
+
class VirtualKernelContext:
|
|
50
|
+
id: str
|
|
51
|
+
kernel: kernel.Kernel
|
|
52
|
+
# we keep track of the session id to prevent kernel hijacking
|
|
53
|
+
# to 'steal' a kernel, one would need to know the session id
|
|
54
|
+
# *and* the kernel id
|
|
55
|
+
session_id: str
|
|
56
|
+
control_sockets: List[WebSocket] = dataclasses.field(default_factory=list)
|
|
57
|
+
# this is the 'private' version of the normally global ipywidgets.Widgets.widget dict
|
|
58
|
+
# see patch.py
|
|
59
|
+
widgets: Dict[str, Widget] = dataclasses.field(default_factory=dict)
|
|
60
|
+
# same, for ipyvue templates
|
|
61
|
+
# see patch.py
|
|
62
|
+
templates: Dict[str, Widget] = dataclasses.field(default_factory=dict)
|
|
63
|
+
user_dicts: Dict[str, Dict] = dataclasses.field(default_factory=dict)
|
|
64
|
+
# anything we need to attach to the context
|
|
65
|
+
# e.g. for a react app the render context, so that we can store/restore the state
|
|
66
|
+
app_object: Optional[Any] = None
|
|
67
|
+
reload: Callable = lambda: None # noqa: E731
|
|
68
|
+
state: Any = None
|
|
69
|
+
container: Optional[DOMWidget] = None
|
|
70
|
+
# we track which pages are connected to implement kernel culling
|
|
71
|
+
page_status: Dict[str, PageStatus] = dataclasses.field(default_factory=dict)
|
|
72
|
+
# only used for testing
|
|
73
|
+
_last_kernel_cull_task: "Optional[asyncio.Future[None]]" = None
|
|
74
|
+
closed_event: threading.Event = dataclasses.field(default_factory=threading.Event)
|
|
75
|
+
_on_close_callbacks: List[Callable[[], None]] = dataclasses.field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
def __post_init__(self):
|
|
78
|
+
with self:
|
|
79
|
+
for f, *_ in lifecycle._on_kernel_start_callbacks:
|
|
80
|
+
cleanup = f()
|
|
81
|
+
if cleanup:
|
|
82
|
+
self.on_close(cleanup)
|
|
83
|
+
|
|
84
|
+
def restart(self):
|
|
85
|
+
# should we do this, or maybe close the context and create a new one?
|
|
86
|
+
with self:
|
|
87
|
+
for f in reversed(self._on_close_callbacks):
|
|
88
|
+
f()
|
|
89
|
+
self._on_close_callbacks.clear()
|
|
90
|
+
self.__post_init__()
|
|
91
|
+
|
|
92
|
+
def display(self, *args):
|
|
93
|
+
print(args) # noqa
|
|
94
|
+
|
|
95
|
+
def on_close(self, f: Callable[[], None]):
|
|
96
|
+
self._on_close_callbacks.append(f)
|
|
97
|
+
|
|
98
|
+
def __enter__(self):
|
|
99
|
+
if local.kernel_context_stack is None:
|
|
100
|
+
local.kernel_context_stack = []
|
|
101
|
+
key = get_current_thread_key()
|
|
102
|
+
local.kernel_context_stack.append(current_context.get(key, None))
|
|
103
|
+
current_context[key] = self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, *args):
|
|
106
|
+
key = get_current_thread_key()
|
|
107
|
+
assert local.kernel_context_stack is not None
|
|
108
|
+
current_context[key] = local.kernel_context_stack.pop()
|
|
109
|
+
|
|
110
|
+
def close(self):
|
|
111
|
+
if self.closed_event.is_set():
|
|
112
|
+
logger.error("Tried to close a kernel context that is already closed: %s", self.id)
|
|
113
|
+
return
|
|
114
|
+
logger.info("Shut down virtual kernel: %s", self.id)
|
|
115
|
+
with self:
|
|
116
|
+
for f in reversed(self._on_close_callbacks):
|
|
117
|
+
f()
|
|
118
|
+
with self:
|
|
119
|
+
if self.app_object is not None:
|
|
120
|
+
if isinstance(self.app_object, reacton.core._RenderContext):
|
|
121
|
+
try:
|
|
122
|
+
self.app_object.close()
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.exception("Could not close render context: %s", e)
|
|
125
|
+
# we want to continue, so we at least close all widgets
|
|
126
|
+
widgets.Widget.close_all()
|
|
127
|
+
# what if we reference each other
|
|
128
|
+
# import gc
|
|
129
|
+
# gc.collect()
|
|
130
|
+
self.kernel.session.close()
|
|
131
|
+
if self.id in contexts:
|
|
132
|
+
del contexts[self.id]
|
|
133
|
+
self.closed_event.set()
|
|
134
|
+
|
|
135
|
+
def _state_reset(self):
|
|
136
|
+
state_directory = Path(".") / "states"
|
|
137
|
+
state_directory.mkdir(exist_ok=True)
|
|
138
|
+
path = state_directory / f"{self.id}.pickle"
|
|
139
|
+
path = path.absolute()
|
|
140
|
+
try:
|
|
141
|
+
path.unlink()
|
|
142
|
+
except: # noqa
|
|
143
|
+
pass
|
|
144
|
+
del contexts[self.id]
|
|
145
|
+
key = get_current_thread_key()
|
|
146
|
+
del current_context[key]
|
|
147
|
+
|
|
148
|
+
def state_save(self, state_directory: os.PathLike):
|
|
149
|
+
path = Path(state_directory) / f"{self.id}.pickle"
|
|
150
|
+
render_context = self.app_object
|
|
151
|
+
if render_context is not None:
|
|
152
|
+
render_context = cast(reacton.core._RenderContext, render_context)
|
|
153
|
+
state = render_context.state_get()
|
|
154
|
+
with path.open("wb") as f:
|
|
155
|
+
logger.debug("State: %r", state)
|
|
156
|
+
pickle.dump(state, f)
|
|
157
|
+
|
|
158
|
+
def page_connect(self, page_id: str):
|
|
159
|
+
logger.info("Connect page %s for kernel %s", page_id, self.id)
|
|
160
|
+
assert self.page_status.get(page_id) != PageStatus.CLOSED, "cannot connect with the same page_id after a close"
|
|
161
|
+
self.page_status[page_id] = PageStatus.CONNECTED
|
|
162
|
+
if self._last_kernel_cull_task:
|
|
163
|
+
self._last_kernel_cull_task.cancel()
|
|
164
|
+
|
|
165
|
+
def page_disconnect(self, page_id: str) -> "asyncio.Future[None]":
|
|
166
|
+
"""Signal that a page has disconnected, and schedule a kernel cull if needed.
|
|
167
|
+
|
|
168
|
+
During the kernel reconnect window, we will keep the kernel alive, even if all pages have disconnected.
|
|
169
|
+
|
|
170
|
+
Returns a future that is set when the kernel cull is done.
|
|
171
|
+
The scheduled kernel cull can be cancelled when a new page connects, a new disconnect is scheduled,
|
|
172
|
+
or a page if explicitly closed.
|
|
173
|
+
"""
|
|
174
|
+
logger.info("Disconnect page %s for kernel %s", page_id, self.id)
|
|
175
|
+
future: "asyncio.Future[None]" = asyncio.Future()
|
|
176
|
+
self.page_status[page_id] = PageStatus.DISCONNECTED
|
|
177
|
+
current_event_loop = asyncio.get_event_loop()
|
|
178
|
+
|
|
179
|
+
async def kernel_cull():
|
|
180
|
+
try:
|
|
181
|
+
cull_timeout_sleep_seconds = solara.util.parse_timedelta(solara.server.settings.kernel.cull_timeout)
|
|
182
|
+
logger.info("Scheduling kernel cull, will wait for max %s before shutting down the virtual kernel %s", cull_timeout_sleep_seconds, self.id)
|
|
183
|
+
await asyncio.sleep(cull_timeout_sleep_seconds)
|
|
184
|
+
has_connected_pages = PageStatus.CONNECTED in self.page_status.values()
|
|
185
|
+
if has_connected_pages:
|
|
186
|
+
logger.info("We have (re)connected pages, keeping the virtual kernel %s alive", self.id)
|
|
187
|
+
else:
|
|
188
|
+
logger.info("No connected pages, and timeout reached, shutting down virtual kernel %s", self.id)
|
|
189
|
+
self.close()
|
|
190
|
+
current_event_loop.call_soon_threadsafe(future.set_result, None)
|
|
191
|
+
except asyncio.CancelledError:
|
|
192
|
+
if sys.version_info >= (3, 9):
|
|
193
|
+
current_event_loop.call_soon_threadsafe(future.cancel, "cancelled because a new cull task was scheduled")
|
|
194
|
+
else:
|
|
195
|
+
current_event_loop.call_soon_threadsafe(future.cancel)
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
has_connected_pages = PageStatus.CONNECTED in self.page_status.values()
|
|
199
|
+
if not has_connected_pages:
|
|
200
|
+
# when we have no connected pages, we will schedule a kernel cull
|
|
201
|
+
if self._last_kernel_cull_task:
|
|
202
|
+
self._last_kernel_cull_task.cancel()
|
|
203
|
+
|
|
204
|
+
async def create_task():
|
|
205
|
+
task = asyncio.create_task(kernel_cull())
|
|
206
|
+
# create a reference to the task so we can cancel it later
|
|
207
|
+
self._last_kernel_cull_task = task
|
|
208
|
+
await task
|
|
209
|
+
|
|
210
|
+
asyncio.run_coroutine_threadsafe(create_task(), keep_alive_event_loop)
|
|
211
|
+
else:
|
|
212
|
+
future.set_result(None)
|
|
213
|
+
return future
|
|
214
|
+
|
|
215
|
+
def page_close(self, page_id: str):
|
|
216
|
+
"""Signal that a page has closed, and close the context if needed.
|
|
217
|
+
|
|
218
|
+
Closing the browser tab or a page navigation means an explicit close, which is
|
|
219
|
+
different from a websocket/page disconnect, which we might want to recover from.
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
self.page_status[page_id] = PageStatus.CLOSED
|
|
223
|
+
logger.info("Close page %s for kernel %s", page_id, self.id)
|
|
224
|
+
has_connected_pages = PageStatus.CONNECTED in self.page_status.values()
|
|
225
|
+
has_disconnected_pages = PageStatus.DISCONNECTED in self.page_status.values()
|
|
226
|
+
if not (has_connected_pages or has_disconnected_pages):
|
|
227
|
+
logger.info("No connected or disconnected pages, shutting down virtual kernel %s", self.id)
|
|
228
|
+
if self._last_kernel_cull_task:
|
|
229
|
+
self._last_kernel_cull_task.cancel()
|
|
230
|
+
self.close()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
# Normal Python
|
|
235
|
+
keep_alive_event_loop = asyncio.new_event_loop()
|
|
236
|
+
|
|
237
|
+
def _run():
|
|
238
|
+
asyncio.set_event_loop(keep_alive_event_loop)
|
|
239
|
+
try:
|
|
240
|
+
keep_alive_event_loop.run_forever()
|
|
241
|
+
except Exception:
|
|
242
|
+
logger.exception("Error in keep alive event loop")
|
|
243
|
+
raise
|
|
244
|
+
|
|
245
|
+
threading.Thread(target=_run, daemon=True).start()
|
|
246
|
+
except RuntimeError:
|
|
247
|
+
# Emscripten/pyodide/lite
|
|
248
|
+
keep_alive_event_loop = asyncio.get_event_loop()
|
|
249
|
+
|
|
250
|
+
contexts: Dict[str, VirtualKernelContext] = {}
|
|
251
|
+
# maps from thread key to VirtualKernelContext, if VirtualKernelContext is None, it exists, but is not set as current
|
|
252
|
+
current_context: Dict[str, Optional[VirtualKernelContext]] = {}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def create_dummy_context():
|
|
256
|
+
from . import kernel
|
|
257
|
+
|
|
258
|
+
kernel_context = VirtualKernelContext(
|
|
259
|
+
id="dummy",
|
|
260
|
+
session_id="dummy",
|
|
261
|
+
kernel=kernel.Kernel(),
|
|
262
|
+
)
|
|
263
|
+
return kernel_context
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if contextvars is not None:
|
|
267
|
+
if typing.TYPE_CHECKING:
|
|
268
|
+
async_context_id = contextvars.ContextVar[str]("async_context_id")
|
|
269
|
+
else:
|
|
270
|
+
async_context_id = contextvars.ContextVar("async_context_id")
|
|
271
|
+
async_context_id.set("default")
|
|
272
|
+
else:
|
|
273
|
+
async_context_id = None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def get_current_thread_key() -> str:
|
|
277
|
+
if not solara.server.settings.kernel.threaded:
|
|
278
|
+
if async_context_id is not None:
|
|
279
|
+
try:
|
|
280
|
+
key = async_context_id.get()
|
|
281
|
+
except LookupError:
|
|
282
|
+
raise RuntimeError("no kernel context set")
|
|
283
|
+
else:
|
|
284
|
+
raise RuntimeError("No threading support, and no contextvars support (Python 3.6 is not supported for this)")
|
|
285
|
+
else:
|
|
286
|
+
thread = threading.current_thread()
|
|
287
|
+
key = get_thread_key(thread)
|
|
288
|
+
return key
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_thread_key(thread: threading.Thread) -> str:
|
|
292
|
+
if not solara.server.settings.kernel.threaded:
|
|
293
|
+
if async_context_id is not None:
|
|
294
|
+
return async_context_id.get()
|
|
295
|
+
thread_key = thread._name + str(thread._ident) # type: ignore
|
|
296
|
+
return thread_key
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def set_context_for_thread(context: VirtualKernelContext, thread: threading.Thread):
|
|
300
|
+
key = get_thread_key(thread)
|
|
301
|
+
current_context[key] = context
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def has_current_context() -> bool:
|
|
305
|
+
thread_key = get_current_thread_key()
|
|
306
|
+
return (thread_key in current_context) and (current_context[thread_key] is not None)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def get_current_context() -> VirtualKernelContext:
|
|
310
|
+
thread_key = get_current_thread_key()
|
|
311
|
+
if thread_key not in current_context:
|
|
312
|
+
raise RuntimeError(
|
|
313
|
+
f"Tried to get the current context for thread {thread_key}, but no known context found. This might be a bug in Solara. "
|
|
314
|
+
f"(known contexts: {list(current_context.keys())}"
|
|
315
|
+
)
|
|
316
|
+
context = current_context[thread_key]
|
|
317
|
+
if context is None:
|
|
318
|
+
raise RuntimeError(
|
|
319
|
+
f"Tried to get the current context for thread {thread_key!r}, although the context is know, it was not set for this thread. "
|
|
320
|
+
+ "This might be a bug in Solara."
|
|
321
|
+
)
|
|
322
|
+
return context
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def set_current_context(context: Optional[VirtualKernelContext]):
|
|
326
|
+
thread_key = get_current_thread_key()
|
|
327
|
+
current_context[thread_key] = context
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def initialize_virtual_kernel(session_id: str, kernel_id: str, websocket: websocket.WebsocketWrapper):
|
|
331
|
+
from solara.server import app as appmodule
|
|
332
|
+
|
|
333
|
+
if kernel_id in contexts:
|
|
334
|
+
logger.info("reusing virtual kernel: %s", kernel_id)
|
|
335
|
+
context = contexts[kernel_id]
|
|
336
|
+
if context.session_id != session_id:
|
|
337
|
+
logger.critical("Session id mismatch when reusing kernel (hack attempt?): %s != %s", context.session_id, session_id)
|
|
338
|
+
websocket.send_text("Session id mismatch when reusing kernel (hack attempt?)")
|
|
339
|
+
# to avoid very fast reconnects (we are in a thread anyway)
|
|
340
|
+
time.sleep(0.5)
|
|
341
|
+
raise ValueError("Session id mismatch")
|
|
342
|
+
kernel = context.kernel
|
|
343
|
+
else:
|
|
344
|
+
kernel = Kernel()
|
|
345
|
+
logger.info("new virtual kernel: %s", kernel_id)
|
|
346
|
+
context = contexts[kernel_id] = VirtualKernelContext(id=kernel_id, session_id=session_id, kernel=kernel, control_sockets=[], widgets={}, templates={})
|
|
347
|
+
|
|
348
|
+
with context:
|
|
349
|
+
widgets.register_comm_target(kernel)
|
|
350
|
+
appmodule.register_solara_comm_target(kernel)
|
|
351
|
+
with context:
|
|
352
|
+
assert has_current_context()
|
|
353
|
+
assert kernel is Kernel.instance()
|
|
354
|
+
kernel.shell_stream = WebsocketStreamWrapper(websocket, "shell")
|
|
355
|
+
kernel.control_stream = WebsocketStreamWrapper(websocket, "control")
|
|
356
|
+
kernel.session.websockets.add(websocket)
|
|
357
|
+
return context
|