streamlit-data-editor-plus 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- streamlit/__init__.py +292 -0
- streamlit/__main__.py +20 -0
- streamlit/auth_util.py +583 -0
- streamlit/cli_util.py +107 -0
- streamlit/column_config.py +60 -0
- streamlit/commands/__init__.py +13 -0
- streamlit/commands/echo.py +128 -0
- streamlit/commands/execution_control.py +318 -0
- streamlit/commands/logo.py +250 -0
- streamlit/commands/navigation.py +430 -0
- streamlit/commands/page_config.py +335 -0
- streamlit/components/__init__.py +13 -0
- streamlit/components/lib/__init__.py +13 -0
- streamlit/components/lib/local_component_registry.py +84 -0
- streamlit/components/types/__init__.py +13 -0
- streamlit/components/types/base_component_registry.py +99 -0
- streamlit/components/types/base_custom_component.py +158 -0
- streamlit/components/v1/__init__.py +29 -0
- streamlit/components/v1/component_arrow.py +141 -0
- streamlit/components/v1/component_registry.py +147 -0
- streamlit/components/v1/components.py +38 -0
- streamlit/components/v1/custom_component.py +239 -0
- streamlit/components/v2/__init__.py +531 -0
- streamlit/components/v2/bidi_component/__init__.py +20 -0
- streamlit/components/v2/bidi_component/constants.py +29 -0
- streamlit/components/v2/bidi_component/main.py +537 -0
- streamlit/components/v2/bidi_component/serialization.py +272 -0
- streamlit/components/v2/bidi_component/state.py +92 -0
- streamlit/components/v2/component_definition_resolver.py +143 -0
- streamlit/components/v2/component_file_watcher.py +403 -0
- streamlit/components/v2/component_manager.py +439 -0
- streamlit/components/v2/component_manifest_handler.py +122 -0
- streamlit/components/v2/component_path_utils.py +233 -0
- streamlit/components/v2/component_registry.py +426 -0
- streamlit/components/v2/get_bidi_component_manager.py +51 -0
- streamlit/components/v2/manifest_scanner.py +620 -0
- streamlit/components/v2/presentation.py +198 -0
- streamlit/components/v2/types.py +317 -0
- streamlit/config.py +2977 -0
- streamlit/config_option.py +319 -0
- streamlit/config_util.py +887 -0
- streamlit/connections/__init__.py +32 -0
- streamlit/connections/base_connection.py +215 -0
- streamlit/connections/snowflake_connection.py +765 -0
- streamlit/connections/snowpark_connection.py +213 -0
- streamlit/connections/sql_connection.py +427 -0
- streamlit/connections/util.py +97 -0
- streamlit/cursor.py +314 -0
- streamlit/dataframe_util.py +1434 -0
- streamlit/delta_generator.py +696 -0
- streamlit/delta_generator_singletons.py +243 -0
- streamlit/deprecation_util.py +253 -0
- streamlit/development.py +21 -0
- streamlit/elements/__init__.py +13 -0
- streamlit/elements/alert.py +341 -0
- streamlit/elements/arrow.py +1065 -0
- streamlit/elements/balloons.py +47 -0
- streamlit/elements/bokeh_chart.py +75 -0
- streamlit/elements/code.py +154 -0
- streamlit/elements/deck_gl_json_chart.py +627 -0
- streamlit/elements/dialog_decorator.py +320 -0
- streamlit/elements/empty.py +130 -0
- streamlit/elements/exception.py +370 -0
- streamlit/elements/form.py +481 -0
- streamlit/elements/graphviz_chart.py +226 -0
- streamlit/elements/heading.py +407 -0
- streamlit/elements/help.py +565 -0
- streamlit/elements/html.py +189 -0
- streamlit/elements/iframe.py +245 -0
- streamlit/elements/image.py +221 -0
- streamlit/elements/json.py +171 -0
- streamlit/elements/layouts.py +1534 -0
- streamlit/elements/lib/__init__.py +13 -0
- streamlit/elements/lib/built_in_chart_utils.py +1316 -0
- streamlit/elements/lib/color_util.py +272 -0
- streamlit/elements/lib/column_config_utils.py +555 -0
- streamlit/elements/lib/column_types.py +2699 -0
- streamlit/elements/lib/dialog.py +212 -0
- streamlit/elements/lib/dicttools.py +152 -0
- streamlit/elements/lib/file_uploader_utils.py +91 -0
- streamlit/elements/lib/form_utils.py +77 -0
- streamlit/elements/lib/image_utils.py +447 -0
- streamlit/elements/lib/js_number.py +105 -0
- streamlit/elements/lib/layout_utils.py +325 -0
- streamlit/elements/lib/mutable_expander_container.py +73 -0
- streamlit/elements/lib/mutable_popover_container.py +73 -0
- streamlit/elements/lib/mutable_status_container.py +194 -0
- streamlit/elements/lib/mutable_tab_container.py +73 -0
- streamlit/elements/lib/options_selector_utils.py +480 -0
- streamlit/elements/lib/pandas_styler_utils.py +302 -0
- streamlit/elements/lib/policies.py +198 -0
- streamlit/elements/lib/shortcut_utils.py +152 -0
- streamlit/elements/lib/streamlit_plotly_theme.py +206 -0
- streamlit/elements/lib/subtitle_utils.py +175 -0
- streamlit/elements/lib/utils.py +274 -0
- streamlit/elements/map.py +526 -0
- streamlit/elements/markdown.py +529 -0
- streamlit/elements/media.py +843 -0
- streamlit/elements/metric.py +529 -0
- streamlit/elements/pdf.py +190 -0
- streamlit/elements/plotly_chart.py +779 -0
- streamlit/elements/progress.py +172 -0
- streamlit/elements/pyplot.py +240 -0
- streamlit/elements/snow.py +47 -0
- streamlit/elements/space.py +121 -0
- streamlit/elements/spinner.py +152 -0
- streamlit/elements/table.py +240 -0
- streamlit/elements/text.py +113 -0
- streamlit/elements/toast.py +175 -0
- streamlit/elements/vega_charts.py +2510 -0
- streamlit/elements/widgets/__init__.py +13 -0
- streamlit/elements/widgets/audio_input.py +334 -0
- streamlit/elements/widgets/button.py +1559 -0
- streamlit/elements/widgets/button_group.py +1061 -0
- streamlit/elements/widgets/camera_input.py +281 -0
- streamlit/elements/widgets/chat.py +1028 -0
- streamlit/elements/widgets/checkbox.py +420 -0
- streamlit/elements/widgets/color_picker.py +329 -0
- streamlit/elements/widgets/data_editor.py +1168 -0
- streamlit/elements/widgets/data_editor_plus.py +902 -0
- streamlit/elements/widgets/feedback.py +322 -0
- streamlit/elements/widgets/file_uploader.py +592 -0
- streamlit/elements/widgets/multiselect.py +654 -0
- streamlit/elements/widgets/number_input.py +719 -0
- streamlit/elements/widgets/radio.py +551 -0
- streamlit/elements/widgets/select_slider.py +568 -0
- streamlit/elements/widgets/selectbox.py +680 -0
- streamlit/elements/widgets/slider.py +1093 -0
- streamlit/elements/widgets/text_widgets.py +779 -0
- streamlit/elements/widgets/time_widgets.py +1712 -0
- streamlit/elements/write.py +585 -0
- streamlit/emojis.py +34 -0
- streamlit/env_util.py +56 -0
- streamlit/error_util.py +112 -0
- streamlit/errors.py +680 -0
- streamlit/external/__init__.py +13 -0
- streamlit/external/langchain/__init__.py +23 -0
- streamlit/external/langchain/streamlit_callback_handler.py +410 -0
- streamlit/file_util.py +266 -0
- streamlit/git_util.py +200 -0
- streamlit/hello/__init__.py +13 -0
- streamlit/hello/__pycache__/__init__.cpython-312.pyc +0 -0
- streamlit/hello/__pycache__/streamlit_app.cpython-312.pyc +0 -0
- streamlit/hello/animation_demo.py +82 -0
- streamlit/hello/dataframe_demo.py +71 -0
- streamlit/hello/hello.py +46 -0
- streamlit/hello/mapping_demo.py +113 -0
- streamlit/hello/plotting_demo.py +62 -0
- streamlit/hello/streamlit_app.py +57 -0
- streamlit/hello/utils.py +30 -0
- streamlit/logger.py +129 -0
- streamlit/material_icon_names.py +25 -0
- streamlit/navigation/__init__.py +13 -0
- streamlit/navigation/page.py +365 -0
- streamlit/net_util.py +125 -0
- streamlit/path_security.py +98 -0
- streamlit/platform.py +31 -0
- streamlit/proto/Alert_pb2.py +28 -0
- streamlit/proto/Alert_pb2.pyi +100 -0
- streamlit/proto/AppPage_pb2.py +25 -0
- streamlit/proto/AppPage_pb2.pyi +75 -0
- streamlit/proto/ArrowData_pb2.py +27 -0
- streamlit/proto/ArrowData_pb2.pyi +103 -0
- streamlit/proto/ArrowNamedDataSet_pb2.py +26 -0
- streamlit/proto/ArrowNamedDataSet_pb2.pyi +65 -0
- streamlit/proto/AudioInput_pb2.py +26 -0
- streamlit/proto/AudioInput_pb2.pyi +72 -0
- streamlit/proto/Audio_pb2.py +26 -0
- streamlit/proto/Audio_pb2.pyi +75 -0
- streamlit/proto/AuthRedirect_pb2.py +25 -0
- streamlit/proto/AuthRedirect_pb2.pyi +48 -0
- streamlit/proto/AutoRerun_pb2.py +25 -0
- streamlit/proto/AutoRerun_pb2.pyi +52 -0
- streamlit/proto/BackMsg_pb2.py +29 -0
- streamlit/proto/BackMsg_pb2.pyi +132 -0
- streamlit/proto/Balloons_pb2.py +25 -0
- streamlit/proto/Balloons_pb2.pyi +48 -0
- streamlit/proto/BidiComponent_pb2.py +32 -0
- streamlit/proto/BidiComponent_pb2.pyi +176 -0
- streamlit/proto/Block_pb2.py +62 -0
- streamlit/proto/Block_pb2.pyi +518 -0
- streamlit/proto/ButtonGroup_pb2.py +32 -0
- streamlit/proto/ButtonGroup_pb2.pyi +157 -0
- streamlit/proto/ButtonLikeIconPosition_pb2.py +25 -0
- streamlit/proto/ButtonLikeIconPosition_pb2.pyi +47 -0
- streamlit/proto/Button_pb2.py +26 -0
- streamlit/proto/Button_pb2.pyi +82 -0
- streamlit/proto/CameraInput_pb2.py +26 -0
- streamlit/proto/CameraInput_pb2.pyi +66 -0
- streamlit/proto/ChatInput_pb2.py +27 -0
- streamlit/proto/ChatInput_pb2.pyi +113 -0
- streamlit/proto/Checkbox_pb2.py +28 -0
- streamlit/proto/Checkbox_pb2.pyi +99 -0
- streamlit/proto/ClientState_pb2.py +28 -0
- streamlit/proto/ClientState_pb2.pyi +137 -0
- streamlit/proto/Code_pb2.py +25 -0
- streamlit/proto/Code_pb2.pyi +59 -0
- streamlit/proto/ColorPicker_pb2.py +26 -0
- streamlit/proto/ColorPicker_pb2.pyi +82 -0
- streamlit/proto/Common_pb2.py +49 -0
- streamlit/proto/Common_pb2.pyi +325 -0
- streamlit/proto/Components_pb2.py +33 -0
- streamlit/proto/Components_pb2.pyi +196 -0
- streamlit/proto/Dataframe_pb2.py +30 -0
- streamlit/proto/Dataframe_pb2.pyi +172 -0
- streamlit/proto/DateInput_pb2.py +26 -0
- streamlit/proto/DateInput_pb2.pyi +91 -0
- streamlit/proto/DateTimeInput_pb2.py +26 -0
- streamlit/proto/DateTimeInput_pb2.pyi +94 -0
- streamlit/proto/DeckGlJsonChart_pb2.py +27 -0
- streamlit/proto/DeckGlJsonChart_pb2.pyi +91 -0
- streamlit/proto/Delta_pb2.py +29 -0
- streamlit/proto/Delta_pb2.pyi +82 -0
- streamlit/proto/DownloadButton_pb2.py +26 -0
- streamlit/proto/DownloadButton_pb2.pyi +92 -0
- streamlit/proto/Element_pb2.py +81 -0
- streamlit/proto/Element_pb2.pyi +348 -0
- streamlit/proto/Empty_pb2.py +25 -0
- streamlit/proto/Empty_pb2.pyi +42 -0
- streamlit/proto/Exception_pb2.py +26 -0
- streamlit/proto/Exception_pb2.pyi +88 -0
- streamlit/proto/Favicon_pb2.py +25 -0
- streamlit/proto/Favicon_pb2.pyi +47 -0
- streamlit/proto/Feedback_pb2.py +27 -0
- streamlit/proto/Feedback_pb2.pyi +93 -0
- streamlit/proto/FileUploader_pb2.py +26 -0
- streamlit/proto/FileUploader_pb2.pyi +90 -0
- streamlit/proto/ForwardMsg_pb2.py +48 -0
- streamlit/proto/ForwardMsg_pb2.pyi +296 -0
- streamlit/proto/GapSize_pb2.py +27 -0
- streamlit/proto/GapSize_pb2.pyi +82 -0
- streamlit/proto/GitInfo_pb2.py +27 -0
- streamlit/proto/GitInfo_pb2.pyi +84 -0
- streamlit/proto/GraphVizChart_pb2.py +25 -0
- streamlit/proto/GraphVizChart_pb2.pyi +56 -0
- streamlit/proto/Heading_pb2.py +25 -0
- streamlit/proto/Heading_pb2.pyi +63 -0
- streamlit/proto/HeightConfig_pb2.py +25 -0
- streamlit/proto/HeightConfig_pb2.pyi +61 -0
- streamlit/proto/Help_pb2.py +27 -0
- streamlit/proto/Help_pb2.pyi +104 -0
- streamlit/proto/Html_pb2.py +25 -0
- streamlit/proto/Html_pb2.pyi +52 -0
- streamlit/proto/IFrame_pb2.py +25 -0
- streamlit/proto/IFrame_pb2.pyi +68 -0
- streamlit/proto/Image_pb2.py +27 -0
- streamlit/proto/Image_pb2.pyi +73 -0
- streamlit/proto/Json_pb2.py +25 -0
- streamlit/proto/Json_pb2.pyi +63 -0
- streamlit/proto/LabelVisibility_pb2.py +27 -0
- streamlit/proto/LabelVisibility_pb2.pyi +69 -0
- streamlit/proto/LinkButton_pb2.py +26 -0
- streamlit/proto/LinkButton_pb2.pyi +76 -0
- streamlit/proto/Logo_pb2.py +27 -0
- streamlit/proto/Logo_pb2.pyi +82 -0
- streamlit/proto/Markdown_pb2.py +27 -0
- streamlit/proto/Markdown_pb2.pyi +83 -0
- streamlit/proto/Metric_pb2.py +32 -0
- streamlit/proto/Metric_pb2.pyi +145 -0
- streamlit/proto/MetricsEvent_pb2.py +28 -0
- streamlit/proto/MetricsEvent_pb2.pyi +224 -0
- streamlit/proto/MultiSelect_pb2.py +26 -0
- streamlit/proto/MultiSelect_pb2.pyi +108 -0
- streamlit/proto/Navigation_pb2.py +28 -0
- streamlit/proto/Navigation_pb2.pyi +89 -0
- streamlit/proto/NewSession_pb2.py +47 -0
- streamlit/proto/NewSession_pb2.pyi +753 -0
- streamlit/proto/NumberInput_pb2.py +28 -0
- streamlit/proto/NumberInput_pb2.pyi +138 -0
- streamlit/proto/PageConfig_pb2.py +33 -0
- streamlit/proto/PageConfig_pb2.pyi +179 -0
- streamlit/proto/PageInfo_pb2.py +25 -0
- streamlit/proto/PageInfo_pb2.pyi +50 -0
- streamlit/proto/PageLink_pb2.py +26 -0
- streamlit/proto/PageLink_pb2.pyi +80 -0
- streamlit/proto/PageNotFound_pb2.py +25 -0
- streamlit/proto/PageNotFound_pb2.pyi +49 -0
- streamlit/proto/PageProfile_pb2.py +29 -0
- streamlit/proto/PageProfile_pb2.pyi +146 -0
- streamlit/proto/ParentMessage_pb2.py +25 -0
- streamlit/proto/ParentMessage_pb2.pyi +53 -0
- streamlit/proto/PlotlyChart_pb2.py +27 -0
- streamlit/proto/PlotlyChart_pb2.pyi +96 -0
- streamlit/proto/Progress_pb2.py +25 -0
- streamlit/proto/Progress_pb2.pyi +50 -0
- streamlit/proto/Radio_pb2.py +26 -0
- streamlit/proto/Radio_pb2.pyi +104 -0
- streamlit/proto/RootContainer_pb2.py +25 -0
- streamlit/proto/RootContainer_pb2.pyi +56 -0
- streamlit/proto/Selectbox_pb2.py +26 -0
- streamlit/proto/Selectbox_pb2.pyi +107 -0
- streamlit/proto/SessionEvent_pb2.py +26 -0
- streamlit/proto/SessionEvent_pb2.pyi +72 -0
- streamlit/proto/SessionStatus_pb2.py +25 -0
- streamlit/proto/SessionStatus_pb2.pyi +64 -0
- streamlit/proto/Skeleton_pb2.py +27 -0
- streamlit/proto/Skeleton_pb2.pyi +75 -0
- streamlit/proto/Slider_pb2.py +30 -0
- streamlit/proto/Slider_pb2.pyi +161 -0
- streamlit/proto/Snow_pb2.py +25 -0
- streamlit/proto/Snow_pb2.pyi +48 -0
- streamlit/proto/Space_pb2.py +25 -0
- streamlit/proto/Space_pb2.pyi +48 -0
- streamlit/proto/Spinner_pb2.py +25 -0
- streamlit/proto/Spinner_pb2.pyi +56 -0
- streamlit/proto/Table_pb2.py +28 -0
- streamlit/proto/Table_pb2.pyi +83 -0
- streamlit/proto/TextAlignmentConfig_pb2.py +27 -0
- streamlit/proto/TextAlignmentConfig_pb2.pyi +69 -0
- streamlit/proto/TextArea_pb2.py +26 -0
- streamlit/proto/TextArea_pb2.pyi +97 -0
- streamlit/proto/TextInput_pb2.py +28 -0
- streamlit/proto/TextInput_pb2.pyi +124 -0
- streamlit/proto/Text_pb2.py +25 -0
- streamlit/proto/Text_pb2.pyi +53 -0
- streamlit/proto/TimeInput_pb2.py +26 -0
- streamlit/proto/TimeInput_pb2.pyi +86 -0
- streamlit/proto/Toast_pb2.py +25 -0
- streamlit/proto/Toast_pb2.pyi +64 -0
- streamlit/proto/Transient_pb2.py +26 -0
- streamlit/proto/Transient_pb2.pyi +53 -0
- streamlit/proto/VegaLiteChart_pb2.py +27 -0
- streamlit/proto/VegaLiteChart_pb2.pyi +92 -0
- streamlit/proto/Video_pb2.py +30 -0
- streamlit/proto/Video_pb2.pyi +129 -0
- streamlit/proto/WidgetStates_pb2.py +31 -0
- streamlit/proto/WidgetStates_pb2.pyi +157 -0
- streamlit/proto/WidthConfig_pb2.py +25 -0
- streamlit/proto/WidthConfig_pb2.pyi +61 -0
- streamlit/proto/__init__.py +15 -0
- streamlit/proto/openmetrics_data_model_pb2.py +58 -0
- streamlit/proto/openmetrics_data_model_pb2.pyi +558 -0
- streamlit/py.typed +0 -0
- streamlit/runtime/__init__.py +50 -0
- streamlit/runtime/app_session.py +1302 -0
- streamlit/runtime/caching/__init__.py +118 -0
- streamlit/runtime/caching/cache_data_api.py +775 -0
- streamlit/runtime/caching/cache_errors.py +143 -0
- streamlit/runtime/caching/cache_resource_api.py +756 -0
- streamlit/runtime/caching/cache_type.py +33 -0
- streamlit/runtime/caching/cache_utils.py +601 -0
- streamlit/runtime/caching/cached_message_replay.py +290 -0
- streamlit/runtime/caching/hashing.py +655 -0
- streamlit/runtime/caching/legacy_cache_api.py +170 -0
- streamlit/runtime/caching/storage/__init__.py +29 -0
- streamlit/runtime/caching/storage/cache_storage_protocol.py +236 -0
- streamlit/runtime/caching/storage/dummy_cache_storage.py +60 -0
- streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py +159 -0
- streamlit/runtime/caching/storage/local_disk_cache_storage.py +223 -0
- streamlit/runtime/caching/ttl_cleanup_cache.py +85 -0
- streamlit/runtime/connection_factory.py +498 -0
- streamlit/runtime/context.py +449 -0
- streamlit/runtime/context_util.py +49 -0
- streamlit/runtime/credentials.py +355 -0
- streamlit/runtime/download_data_util.py +53 -0
- streamlit/runtime/forward_msg_cache.py +101 -0
- streamlit/runtime/forward_msg_queue.py +263 -0
- streamlit/runtime/fragment.py +441 -0
- streamlit/runtime/media_file_manager.py +409 -0
- streamlit/runtime/media_file_storage.py +143 -0
- streamlit/runtime/memory_media_file_storage.py +201 -0
- streamlit/runtime/memory_session_storage.py +77 -0
- streamlit/runtime/memory_uploaded_file_manager.py +149 -0
- streamlit/runtime/metrics_util.py +612 -0
- streamlit/runtime/pages_manager.py +160 -0
- streamlit/runtime/runtime.py +775 -0
- streamlit/runtime/runtime_util.py +108 -0
- streamlit/runtime/script_data.py +46 -0
- streamlit/runtime/scriptrunner/__init__.py +38 -0
- streamlit/runtime/scriptrunner/exec_code.py +167 -0
- streamlit/runtime/scriptrunner/magic.py +277 -0
- streamlit/runtime/scriptrunner/magic_funcs.py +32 -0
- streamlit/runtime/scriptrunner/script_cache.py +89 -0
- streamlit/runtime/scriptrunner/script_runner.py +805 -0
- streamlit/runtime/scriptrunner_utils/__init__.py +19 -0
- streamlit/runtime/scriptrunner_utils/exceptions.py +44 -0
- streamlit/runtime/scriptrunner_utils/script_requests.py +311 -0
- streamlit/runtime/scriptrunner_utils/script_run_context.py +277 -0
- streamlit/runtime/secrets.py +533 -0
- streamlit/runtime/session_manager.py +430 -0
- streamlit/runtime/state/__init__.py +47 -0
- streamlit/runtime/state/common.py +256 -0
- streamlit/runtime/state/presentation.py +85 -0
- streamlit/runtime/state/query_params.py +767 -0
- streamlit/runtime/state/query_params_proxy.py +222 -0
- streamlit/runtime/state/safe_session_state.py +146 -0
- streamlit/runtime/state/session_state.py +1299 -0
- streamlit/runtime/state/session_state_proxy.py +153 -0
- streamlit/runtime/state/widgets.py +197 -0
- streamlit/runtime/stats.py +356 -0
- streamlit/runtime/theme_util.py +148 -0
- streamlit/runtime/uploaded_file_manager.py +152 -0
- streamlit/runtime/websocket_session_manager.py +301 -0
- streamlit/source_util.py +97 -0
- streamlit/starlette.py +34 -0
- streamlit/string_util.py +264 -0
- streamlit/temporary_directory.py +67 -0
- streamlit/testing/__init__.py +13 -0
- streamlit/testing/v1/__init__.py +17 -0
- streamlit/testing/v1/app_test.py +1089 -0
- streamlit/testing/v1/element_tree.py +2272 -0
- streamlit/testing/v1/local_script_runner.py +176 -0
- streamlit/testing/v1/util.py +61 -0
- streamlit/time_util.py +73 -0
- streamlit/type_util.py +480 -0
- streamlit/url_util.py +120 -0
- streamlit/user_info.py +698 -0
- streamlit/util.py +108 -0
- streamlit/vendor/__init__.py +0 -0
- streamlit/vendor/pympler/__init__.py +0 -0
- streamlit/vendor/pympler/asizeof.py +2870 -0
- streamlit/version.py +18 -0
- streamlit/watcher/__init__.py +28 -0
- streamlit/watcher/event_based_path_watcher.py +500 -0
- streamlit/watcher/folder_black_list.py +82 -0
- streamlit/watcher/local_sources_watcher.py +297 -0
- streamlit/watcher/path_watcher.py +244 -0
- streamlit/watcher/polling_path_watcher.py +138 -0
- streamlit/watcher/util.py +223 -0
- streamlit/web/__init__.py +13 -0
- streamlit/web/bootstrap.py +463 -0
- streamlit/web/cache_storage_manager_config.py +38 -0
- streamlit/web/cli.py +465 -0
- streamlit/web/server/__init__.py +30 -0
- streamlit/web/server/app_discovery.py +422 -0
- streamlit/web/server/app_static_file_handler.py +102 -0
- streamlit/web/server/authlib_tornado_integration.py +125 -0
- streamlit/web/server/bidi_component_request_handler.py +193 -0
- streamlit/web/server/browser_websocket_handler.py +341 -0
- streamlit/web/server/component_file_utils.py +105 -0
- streamlit/web/server/component_request_handler.py +107 -0
- streamlit/web/server/media_file_handler.py +148 -0
- streamlit/web/server/oauth_authlib_routes.py +314 -0
- streamlit/web/server/oidc_mixin.py +138 -0
- streamlit/web/server/routes.py +257 -0
- streamlit/web/server/server.py +555 -0
- streamlit/web/server/server_util.py +235 -0
- streamlit/web/server/starlette/__init__.py +23 -0
- streamlit/web/server/starlette/starlette_app.py +541 -0
- streamlit/web/server/starlette/starlette_app_utils.py +306 -0
- streamlit/web/server/starlette/starlette_auth_routes.py +584 -0
- streamlit/web/server/starlette/starlette_gzip_middleware.py +121 -0
- streamlit/web/server/starlette/starlette_path_security_middleware.py +97 -0
- streamlit/web/server/starlette/starlette_routes.py +884 -0
- streamlit/web/server/starlette/starlette_server.py +496 -0
- streamlit/web/server/starlette/starlette_server_config.py +55 -0
- streamlit/web/server/starlette/starlette_static_routes.py +181 -0
- streamlit/web/server/starlette/starlette_websocket.py +560 -0
- streamlit/web/server/stats_request_handler.py +116 -0
- streamlit/web/server/upload_file_request_handler.py +158 -0
- streamlit/web/server/websocket_headers.py +56 -0
- streamlit_data_editor_plus-0.1.0.dist-info/METADATA +76 -0
- streamlit_data_editor_plus-0.1.0.dist-info/RECORD +456 -0
- streamlit_data_editor_plus-0.1.0.dist-info/WHEEL +5 -0
- streamlit_data_editor_plus-0.1.0.dist-info/entry_points.txt +2 -0
- streamlit_data_editor_plus-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Final, cast
|
|
18
|
+
|
|
19
|
+
import tornado.web
|
|
20
|
+
|
|
21
|
+
import streamlit.web.server.routes
|
|
22
|
+
from streamlit.logger import get_logger
|
|
23
|
+
from streamlit.web.server.component_file_utils import (
|
|
24
|
+
build_safe_abspath,
|
|
25
|
+
guess_content_type,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from streamlit.components.types.base_component_registry import BaseComponentRegistry
|
|
30
|
+
|
|
31
|
+
_LOGGER: Final = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ComponentRequestHandler(tornado.web.RequestHandler):
|
|
35
|
+
def initialize(self, registry: BaseComponentRegistry) -> None:
|
|
36
|
+
self._registry = registry
|
|
37
|
+
|
|
38
|
+
def get(self, path: str) -> None:
|
|
39
|
+
parts = path.split("/")
|
|
40
|
+
component_name = parts[0]
|
|
41
|
+
component_root = self._registry.get_component_path(component_name)
|
|
42
|
+
if component_root is None:
|
|
43
|
+
self.write("not found")
|
|
44
|
+
self.set_status(404)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Build a safe absolute path within the component root
|
|
48
|
+
filename = "/".join(parts[1:])
|
|
49
|
+
abspath = build_safe_abspath(component_root, filename)
|
|
50
|
+
if abspath is None:
|
|
51
|
+
self.write("Bad Request")
|
|
52
|
+
self.set_status(400)
|
|
53
|
+
return
|
|
54
|
+
try:
|
|
55
|
+
with open(abspath, "rb") as file:
|
|
56
|
+
contents = file.read()
|
|
57
|
+
except OSError:
|
|
58
|
+
sanitized_abspath = abspath.replace("\n", "").replace("\r", "")
|
|
59
|
+
_LOGGER.exception(
|
|
60
|
+
"ComponentRequestHandler: GET %s read error", sanitized_abspath
|
|
61
|
+
)
|
|
62
|
+
self.write("read error")
|
|
63
|
+
self.set_status(404)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
self.write(contents)
|
|
67
|
+
self.set_header("Content-Type", self.get_content_type(abspath))
|
|
68
|
+
|
|
69
|
+
self.set_extra_headers(path)
|
|
70
|
+
|
|
71
|
+
def set_extra_headers(self, path: str) -> None:
|
|
72
|
+
"""Disable cache for HTML files.
|
|
73
|
+
|
|
74
|
+
Other assets like JS and CSS are suffixed with their hash, so they can
|
|
75
|
+
be cached indefinitely.
|
|
76
|
+
"""
|
|
77
|
+
is_index_url = len(path) == 0
|
|
78
|
+
|
|
79
|
+
if is_index_url or path.endswith(".html"):
|
|
80
|
+
self.set_header("Cache-Control", "no-cache")
|
|
81
|
+
else:
|
|
82
|
+
self.set_header("Cache-Control", "public")
|
|
83
|
+
|
|
84
|
+
def set_default_headers(self) -> None:
|
|
85
|
+
if streamlit.web.server.routes.allow_all_cross_origin_requests():
|
|
86
|
+
self.set_header("Access-Control-Allow-Origin", "*")
|
|
87
|
+
elif streamlit.web.server.routes.is_allowed_origin(
|
|
88
|
+
origin := self.request.headers.get("Origin")
|
|
89
|
+
):
|
|
90
|
+
self.set_header("Access-Control-Allow-Origin", cast("str", origin))
|
|
91
|
+
|
|
92
|
+
def options(self) -> None:
|
|
93
|
+
"""/OPTIONS handler for preflight CORS checks."""
|
|
94
|
+
self.set_status(204)
|
|
95
|
+
self.finish()
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def get_content_type(abspath: str) -> str:
|
|
99
|
+
"""Returns the ``Content-Type`` header to be used for this request.
|
|
100
|
+
From tornado.web.StaticFileHandler.
|
|
101
|
+
"""
|
|
102
|
+
return guess_content_type(abspath)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def get_url(file_id: str) -> str:
|
|
106
|
+
"""Return the URL for a component file with the given ID."""
|
|
107
|
+
return f"components/{file_id}"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, cast
|
|
18
|
+
from urllib.parse import quote
|
|
19
|
+
|
|
20
|
+
import tornado.web
|
|
21
|
+
|
|
22
|
+
from streamlit.logger import get_logger
|
|
23
|
+
from streamlit.runtime.media_file_storage import MediaFileKind, MediaFileStorageError
|
|
24
|
+
from streamlit.runtime.memory_media_file_storage import (
|
|
25
|
+
MemoryMediaFileStorage,
|
|
26
|
+
get_extension_for_mimetype,
|
|
27
|
+
)
|
|
28
|
+
from streamlit.web.server import allow_all_cross_origin_requests, is_allowed_origin
|
|
29
|
+
|
|
30
|
+
_LOGGER = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MediaFileHandler(tornado.web.StaticFileHandler):
|
|
34
|
+
_storage: MemoryMediaFileStorage
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def initialize_storage(cls, storage: MemoryMediaFileStorage) -> None:
|
|
38
|
+
"""Set the MemoryMediaFileStorage object used by instances of this
|
|
39
|
+
handler. Must be called on server startup.
|
|
40
|
+
"""
|
|
41
|
+
# This is a class method, rather than an instance method, because
|
|
42
|
+
# `get_content()` is a class method and needs to access the storage
|
|
43
|
+
# instance.
|
|
44
|
+
cls._storage = storage
|
|
45
|
+
|
|
46
|
+
def set_default_headers(self) -> None:
|
|
47
|
+
if allow_all_cross_origin_requests():
|
|
48
|
+
self.set_header("Access-Control-Allow-Origin", "*")
|
|
49
|
+
elif is_allowed_origin(origin := self.request.headers.get("Origin")):
|
|
50
|
+
self.set_header("Access-Control-Allow-Origin", cast("str", origin))
|
|
51
|
+
|
|
52
|
+
def set_extra_headers(self, path: str) -> None:
|
|
53
|
+
"""Add Content-Disposition header for downloadable files.
|
|
54
|
+
|
|
55
|
+
Set header value to "attachment" indicating that file should be saved
|
|
56
|
+
locally instead of displaying inline in browser.
|
|
57
|
+
|
|
58
|
+
We also set filename to specify the filename for downloaded files.
|
|
59
|
+
Used for serving downloadable files, like files stored via the
|
|
60
|
+
`st.download_button` widget.
|
|
61
|
+
"""
|
|
62
|
+
media_file = self._storage.get_file(path)
|
|
63
|
+
|
|
64
|
+
if media_file and media_file.kind == MediaFileKind.DOWNLOADABLE:
|
|
65
|
+
filename = media_file.filename
|
|
66
|
+
|
|
67
|
+
if not filename:
|
|
68
|
+
filename = f"streamlit_download{get_extension_for_mimetype(media_file.mimetype)}"
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# Check that the value can be encoded in latin1. Latin1 is
|
|
72
|
+
# the default encoding for headers.
|
|
73
|
+
filename.encode("latin1")
|
|
74
|
+
file_expr = f'filename="{filename}"'
|
|
75
|
+
except UnicodeEncodeError:
|
|
76
|
+
# RFC5987 syntax.
|
|
77
|
+
# See: https://datatracker.ietf.org/doc/html/rfc5987
|
|
78
|
+
file_expr = f"filename*=utf-8''{quote(filename)}"
|
|
79
|
+
|
|
80
|
+
self.set_header("Content-Disposition", f"attachment; {file_expr}")
|
|
81
|
+
|
|
82
|
+
# Overriding StaticFileHandler to use the MediaFileManager
|
|
83
|
+
#
|
|
84
|
+
# From the Tornado docs:
|
|
85
|
+
# To replace all interaction with the filesystem (e.g. to serve
|
|
86
|
+
# static content from a database), override `get_content`,
|
|
87
|
+
# `get_content_size`, `get_modified_time`, `get_absolute_path`, and
|
|
88
|
+
# `validate_absolute_path`.
|
|
89
|
+
def validate_absolute_path(
|
|
90
|
+
self,
|
|
91
|
+
root: str, # noqa: ARG002
|
|
92
|
+
absolute_path: str,
|
|
93
|
+
) -> str:
|
|
94
|
+
try:
|
|
95
|
+
self._storage.get_file(absolute_path)
|
|
96
|
+
except MediaFileStorageError:
|
|
97
|
+
_LOGGER.exception("MediaFileHandler: Missing file %s", absolute_path)
|
|
98
|
+
raise tornado.web.HTTPError(404, "not found")
|
|
99
|
+
|
|
100
|
+
return absolute_path
|
|
101
|
+
|
|
102
|
+
def get_content_size(self) -> int:
|
|
103
|
+
abspath = self.absolute_path
|
|
104
|
+
if abspath is None:
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
media_file = self._storage.get_file(abspath)
|
|
108
|
+
return media_file.content_size
|
|
109
|
+
|
|
110
|
+
def get_modified_time(self) -> None:
|
|
111
|
+
# We do not track last modified time, but this can be improved to
|
|
112
|
+
# allow caching among files in the MediaFileManager
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def get_absolute_path(cls, root: str, path: str) -> str: # noqa: ARG003
|
|
117
|
+
# All files are stored in memory, so the absolute path is just the
|
|
118
|
+
# path itself. In the MediaFileHandler, it's just the filename
|
|
119
|
+
return path
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def get_content(
|
|
123
|
+
cls, abspath: str, start: int | None = None, end: int | None = None
|
|
124
|
+
) -> Any:
|
|
125
|
+
_LOGGER.debug("MediaFileHandler: GET %s", abspath)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# abspath is the hash as used `get_absolute_path`
|
|
129
|
+
media_file = cls._storage.get_file(abspath)
|
|
130
|
+
except Exception:
|
|
131
|
+
_LOGGER.exception("MediaFileHandler: Missing file %s", abspath)
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
_LOGGER.debug(
|
|
135
|
+
"MediaFileHandler: Sending %s file %s", media_file.mimetype, abspath
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# If there is no start and end, just return the full content
|
|
139
|
+
if start is None and end is None:
|
|
140
|
+
return media_file.content
|
|
141
|
+
|
|
142
|
+
if start is None:
|
|
143
|
+
start = 0
|
|
144
|
+
if end is None:
|
|
145
|
+
end = len(media_file.content)
|
|
146
|
+
|
|
147
|
+
# content is bytes that work just by slicing supplied by start and end
|
|
148
|
+
return media_file.content[start:end]
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from typing import Any, Final, cast
|
|
18
|
+
|
|
19
|
+
import tornado.web
|
|
20
|
+
|
|
21
|
+
from streamlit.auth_util import (
|
|
22
|
+
AuthCache,
|
|
23
|
+
build_logout_url,
|
|
24
|
+
clear_cookie_and_chunks,
|
|
25
|
+
decode_provider_token,
|
|
26
|
+
generate_default_provider_section,
|
|
27
|
+
get_cookie_with_chunks,
|
|
28
|
+
get_origin_from_redirect_uri,
|
|
29
|
+
get_redirect_uri,
|
|
30
|
+
get_secrets_auth_section,
|
|
31
|
+
get_validated_redirect_uri,
|
|
32
|
+
set_cookie_with_chunks,
|
|
33
|
+
)
|
|
34
|
+
from streamlit.errors import StreamlitAuthError
|
|
35
|
+
from streamlit.logger import get_logger
|
|
36
|
+
from streamlit.url_util import make_url_path
|
|
37
|
+
from streamlit.web.server.oidc_mixin import TornadoOAuth, TornadoOAuth2App
|
|
38
|
+
from streamlit.web.server.server_util import AUTH_COOKIE_NAME, TOKENS_COOKIE_NAME
|
|
39
|
+
|
|
40
|
+
_LOGGER: Final = get_logger(__name__)
|
|
41
|
+
|
|
42
|
+
auth_cache = AuthCache()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_oauth_client(provider: str) -> tuple[TornadoOAuth2App, str]:
|
|
46
|
+
"""Create an OAuth client for the given provider based on secrets.toml configuration."""
|
|
47
|
+
auth_section = get_secrets_auth_section()
|
|
48
|
+
if auth_section:
|
|
49
|
+
redirect_uri = get_redirect_uri(auth_section) or "/"
|
|
50
|
+
config = auth_section.to_dict()
|
|
51
|
+
else:
|
|
52
|
+
config = {}
|
|
53
|
+
redirect_uri = "/"
|
|
54
|
+
|
|
55
|
+
provider_section = config.setdefault(provider, {})
|
|
56
|
+
|
|
57
|
+
if not provider_section and provider == "default":
|
|
58
|
+
provider_section = generate_default_provider_section(auth_section)
|
|
59
|
+
config["default"] = provider_section
|
|
60
|
+
|
|
61
|
+
provider_client_kwargs = provider_section.setdefault("client_kwargs", {})
|
|
62
|
+
if "scope" not in provider_client_kwargs:
|
|
63
|
+
provider_client_kwargs["scope"] = "openid email profile"
|
|
64
|
+
if "prompt" not in provider_client_kwargs:
|
|
65
|
+
provider_client_kwargs["prompt"] = "select_account"
|
|
66
|
+
|
|
67
|
+
oauth = TornadoOAuth(config, cache=auth_cache)
|
|
68
|
+
oauth.register(provider)
|
|
69
|
+
return oauth.create_client(provider), redirect_uri # type: ignore[no-untyped-call]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AuthHandlerMixin(tornado.web.RequestHandler):
|
|
73
|
+
"""Mixin for handling auth cookies. Added for compatibility with Tornado < 6.3.0."""
|
|
74
|
+
|
|
75
|
+
def initialize(self, base_url: str) -> None:
|
|
76
|
+
self.base_url = base_url
|
|
77
|
+
|
|
78
|
+
def redirect_to_base(self) -> None:
|
|
79
|
+
self.redirect(make_url_path(self.base_url, "/"))
|
|
80
|
+
|
|
81
|
+
def set_auth_cookie(
|
|
82
|
+
self, user_info: dict[str, Any], tokens: dict[str, Any]
|
|
83
|
+
) -> None:
|
|
84
|
+
set_cookie_with_chunks(
|
|
85
|
+
self._set_single_cookie,
|
|
86
|
+
self._create_signed_value,
|
|
87
|
+
AUTH_COOKIE_NAME,
|
|
88
|
+
user_info,
|
|
89
|
+
)
|
|
90
|
+
set_cookie_with_chunks(
|
|
91
|
+
self._set_single_cookie,
|
|
92
|
+
self._create_signed_value,
|
|
93
|
+
TOKENS_COOKIE_NAME,
|
|
94
|
+
tokens,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _set_single_cookie(self, cookie_name: str, value: str) -> None:
|
|
98
|
+
"""Set a single cookie."""
|
|
99
|
+
try:
|
|
100
|
+
# We don't specify Tornado secure flag here because it leads to missing cookie on Safari.
|
|
101
|
+
# The OIDC flow should work only on secure context anyway (localhost or HTTPS),
|
|
102
|
+
# so specifying the secure flag here will not add anything in terms of security.
|
|
103
|
+
self.set_signed_cookie(
|
|
104
|
+
cookie_name,
|
|
105
|
+
value,
|
|
106
|
+
httpOnly=True,
|
|
107
|
+
)
|
|
108
|
+
except AttributeError:
|
|
109
|
+
self.set_secure_cookie(
|
|
110
|
+
cookie_name,
|
|
111
|
+
value,
|
|
112
|
+
httponly=True,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _create_signed_value(self, cookie_name: str, value: str) -> bytes:
|
|
116
|
+
"""Create a signed cookie value."""
|
|
117
|
+
try:
|
|
118
|
+
return self.create_signed_value(cookie_name, value)
|
|
119
|
+
except AttributeError:
|
|
120
|
+
# Default to the older method for compatibility with Tornado < 6.3.0
|
|
121
|
+
return cast("bytes", self.create_secure_cookie_value(cookie_name, value)) # type: ignore[attr-defined]
|
|
122
|
+
|
|
123
|
+
def _get_signed_cookie(self, cookie_name: str) -> bytes | None:
|
|
124
|
+
"""Get a signed cookie."""
|
|
125
|
+
try:
|
|
126
|
+
return cast("bytes", self.get_signed_cookie(cookie_name))
|
|
127
|
+
except AttributeError:
|
|
128
|
+
# Default to the older method for compatibility with Tornado < 6.3.0
|
|
129
|
+
return cast("bytes", self.get_secure_cookie(cookie_name))
|
|
130
|
+
except Exception:
|
|
131
|
+
# Handle cases where cookie_secret is not configured or other errors
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
def clear_auth_cookie(self) -> None:
|
|
135
|
+
"""Clear auth cookies, including any split cookie chunks."""
|
|
136
|
+
clear_cookie_and_chunks(
|
|
137
|
+
self._get_signed_cookie,
|
|
138
|
+
self.clear_cookie,
|
|
139
|
+
AUTH_COOKIE_NAME,
|
|
140
|
+
)
|
|
141
|
+
clear_cookie_and_chunks(
|
|
142
|
+
self._get_signed_cookie,
|
|
143
|
+
self.clear_cookie,
|
|
144
|
+
TOKENS_COOKIE_NAME,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class AuthLoginHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
149
|
+
async def get(self) -> None:
|
|
150
|
+
"""Redirect to the OAuth provider login page."""
|
|
151
|
+
provider = self._parse_provider_token()
|
|
152
|
+
if provider is None:
|
|
153
|
+
self.redirect_to_base()
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
client, redirect_uri = create_oauth_client(provider)
|
|
157
|
+
try:
|
|
158
|
+
client.authorize_redirect(self, redirect_uri)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
self.send_error(400, reason=str(e))
|
|
161
|
+
|
|
162
|
+
def _parse_provider_token(self) -> str | None:
|
|
163
|
+
provider_token = self.get_argument("provider", None)
|
|
164
|
+
if provider_token is None:
|
|
165
|
+
return None
|
|
166
|
+
try:
|
|
167
|
+
payload = decode_provider_token(provider_token)
|
|
168
|
+
except StreamlitAuthError:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
return payload["provider"]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class AuthLogoutHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
175
|
+
def get(self) -> None:
|
|
176
|
+
self.clear_auth_cookie()
|
|
177
|
+
|
|
178
|
+
provider_logout_url = self._get_provider_logout_url()
|
|
179
|
+
if provider_logout_url:
|
|
180
|
+
self.redirect(provider_logout_url)
|
|
181
|
+
else:
|
|
182
|
+
self.redirect_to_base()
|
|
183
|
+
|
|
184
|
+
def _get_provider_logout_url(self) -> str | None:
|
|
185
|
+
"""Get the OAuth provider's logout URL from OIDC metadata."""
|
|
186
|
+
cookie_value = get_cookie_with_chunks(self._get_signed_cookie, AUTH_COOKIE_NAME)
|
|
187
|
+
|
|
188
|
+
if not cookie_value:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
user_info = json.loads(cookie_value)
|
|
193
|
+
provider = user_info.get("provider")
|
|
194
|
+
if not provider:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
client, _ = create_oauth_client(provider)
|
|
198
|
+
|
|
199
|
+
metadata = client.load_server_metadata()
|
|
200
|
+
end_session_endpoint = metadata.get("end_session_endpoint")
|
|
201
|
+
|
|
202
|
+
if not end_session_endpoint:
|
|
203
|
+
_LOGGER.info("No end_session_endpoint found for provider %s", provider)
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
# Use redirect_uri (i.e. /oauth2callback) for post_logout_redirect_uri
|
|
207
|
+
# This is safer than redirecting to root as some providers seem to
|
|
208
|
+
# require url to be in a whitelist /oauth2callback should be whitelisted
|
|
209
|
+
redirect_uri = get_validated_redirect_uri()
|
|
210
|
+
if redirect_uri is None:
|
|
211
|
+
_LOGGER.info("Redirect url could not be determined")
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
# Get id_token_hint from tokens cookie if available
|
|
215
|
+
id_token: str | None = None
|
|
216
|
+
tokens_cookie_value = get_cookie_with_chunks(
|
|
217
|
+
self._get_signed_cookie, TOKENS_COOKIE_NAME
|
|
218
|
+
)
|
|
219
|
+
if tokens_cookie_value:
|
|
220
|
+
try:
|
|
221
|
+
tokens = json.loads(tokens_cookie_value)
|
|
222
|
+
id_token = tokens.get("id_token")
|
|
223
|
+
except (json.JSONDecodeError, TypeError):
|
|
224
|
+
_LOGGER.exception("Error, invalid tokens cookie value.")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
return build_logout_url(
|
|
228
|
+
end_session_endpoint=end_session_endpoint,
|
|
229
|
+
client_id=client.client_id,
|
|
230
|
+
post_logout_redirect_uri=redirect_uri,
|
|
231
|
+
id_token=id_token,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
_LOGGER.warning("Failed to get provider logout URL: %s", e)
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class AuthCallbackHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
240
|
+
async def get(self) -> None:
|
|
241
|
+
provider = self._get_provider_by_state()
|
|
242
|
+
if provider is None:
|
|
243
|
+
# This could be a logout redirect (no state parameter) or invalid state
|
|
244
|
+
# In both cases, redirect to base
|
|
245
|
+
self.redirect_to_base()
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
origin = self._get_origin_from_secrets()
|
|
249
|
+
if origin is None:
|
|
250
|
+
_LOGGER.error(
|
|
251
|
+
"Error, misconfigured origin for `redirect_uri` in secrets.",
|
|
252
|
+
)
|
|
253
|
+
self.redirect_to_base()
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
error = self.get_argument("error", None)
|
|
257
|
+
if error:
|
|
258
|
+
error_description = self.get_argument("error_description", None)
|
|
259
|
+
sanitized_error = error.replace("\n", "").replace("\r", "")
|
|
260
|
+
sanitized_error_description = (
|
|
261
|
+
error_description.replace("\n", "").replace("\r", "")
|
|
262
|
+
if error_description
|
|
263
|
+
else None
|
|
264
|
+
)
|
|
265
|
+
_LOGGER.error(
|
|
266
|
+
"Error during authentication: %s. Error description: %s",
|
|
267
|
+
sanitized_error,
|
|
268
|
+
sanitized_error_description,
|
|
269
|
+
)
|
|
270
|
+
self.redirect_to_base()
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
client, _ = create_oauth_client(provider)
|
|
274
|
+
token = client.authorize_access_token(self)
|
|
275
|
+
user = cast("dict[str, Any]", token.get("userinfo"))
|
|
276
|
+
|
|
277
|
+
cookie_value = dict(user, origin=origin, is_logged_in=True, provider=provider)
|
|
278
|
+
tokens = {k: token[k] for k in ["id_token", "access_token"] if k in token}
|
|
279
|
+
|
|
280
|
+
if user:
|
|
281
|
+
self.set_auth_cookie(cookie_value, tokens)
|
|
282
|
+
# Keep tokens in a separate cookie to avoid hitting the size limit
|
|
283
|
+
else:
|
|
284
|
+
_LOGGER.error("Error, missing user info.")
|
|
285
|
+
self.redirect_to_base()
|
|
286
|
+
|
|
287
|
+
def _get_provider_by_state(self) -> str | None:
|
|
288
|
+
state_code_from_url = self.get_argument("state", None)
|
|
289
|
+
if state_code_from_url is None:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
current_cache_keys = list(auth_cache.get_dict().keys())
|
|
293
|
+
state_provider_mapping = {}
|
|
294
|
+
for key in current_cache_keys:
|
|
295
|
+
# Authlib stores OAuth state in the cache using keys in the format:
|
|
296
|
+
# "_state_{provider}_{state_code}" (e.g., "_state_google_abc123").
|
|
297
|
+
#
|
|
298
|
+
# Note: This split assumes no underscores in provider names or state codes.
|
|
299
|
+
# This is safe because: (1) provider names with underscores are explicitly
|
|
300
|
+
# blocked in validate_auth_credentials() in auth_util.py, and (2) Authlib's
|
|
301
|
+
# generate_token() uses only alphanumeric characters (a-zA-Z0-9) for state
|
|
302
|
+
# codes. See auth_util.py for the underscore validation.
|
|
303
|
+
try:
|
|
304
|
+
_, _, recorded_provider, code = key.split("_")
|
|
305
|
+
except ValueError:
|
|
306
|
+
# Skip cache keys that don't match the expected 4-part format.
|
|
307
|
+
continue
|
|
308
|
+
state_provider_mapping[code] = recorded_provider
|
|
309
|
+
|
|
310
|
+
provider: str | None = state_provider_mapping.get(state_code_from_url)
|
|
311
|
+
return provider
|
|
312
|
+
|
|
313
|
+
def _get_origin_from_secrets(self) -> str | None:
|
|
314
|
+
return get_origin_from_redirect_uri()
|