streamlit 1.45.1__py3-none-any.whl → 1.46.1__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 +5 -1
- streamlit/auth_util.py +12 -12
- streamlit/cli_util.py +4 -3
- streamlit/column_config.py +11 -9
- streamlit/commands/echo.py +6 -4
- streamlit/commands/execution_control.py +33 -32
- streamlit/commands/experimental_query_params.py +2 -2
- streamlit/commands/logo.py +9 -4
- streamlit/commands/navigation.py +61 -18
- streamlit/commands/page_config.py +57 -47
- streamlit/components/types/base_custom_component.py +7 -7
- streamlit/components/v1/component_registry.py +7 -3
- streamlit/components/v1/components.py +1 -1
- streamlit/components/v1/custom_component.py +8 -8
- streamlit/config.py +289 -144
- streamlit/config_option.py +19 -15
- streamlit/config_util.py +29 -23
- streamlit/connections/__init__.py +2 -2
- streamlit/connections/base_connection.py +5 -5
- streamlit/connections/snowflake_connection.py +13 -11
- streamlit/connections/snowpark_connection.py +3 -3
- streamlit/connections/sql_connection.py +20 -18
- streamlit/connections/util.py +2 -2
- streamlit/cursor.py +6 -6
- streamlit/dataframe_util.py +52 -52
- streamlit/delta_generator.py +46 -48
- streamlit/delta_generator_singletons.py +3 -3
- streamlit/deprecation_util.py +6 -6
- streamlit/elements/alert.py +37 -29
- streamlit/elements/arrow.py +40 -22
- streamlit/elements/code.py +46 -13
- streamlit/elements/deck_gl_json_chart.py +38 -27
- streamlit/elements/dialog_decorator.py +3 -4
- streamlit/elements/doc_string.py +64 -58
- streamlit/elements/exception.py +23 -27
- streamlit/elements/form.py +41 -0
- streamlit/elements/graphviz_chart.py +1 -1
- streamlit/elements/heading.py +60 -9
- streamlit/elements/html.py +3 -4
- streamlit/elements/image.py +8 -9
- streamlit/elements/json.py +21 -2
- streamlit/elements/layouts.py +120 -31
- streamlit/elements/lib/built_in_chart_utils.py +96 -73
- streamlit/elements/lib/color_util.py +3 -3
- streamlit/elements/lib/column_config_utils.py +2 -4
- streamlit/elements/lib/column_types.py +14 -8
- streamlit/elements/lib/dialog.py +9 -5
- streamlit/elements/lib/image_utils.py +39 -40
- streamlit/elements/lib/js_number.py +4 -4
- streamlit/elements/lib/layout_utils.py +65 -1
- streamlit/elements/lib/mutable_status_container.py +14 -3
- streamlit/elements/lib/options_selector_utils.py +22 -12
- streamlit/elements/lib/pandas_styler_utils.py +25 -21
- streamlit/elements/lib/policies.py +6 -5
- streamlit/elements/lib/streamlit_plotly_theme.py +54 -53
- streamlit/elements/lib/subtitle_utils.py +6 -9
- streamlit/elements/lib/utils.py +20 -5
- streamlit/elements/map.py +32 -56
- streamlit/elements/markdown.py +101 -12
- streamlit/elements/media.py +78 -21
- streamlit/elements/metric.py +32 -16
- streamlit/elements/plotly_chart.py +15 -15
- streamlit/elements/progress.py +33 -15
- streamlit/elements/spinner.py +31 -6
- streamlit/elements/text.py +21 -1
- streamlit/elements/toast.py +1 -2
- streamlit/elements/vega_charts.py +54 -23
- streamlit/elements/widgets/audio_input.py +24 -7
- streamlit/elements/widgets/button.py +26 -19
- streamlit/elements/widgets/button_group.py +10 -15
- streamlit/elements/widgets/camera_input.py +27 -7
- streamlit/elements/widgets/chat.py +91 -38
- streamlit/elements/widgets/checkbox.py +45 -4
- streamlit/elements/widgets/color_picker.py +40 -17
- streamlit/elements/widgets/data_editor.py +76 -37
- streamlit/elements/widgets/file_uploader.py +42 -13
- streamlit/elements/widgets/multiselect.py +7 -10
- streamlit/elements/widgets/number_input.py +123 -47
- streamlit/elements/widgets/radio.py +59 -13
- streamlit/elements/widgets/select_slider.py +35 -30
- streamlit/elements/widgets/selectbox.py +56 -9
- streamlit/elements/widgets/slider.py +190 -99
- streamlit/elements/widgets/text_widgets.py +54 -8
- streamlit/elements/widgets/time_widgets.py +53 -14
- streamlit/elements/write.py +5 -8
- streamlit/env_util.py +2 -7
- streamlit/error_util.py +16 -9
- streamlit/errors.py +69 -48
- streamlit/external/langchain/streamlit_callback_handler.py +10 -5
- streamlit/file_util.py +27 -10
- streamlit/git_util.py +29 -24
- streamlit/hello/animation_demo.py +9 -9
- streamlit/hello/dataframe_demo.py +5 -5
- streamlit/hello/hello.py +1 -0
- streamlit/hello/mapping_demo.py +7 -8
- streamlit/hello/plotting_demo.py +3 -3
- streamlit/hello/streamlit_app.py +28 -26
- streamlit/hello/utils.py +2 -1
- streamlit/logger.py +10 -11
- streamlit/navigation/page.py +11 -8
- streamlit/proto/Audio_pb2.py +4 -3
- streamlit/proto/Audio_pb2.pyi +8 -1
- streamlit/proto/Block_pb2.py +38 -29
- streamlit/proto/Block_pb2.pyi +72 -4
- streamlit/proto/ClientState_pb2.py +4 -4
- streamlit/proto/ClientState_pb2.pyi +7 -2
- streamlit/proto/Code_pb2.py +4 -2
- streamlit/proto/Code_pb2.pyi +1 -0
- streamlit/proto/DataFrame_pb2.pyi +1 -1
- streamlit/proto/DeckGlJsonChart_pb2.pyi +1 -1
- streamlit/proto/Element_pb2.py +5 -3
- streamlit/proto/Element_pb2.pyi +20 -3
- streamlit/proto/GapSize_pb2.py +29 -0
- streamlit/proto/GapSize_pb2.pyi +70 -0
- streamlit/proto/HeightConfig_pb2.py +27 -0
- streamlit/proto/HeightConfig_pb2.pyi +48 -0
- streamlit/proto/NamedDataSet_pb2.pyi +1 -1
- streamlit/proto/Navigation_pb2.py +3 -3
- streamlit/proto/Navigation_pb2.pyi +4 -0
- streamlit/proto/NewSession_pb2.py +18 -16
- streamlit/proto/NewSession_pb2.pyi +29 -3
- streamlit/proto/PageConfig_pb2.py +7 -7
- streamlit/proto/PageConfig_pb2.pyi +21 -1
- streamlit/proto/Video_pb2.py +8 -7
- streamlit/proto/Video_pb2.pyi +8 -1
- streamlit/proto/WidthConfig_pb2.py +2 -2
- streamlit/proto/WidthConfig_pb2.pyi +15 -1
- streamlit/runtime/__init__.py +1 -1
- streamlit/runtime/app_session.py +53 -40
- streamlit/runtime/caching/__init__.py +9 -9
- streamlit/runtime/caching/cache_data_api.py +36 -30
- streamlit/runtime/caching/cache_errors.py +4 -4
- streamlit/runtime/caching/cache_resource_api.py +8 -8
- streamlit/runtime/caching/cache_utils.py +15 -14
- streamlit/runtime/caching/cached_message_replay.py +14 -8
- streamlit/runtime/caching/hashing.py +91 -97
- streamlit/runtime/caching/legacy_cache_api.py +2 -2
- streamlit/runtime/caching/storage/cache_storage_protocol.py +1 -1
- streamlit/runtime/caching/storage/dummy_cache_storage.py +1 -1
- streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py +12 -14
- streamlit/runtime/caching/storage/local_disk_cache_storage.py +6 -6
- streamlit/runtime/connection_factory.py +36 -36
- streamlit/runtime/context.py +58 -9
- streamlit/runtime/credentials.py +29 -40
- streamlit/runtime/forward_msg_queue.py +11 -11
- streamlit/runtime/fragment.py +7 -7
- streamlit/runtime/media_file_manager.py +3 -4
- streamlit/runtime/memory_media_file_storage.py +6 -5
- streamlit/runtime/memory_uploaded_file_manager.py +2 -2
- streamlit/runtime/metrics_util.py +11 -12
- streamlit/runtime/pages_manager.py +4 -6
- streamlit/runtime/runtime.py +8 -6
- streamlit/runtime/runtime_util.py +7 -6
- streamlit/runtime/scriptrunner/__init__.py +4 -4
- streamlit/runtime/scriptrunner/exec_code.py +12 -5
- streamlit/runtime/scriptrunner/magic.py +16 -12
- streamlit/runtime/scriptrunner/script_cache.py +1 -1
- streamlit/runtime/scriptrunner/script_runner.py +53 -29
- streamlit/runtime/scriptrunner_utils/exceptions.py +1 -1
- streamlit/runtime/scriptrunner_utils/script_requests.py +7 -4
- streamlit/runtime/scriptrunner_utils/script_run_context.py +10 -23
- streamlit/runtime/secrets.py +40 -35
- streamlit/runtime/session_manager.py +2 -1
- streamlit/runtime/state/__init__.py +5 -5
- streamlit/runtime/state/common.py +2 -2
- streamlit/runtime/state/query_params.py +13 -15
- streamlit/runtime/state/query_params_proxy.py +17 -13
- streamlit/runtime/state/safe_session_state.py +2 -2
- streamlit/runtime/state/session_state.py +52 -34
- streamlit/runtime/stats.py +2 -2
- streamlit/runtime/uploaded_file_manager.py +1 -1
- streamlit/runtime/websocket_session_manager.py +10 -6
- streamlit/source_util.py +8 -6
- streamlit/static/index.html +3 -17
- streamlit/static/manifest.json +1180 -0
- streamlit/static/static/css/{index.DqDwtg6_.css → index.CJVRHjQZ.css} +1 -1
- streamlit/static/static/js/{ErrorOutline.esm.DU9IrB3M.js → ErrorOutline.esm.DitPpe1Y.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.P9rKwKo8.js → FileDownload.esm.AI3watX9.js} +1 -1
- streamlit/static/static/js/{FileHelper.D7RMkx0e.js → FileHelper.kt7mhnu8.js} +5 -5
- streamlit/static/static/js/{FormClearHelper.B67tgll0.js → FormClearHelper.D1M9GM_c.js} +1 -1
- streamlit/static/static/js/{Hooks.ncTJktu9.js → Hooks.BGwHKeUc.js} +1 -1
- streamlit/static/static/js/{InputInstructions.D-Y8geDN.js → InputInstructions.DaZ89mzH.js} +1 -1
- streamlit/static/static/js/{ProgressBar.B-kexwwD.js → ProgressBar.C0zPMe-p.js} +2 -2
- streamlit/static/static/js/{RenderInPortalIfExists.BgaoZgep.js → RenderInPortalIfExists.Ox8gQvdz.js} +1 -1
- streamlit/static/static/js/Toolbar.KhlcEc0K.js +1 -0
- streamlit/static/static/js/UploadFileInfo.0DCkpDDf.js +6 -0
- streamlit/static/static/js/{base-input.BoAa1U94.js → base-input.BJ4qsfSq.js} +4 -4
- streamlit/static/static/js/{checkbox.Z6iSfe5F.js → checkbox.DSDh78Xz.js} +2 -2
- streamlit/static/static/js/{createSuper.B4oGDYRm.js → createSuper.wQ9SIXEJ.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.msYws2Ou.js → data-grid-overlay-editor.DvbdPJ15.js} +1 -1
- streamlit/static/static/js/{downloader.kc14n2Hv.js → downloader.CD9rzih5.js} +1 -1
- streamlit/static/static/js/{es6.CxQz807-.js → es6.48Q9Qjgb.js} +2 -2
- streamlit/static/static/js/{iframeResizer.contentWindow.B19u0ONI.js → iframeResizer.contentWindow.CKdem3Bn.js} +1 -1
- streamlit/static/static/js/{index.LaIasviC.js → index.6md5Qhod.js} +1 -1
- streamlit/static/static/js/index.7hy6AeJ1.js +1 -0
- streamlit/static/static/js/index.B4CGJiBW.js +1 -0
- streamlit/static/static/js/index.B8oW0ZTD.js +1 -0
- streamlit/static/static/js/index.BU6RnlHI.js +73 -0
- streamlit/static/static/js/index.BUq9Wcf8.js +197 -0
- streamlit/static/static/js/{index.BFz9U2y0.js → index.BXXo-Yoj.js} +1 -1
- streamlit/static/static/js/index.Bae9H0OS.js +1 -0
- streamlit/static/static/js/{index.-5ruC9At.js → index.BhTl2Uyb.js} +1 -1
- streamlit/static/static/js/{index.BpILzHf_.js → index.BiSaCB1o.js} +20 -20
- streamlit/static/static/js/{index.xNQq3Ei5.js → index.BulSAJ9z.js} +1 -1
- streamlit/static/static/js/{index.9V1KdxfP.js → index.Bv-EuTKR.js} +1 -1
- streamlit/static/static/js/index.BvMLYCHi.js +1 -0
- streamlit/static/static/js/index.C1NIn1Y2.js +783 -0
- streamlit/static/static/js/index.CP-fthOJ.js +2 -0
- streamlit/static/static/js/{index.BoigZiu7.js → index.CS9guO3p.js} +1 -1
- streamlit/static/static/js/index.CYTBHth8.js +1 -0
- streamlit/static/static/js/{index.CmTAF0dM.js → index.CcJufcuD.js} +1 -1
- streamlit/static/static/js/index.CnENU1yn.js +1 -0
- streamlit/static/static/js/index.Cns13qBb.js +1 -0
- streamlit/static/static/js/index.Ct_xXq7w.js +1 -0
- streamlit/static/static/js/{index.BqfdT8-Q.js → index.CxGSemHL.js} +1 -1
- streamlit/static/static/js/index.D5S0ldVb.js +1 -0
- streamlit/static/static/js/index.D72B_ksb.js +2 -0
- streamlit/static/static/js/index.DI4yZ27M.js +1 -0
- streamlit/static/static/js/index.DN51vLxR.js +1 -0
- streamlit/static/static/js/index.DRtq5dka.js +1 -0
- streamlit/static/static/js/{index.BHXxWdde.js → index.DX-oiXlb.js} +1 -1
- streamlit/static/static/js/index.DlFE4_Aq.js +12 -0
- streamlit/static/static/js/{index.BHGGDa8K.js → index.J7BJwXOi.js} +2 -2
- streamlit/static/static/js/index.Jg38kJPP.js +1 -0
- streamlit/static/static/js/index.JhIO6abf.js +3 -0
- streamlit/static/static/js/{index.DeB9iKFW.js → index.NkRcWwc5.js} +255 -255
- streamlit/static/static/js/{index.BGga-hcS.js → index.prekPLrm.js} +25 -25
- streamlit/static/static/js/{index.BRXmLIsC.js → index.wyzngKUE.js} +1 -1
- streamlit/static/static/js/index.xW7mVdI8.js +1 -0
- streamlit/static/static/js/index.yk07dYGx.js +1 -0
- streamlit/static/static/js/{input.DsCfafm0.js → input.CxKZ5Wrc.js} +2 -2
- streamlit/static/static/js/{memory.nY_lMTtu.js → memory.DeZ9VUvl.js} +1 -1
- streamlit/static/static/js/{mergeWith.B_7zmsM4.js → mergeWith.CVkhrWUb.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.CSeVhHRU.js → number-overlay-editor.Bpkm3nTq.js} +1 -1
- streamlit/static/static/js/{possibleConstructorReturn.nNhsvgRd.js → possibleConstructorReturn.CIDCId52.js} +1 -1
- streamlit/static/static/js/{sandbox.Cgm3iuL6.js → sandbox.TrkMaokR.js} +1 -1
- streamlit/static/static/js/{textarea.BR8rlyih.js → textarea.QKjxR64N.js} +2 -2
- streamlit/static/static/js/{timepicker.w4XhAenH.js → timepicker.DJYmE1dK.js} +1 -1
- streamlit/static/static/js/{toConsumableArray.CgkEPBwD.js → toConsumableArray.BZoworE-.js} +1 -1
- streamlit/static/static/js/{uniqueId.j-1rlNNH.js → uniqueId.O0UbJ2Bu.js} +1 -1
- streamlit/static/static/js/{useBasicWidgetState.zXY9CjFS.js → useBasicWidgetState.Ci89jaH5.js} +1 -1
- streamlit/static/static/js/useOnInputChange.Cxh6ExEn.js +1 -0
- streamlit/static/static/js/{withFullScreenWrapper.Ov13692o.js → withFullScreenWrapper.iW37lS8Z.js} +1 -1
- streamlit/static/static/media/SourceCodeVF-Italic.ttf.Ba1oaZG1.woff2 +0 -0
- streamlit/static/static/media/SourceCodeVF-Upright.ttf.BjWn63N-.woff2 +0 -0
- streamlit/static/static/media/SourceSansVF-Italic.ttf.Bt9VkdQ3.woff2 +0 -0
- streamlit/static/static/media/SourceSansVF-Upright.ttf.BsWL4Kly.woff2 +0 -0
- streamlit/static/static/media/SourceSerifVariable-Italic.ttf.CVdzAtxO.woff2 +0 -0
- streamlit/static/static/media/SourceSerifVariable-Roman.ttf.mdpVL9bi.woff2 +0 -0
- streamlit/string_util.py +14 -19
- streamlit/temporary_directory.py +13 -4
- streamlit/testing/v1/app_test.py +15 -10
- streamlit/testing/v1/element_tree.py +157 -178
- streamlit/testing/v1/local_script_runner.py +11 -15
- streamlit/testing/v1/util.py +11 -4
- streamlit/type_util.py +8 -12
- streamlit/url_util.py +1 -1
- streamlit/user_info.py +6 -5
- streamlit/util.py +25 -1
- streamlit/vendor/pympler/asizeof.py +3 -2
- streamlit/watcher/event_based_path_watcher.py +21 -2
- streamlit/watcher/folder_black_list.py +2 -2
- streamlit/watcher/local_sources_watcher.py +64 -18
- streamlit/watcher/path_watcher.py +6 -10
- streamlit/watcher/polling_path_watcher.py +8 -7
- streamlit/watcher/util.py +7 -6
- streamlit/web/bootstrap.py +16 -14
- streamlit/web/cli.py +52 -45
- streamlit/web/server/__init__.py +7 -3
- streamlit/web/server/app_static_file_handler.py +1 -1
- streamlit/web/server/authlib_tornado_integration.py +9 -4
- streamlit/web/server/browser_websocket_handler.py +8 -2
- streamlit/web/server/component_request_handler.py +14 -10
- streamlit/web/server/media_file_handler.py +14 -7
- streamlit/web/server/oauth_authlib_routes.py +41 -9
- streamlit/web/server/oidc_mixin.py +35 -17
- streamlit/web/server/routes.py +32 -22
- streamlit/web/server/server.py +13 -24
- streamlit/web/server/server_util.py +43 -9
- streamlit/web/server/stats_request_handler.py +7 -5
- streamlit/web/server/upload_file_request_handler.py +22 -19
- streamlit/web/server/websocket_headers.py +1 -1
- {streamlit-1.45.1.dist-info → streamlit-1.46.1.dist-info}/METADATA +4 -4
- streamlit-1.46.1.dist-info/RECORD +559 -0
- {streamlit-1.45.1.dist-info → streamlit-1.46.1.dist-info}/WHEEL +1 -1
- streamlit/elements/lib/event_utils.py +0 -39
- streamlit/static/static/js/Toolbar.D9RUZv9G.js +0 -1
- streamlit/static/static/js/UploadFileInfo.C-jY39rj.js +0 -1
- streamlit/static/static/js/index.8jhZBWF2.js +0 -3
- streamlit/static/static/js/index.BCx3C6e_.js +0 -1
- streamlit/static/static/js/index.BRuTz_S4.js +0 -1
- streamlit/static/static/js/index.Bcru_ti-.js +0 -1
- streamlit/static/static/js/index.Bl1FMJRd.js +0 -1
- streamlit/static/static/js/index.C1z8KpLA.js +0 -779
- streamlit/static/static/js/index.C32I2PUe.js +0 -2
- streamlit/static/static/js/index.C5GnDRB7.js +0 -1
- streamlit/static/static/js/index.CG4qPaaW.js +0 -2
- streamlit/static/static/js/index.C_msmT1u.js +0 -1
- streamlit/static/static/js/index.CbeNTdd6.js +0 -1
- streamlit/static/static/js/index.CnGQVJcw.js +0 -12
- streamlit/static/static/js/index.CopVVq4l.js +0 -1
- streamlit/static/static/js/index.CtXupx4d.js +0 -197
- streamlit/static/static/js/index.DGmCchO7.js +0 -1
- streamlit/static/static/js/index.DH6zBk0e.js +0 -1
- streamlit/static/static/js/index.DHVlVWsm.js +0 -1
- streamlit/static/static/js/index.DRKIVBoi.js +0 -1
- streamlit/static/static/js/index.DUd-lFXx.js +0 -73
- streamlit/static/static/js/index.D_uRBA4B.js +0 -1
- streamlit/static/static/js/index.QHNfgPJd.js +0 -1
- streamlit/static/static/js/index.a-RJocYL.js +0 -1
- streamlit/static/static/js/index.cvz4B1gy.js +0 -1
- streamlit/static/static/js/index.t--hEgTQ.js +0 -6
- streamlit/static/static/js/useOnInputChange.z04u96A8.js +0 -1
- streamlit/static/static/media/SourceCodePro-Bold.CFEfr7-q.woff2 +0 -0
- streamlit/static/static/media/SourceCodePro-BoldItalic.C-LkFXxa.woff2 +0 -0
- streamlit/static/static/media/SourceCodePro-Italic.CxFOx7N-.woff2 +0 -0
- streamlit/static/static/media/SourceCodePro-Regular.CBOlD63d.woff2 +0 -0
- streamlit/static/static/media/SourceCodePro-SemiBold.CFHwW3Wd.woff2 +0 -0
- streamlit/static/static/media/SourceCodePro-SemiBoldItalic.Cg2yRu82.woff2 +0 -0
- streamlit/static/static/media/SourceSansPro-Bold.-6c9oR8J.woff2 +0 -0
- streamlit/static/static/media/SourceSansPro-BoldItalic.DmM_grLY.woff2 +0 -0
- streamlit/static/static/media/SourceSansPro-Italic.I1ipWe7Q.woff2 +0 -0
- streamlit/static/static/media/SourceSansPro-Regular.DZLUzqI4.woff2 +0 -0
- streamlit/static/static/media/SourceSansPro-SemiBold.sKQIyTMz.woff2 +0 -0
- streamlit/static/static/media/SourceSansPro-SemiBoldItalic.C0wP0icr.woff2 +0 -0
- streamlit/static/static/media/SourceSerifPro-Bold.8TUnKj4x.woff2 +0 -0
- streamlit/static/static/media/SourceSerifPro-BoldItalic.CBVO7Ve7.woff2 +0 -0
- streamlit/static/static/media/SourceSerifPro-Italic.DkFgL2HZ.woff2 +0 -0
- streamlit/static/static/media/SourceSerifPro-Regular.CNJNET2S.woff2 +0 -0
- streamlit/static/static/media/SourceSerifPro-SemiBold.CHyh9GC5.woff2 +0 -0
- streamlit/static/static/media/SourceSerifPro-SemiBoldItalic.CBtz8sWN.woff2 +0 -0
- streamlit-1.45.1.dist-info/RECORD +0 -568
- {streamlit-1.45.1.data → streamlit-1.46.1.data}/scripts/streamlit.cmd +0 -0
- {streamlit-1.45.1.dist-info → streamlit-1.46.1.dist-info}/entry_points.txt +0 -0
- {streamlit-1.45.1.dist-info → streamlit-1.46.1.dist-info}/top_level.txt +0 -0
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
|
-
from typing import Any
|
|
17
|
+
from typing import Any, Final, cast
|
|
18
18
|
from urllib.parse import urlparse
|
|
19
19
|
|
|
20
20
|
import tornado.web
|
|
@@ -26,10 +26,13 @@ from streamlit.auth_util import (
|
|
|
26
26
|
get_secrets_auth_section,
|
|
27
27
|
)
|
|
28
28
|
from streamlit.errors import StreamlitAuthError
|
|
29
|
+
from streamlit.logger import get_logger
|
|
29
30
|
from streamlit.url_util import make_url_path
|
|
30
31
|
from streamlit.web.server.oidc_mixin import TornadoOAuth, TornadoOAuth2App
|
|
31
32
|
from streamlit.web.server.server_util import AUTH_COOKIE_NAME
|
|
32
33
|
|
|
34
|
+
_LOGGER: Final = get_logger(__name__)
|
|
35
|
+
|
|
33
36
|
auth_cache = AuthCache()
|
|
34
37
|
|
|
35
38
|
|
|
@@ -57,7 +60,7 @@ def create_oauth_client(provider: str) -> tuple[TornadoOAuth2App, str]:
|
|
|
57
60
|
|
|
58
61
|
oauth = TornadoOAuth(config, cache=auth_cache)
|
|
59
62
|
oauth.register(provider)
|
|
60
|
-
return oauth.create_client(provider), redirect_uri
|
|
63
|
+
return oauth.create_client(provider), redirect_uri # type: ignore[no-untyped-call]
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
class AuthHandlerMixin(tornado.web.RequestHandler):
|
|
@@ -71,6 +74,13 @@ class AuthHandlerMixin(tornado.web.RequestHandler):
|
|
|
71
74
|
|
|
72
75
|
def set_auth_cookie(self, user_info: dict[str, Any]) -> None:
|
|
73
76
|
serialized_cookie_value = json.dumps(user_info)
|
|
77
|
+
|
|
78
|
+
# log error if cookie value is larger than 4096 bytes
|
|
79
|
+
if len(serialized_cookie_value.encode()) > 4096:
|
|
80
|
+
_LOGGER.error(
|
|
81
|
+
"Authentication cookie size exceeds maximum browser limit of 4096 bytes. Authentication may fail."
|
|
82
|
+
)
|
|
83
|
+
|
|
74
84
|
try:
|
|
75
85
|
# We don't specify Tornado secure flag here because it leads to missing cookie on Safari.
|
|
76
86
|
# The OIDC flow should work only on secure context anyway (localhost or HTTPS),
|
|
@@ -92,7 +102,7 @@ class AuthHandlerMixin(tornado.web.RequestHandler):
|
|
|
92
102
|
|
|
93
103
|
|
|
94
104
|
class AuthLoginHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
95
|
-
async def get(self):
|
|
105
|
+
async def get(self) -> None:
|
|
96
106
|
"""Redirect to the OAuth provider login page."""
|
|
97
107
|
provider = self._parse_provider_token()
|
|
98
108
|
if provider is None:
|
|
@@ -107,9 +117,9 @@ class AuthLoginHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
|
107
117
|
|
|
108
118
|
def _parse_provider_token(self) -> str | None:
|
|
109
119
|
provider_token = self.get_argument("provider", None)
|
|
120
|
+
if provider_token is None:
|
|
121
|
+
return None
|
|
110
122
|
try:
|
|
111
|
-
if provider_token is None:
|
|
112
|
-
raise StreamlitAuthError("Missing provider token")
|
|
113
123
|
payload = decode_provider_token(provider_token)
|
|
114
124
|
except StreamlitAuthError:
|
|
115
125
|
return None
|
|
@@ -118,35 +128,57 @@ class AuthLoginHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
|
118
128
|
|
|
119
129
|
|
|
120
130
|
class AuthLogoutHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
121
|
-
def get(self):
|
|
131
|
+
def get(self) -> None:
|
|
122
132
|
self.clear_auth_cookie()
|
|
123
133
|
self.redirect_to_base()
|
|
124
134
|
|
|
125
135
|
|
|
126
136
|
class AuthCallbackHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
127
|
-
async def get(self):
|
|
137
|
+
async def get(self) -> None:
|
|
128
138
|
provider = self._get_provider_by_state()
|
|
129
139
|
origin = self._get_origin_from_secrets()
|
|
130
140
|
if origin is None:
|
|
141
|
+
_LOGGER.error(
|
|
142
|
+
"Error, misconfigured origin for `redirect_uri` in secrets. ",
|
|
143
|
+
)
|
|
131
144
|
self.redirect_to_base()
|
|
132
145
|
return
|
|
133
146
|
|
|
134
147
|
error = self.get_argument("error", None)
|
|
135
148
|
if error:
|
|
149
|
+
error_description = self.get_argument("error_description", None)
|
|
150
|
+
sanitized_error = error.replace("\n", "").replace("\r", "")
|
|
151
|
+
sanitized_error_description = (
|
|
152
|
+
error_description.replace("\n", "").replace("\r", "")
|
|
153
|
+
if error_description
|
|
154
|
+
else None
|
|
155
|
+
)
|
|
156
|
+
_LOGGER.error(
|
|
157
|
+
"Error during authentication: %s. Error description: %s",
|
|
158
|
+
sanitized_error,
|
|
159
|
+
sanitized_error_description,
|
|
160
|
+
)
|
|
136
161
|
self.redirect_to_base()
|
|
137
162
|
return
|
|
138
163
|
|
|
139
164
|
if provider is None:
|
|
165
|
+
_LOGGER.error(
|
|
166
|
+
"Error, missing provider for oauth callback.",
|
|
167
|
+
)
|
|
140
168
|
self.redirect_to_base()
|
|
141
169
|
return
|
|
142
170
|
|
|
143
171
|
client, _ = create_oauth_client(provider)
|
|
144
172
|
token = client.authorize_access_token(self)
|
|
145
|
-
user = token.get("userinfo")
|
|
173
|
+
user = cast("dict[str, Any]", token.get("userinfo"))
|
|
146
174
|
|
|
147
175
|
cookie_value = dict(user, origin=origin, is_logged_in=True)
|
|
148
176
|
if user:
|
|
149
177
|
self.set_auth_cookie(cookie_value)
|
|
178
|
+
else:
|
|
179
|
+
_LOGGER.error(
|
|
180
|
+
"Error, missing user info.",
|
|
181
|
+
)
|
|
150
182
|
self.redirect_to_base()
|
|
151
183
|
|
|
152
184
|
def _get_provider_by_state(self) -> str | None:
|
|
@@ -157,7 +189,7 @@ class AuthCallbackHandler(AuthHandlerMixin, tornado.web.RequestHandler):
|
|
|
157
189
|
_, _, recorded_provider, code = key.split("_")
|
|
158
190
|
state_provider_mapping[code] = recorded_provider
|
|
159
191
|
|
|
160
|
-
provider: str | None = state_provider_mapping.get(state_code_from_url
|
|
192
|
+
provider: str | None = state_provider_mapping.get(state_code_from_url)
|
|
161
193
|
return provider
|
|
162
194
|
|
|
163
195
|
def _get_origin_from_secrets(self) -> str | None:
|
|
@@ -12,34 +12,48 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# mypy: disable-error-code="no-untyped-call"
|
|
16
|
+
# ruff: noqa: ANN201
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Callable, cast
|
|
21
|
+
|
|
22
|
+
from authlib.integrations.base_client import (
|
|
17
23
|
BaseApp,
|
|
18
24
|
BaseOAuth,
|
|
19
25
|
OAuth2Mixin,
|
|
20
26
|
OAuthError,
|
|
21
27
|
OpenIDMixin,
|
|
22
28
|
)
|
|
23
|
-
from authlib.integrations.requests_client import (
|
|
29
|
+
from authlib.integrations.requests_client import (
|
|
24
30
|
OAuth2Session,
|
|
25
31
|
)
|
|
26
32
|
|
|
27
33
|
from streamlit.web.server.authlib_tornado_integration import TornadoIntegration
|
|
28
34
|
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
import tornado.web
|
|
37
|
+
|
|
38
|
+
from streamlit.auth_util import AuthCache
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
|
|
41
|
+
class TornadoOAuth2App(OAuth2Mixin, OpenIDMixin, BaseApp):
|
|
31
42
|
client_cls = OAuth2Session
|
|
32
43
|
|
|
33
|
-
def load_server_metadata(self):
|
|
44
|
+
def load_server_metadata(self) -> dict[str, Any]:
|
|
34
45
|
"""We enforce S256 code challenge method if it is supported by the server."""
|
|
35
|
-
result = super().load_server_metadata()
|
|
46
|
+
result = cast("dict[str, Any]", super().load_server_metadata())
|
|
36
47
|
if "S256" in result.get("code_challenge_methods_supported", []):
|
|
37
48
|
self.client_kwargs["code_challenge_method"] = "S256"
|
|
38
49
|
return result
|
|
39
50
|
|
|
40
51
|
def authorize_redirect(
|
|
41
|
-
self,
|
|
42
|
-
|
|
52
|
+
self,
|
|
53
|
+
request_handler: tornado.web.RequestHandler,
|
|
54
|
+
redirect_uri: Any = None,
|
|
55
|
+
**kwargs: Any,
|
|
56
|
+
) -> None:
|
|
43
57
|
"""Create a HTTP Redirect for Authorization Endpoint.
|
|
44
58
|
|
|
45
59
|
:param request_handler: HTTP request instance from Tornado.
|
|
@@ -52,8 +66,8 @@ class TornadoOAuth2App(OAuth2Mixin, OpenIDMixin, BaseApp): # type: ignore[misc]
|
|
|
52
66
|
request_handler.redirect(auth_context["url"], status=302)
|
|
53
67
|
|
|
54
68
|
def authorize_access_token(
|
|
55
|
-
self, request_handler: tornado.web.RequestHandler, **kwargs
|
|
56
|
-
):
|
|
69
|
+
self, request_handler: tornado.web.RequestHandler, **kwargs: Any
|
|
70
|
+
) -> dict[str, Any]:
|
|
57
71
|
"""
|
|
58
72
|
:param request_handler: HTTP request instance from Tornado.
|
|
59
73
|
:return: A token dict.
|
|
@@ -68,13 +82,12 @@ class TornadoOAuth2App(OAuth2Mixin, OpenIDMixin, BaseApp): # type: ignore[misc]
|
|
|
68
82
|
"state": request_handler.get_argument("state"),
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
assert self.framework.cache is not None
|
|
72
85
|
session = None
|
|
73
86
|
|
|
74
87
|
claims_options = kwargs.pop("claims_options", None)
|
|
75
88
|
state_data = self.framework.get_state_data(session, params.get("state"))
|
|
76
89
|
self.framework.clear_state_data(session, params.get("state"))
|
|
77
|
-
params = self._format_state_params(state_data, params)
|
|
90
|
+
params = self._format_state_params(state_data, params) # type: ignore[attr-defined]
|
|
78
91
|
token = self.fetch_access_token(**params, **kwargs)
|
|
79
92
|
|
|
80
93
|
if "id_token" in token and "nonce" in state_data:
|
|
@@ -82,26 +95,31 @@ class TornadoOAuth2App(OAuth2Mixin, OpenIDMixin, BaseApp): # type: ignore[misc]
|
|
|
82
95
|
token, nonce=state_data["nonce"], claims_options=claims_options
|
|
83
96
|
)
|
|
84
97
|
token = {**token, "userinfo": userinfo}
|
|
85
|
-
return token
|
|
98
|
+
return cast("dict[str, Any]", token)
|
|
86
99
|
|
|
87
|
-
def _save_authorize_data(self, **kwargs):
|
|
100
|
+
def _save_authorize_data(self, **kwargs: Any) -> None:
|
|
88
101
|
"""Authlib underlying uses the concept of "session" to store state data.
|
|
89
102
|
In Tornado, we don't have a session, so we use the framework's cache option.
|
|
90
103
|
"""
|
|
91
104
|
state = kwargs.pop("state", None)
|
|
92
105
|
if state:
|
|
93
|
-
assert self.framework.cache is not None
|
|
94
106
|
session = None
|
|
95
107
|
self.framework.set_state_data(session, state, kwargs)
|
|
96
108
|
else:
|
|
97
109
|
raise RuntimeError("Missing state value")
|
|
98
110
|
|
|
99
111
|
|
|
100
|
-
class TornadoOAuth(BaseOAuth):
|
|
112
|
+
class TornadoOAuth(BaseOAuth):
|
|
101
113
|
oauth2_client_cls = TornadoOAuth2App
|
|
102
114
|
framework_integration_cls = TornadoIntegration
|
|
103
115
|
|
|
104
|
-
def __init__(
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
config: dict[str, Any] | None = None,
|
|
119
|
+
cache: AuthCache | None = None,
|
|
120
|
+
fetch_token: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
|
|
121
|
+
update_token: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
|
|
122
|
+
):
|
|
105
123
|
super().__init__(
|
|
106
124
|
cache=cache, fetch_token=fetch_token, update_token=update_token
|
|
107
125
|
)
|
streamlit/web/server/routes.py
CHANGED
|
@@ -15,40 +15,48 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import os
|
|
18
|
-
from typing import TYPE_CHECKING
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Callable, cast
|
|
19
19
|
|
|
20
20
|
import tornado.web
|
|
21
21
|
|
|
22
22
|
from streamlit import config, file_util
|
|
23
23
|
from streamlit.web.server.server_util import (
|
|
24
|
+
allowlisted_origins,
|
|
24
25
|
emit_endpoint_deprecation_notice,
|
|
25
26
|
is_xsrf_enabled,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING:
|
|
29
|
-
from collections.abc import Sequence
|
|
30
|
+
from collections.abc import Awaitable, Sequence
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def
|
|
33
|
-
"""True if cross-origin requests are allowed.
|
|
34
|
-
|
|
35
|
-
We only allow cross-origin requests when CORS protection has been disabled
|
|
36
|
-
with server.enableCORS=False or if using the Node server. When using the
|
|
37
|
-
Node server, we have a dev and prod port, which count as two origins.
|
|
33
|
+
def allow_all_cross_origin_requests() -> bool:
|
|
34
|
+
"""True if cross-origin requests from any origin are allowed.
|
|
38
35
|
|
|
36
|
+
We only allow ALL cross-origin requests when CORS protection has been
|
|
37
|
+
disabled with server.enableCORS=False or if using the Node server in dev
|
|
38
|
+
mode. When in dev mode, we have a dev and prod port, which count as two
|
|
39
|
+
origins.
|
|
39
40
|
"""
|
|
41
|
+
|
|
40
42
|
return not config.get_option("server.enableCORS") or config.get_option(
|
|
41
43
|
"global.developmentMode"
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
def is_allowed_origin(origin: Any) -> bool:
|
|
48
|
+
if not isinstance(origin, str):
|
|
49
|
+
return False
|
|
50
|
+
return origin in allowlisted_origins()
|
|
51
|
+
|
|
52
|
+
|
|
45
53
|
class StaticFileHandler(tornado.web.StaticFileHandler):
|
|
46
54
|
def initialize(
|
|
47
55
|
self,
|
|
48
56
|
path: str,
|
|
49
57
|
default_filename: str | None = None,
|
|
50
58
|
reserved_paths: Sequence[str] = (),
|
|
51
|
-
):
|
|
59
|
+
) -> None:
|
|
52
60
|
self._reserved_paths = reserved_paths
|
|
53
61
|
|
|
54
62
|
super().initialize(path, default_filename)
|
|
@@ -78,15 +86,15 @@ class StaticFileHandler(tornado.web.StaticFileHandler):
|
|
|
78
86
|
if os.path.sep != "/":
|
|
79
87
|
url_path = url_path.replace(os.path.sep, "/")
|
|
80
88
|
if any(url_path.endswith(x) for x in self._reserved_paths):
|
|
81
|
-
raise
|
|
89
|
+
raise
|
|
82
90
|
|
|
83
91
|
self.path = self.parse_url_path(self.default_filename or "index.html")
|
|
84
92
|
absolute_path = self.get_absolute_path(self.root, self.path)
|
|
85
93
|
return super().validate_absolute_path(root, absolute_path)
|
|
86
94
|
|
|
87
|
-
raise
|
|
95
|
+
raise
|
|
88
96
|
|
|
89
|
-
def write_error(self, status_code: int, **kwargs) -> None:
|
|
97
|
+
def write_error(self, status_code: int, **kwargs: Any) -> None:
|
|
90
98
|
if status_code == 404:
|
|
91
99
|
index_file = os.path.join(file_util.get_static_dir(), "index.html")
|
|
92
100
|
self.render(index_file)
|
|
@@ -96,25 +104,27 @@ class StaticFileHandler(tornado.web.StaticFileHandler):
|
|
|
96
104
|
|
|
97
105
|
class AddSlashHandler(tornado.web.RequestHandler):
|
|
98
106
|
@tornado.web.addslash
|
|
99
|
-
def get(self):
|
|
107
|
+
def get(self) -> None:
|
|
100
108
|
pass
|
|
101
109
|
|
|
102
110
|
|
|
103
111
|
class RemoveSlashHandler(tornado.web.RequestHandler):
|
|
104
112
|
@tornado.web.removeslash
|
|
105
|
-
def get(self):
|
|
113
|
+
def get(self) -> None:
|
|
106
114
|
pass
|
|
107
115
|
|
|
108
116
|
|
|
109
117
|
class _SpecialRequestHandler(tornado.web.RequestHandler):
|
|
110
118
|
"""Superclass for "special" endpoints, like /healthz."""
|
|
111
119
|
|
|
112
|
-
def set_default_headers(self):
|
|
120
|
+
def set_default_headers(self) -> None:
|
|
113
121
|
self.set_header("Cache-Control", "no-cache")
|
|
114
|
-
if
|
|
122
|
+
if allow_all_cross_origin_requests():
|
|
115
123
|
self.set_header("Access-Control-Allow-Origin", "*")
|
|
124
|
+
elif is_allowed_origin(origin := self.request.headers.get("Origin")):
|
|
125
|
+
self.set_header("Access-Control-Allow-Origin", cast("str", origin))
|
|
116
126
|
|
|
117
|
-
def options(self):
|
|
127
|
+
def options(self) -> None:
|
|
118
128
|
"""/OPTIONS handler for preflight CORS checks.
|
|
119
129
|
|
|
120
130
|
When a browser is making a CORS request, it may sometimes first
|
|
@@ -136,7 +146,7 @@ class _SpecialRequestHandler(tornado.web.RequestHandler):
|
|
|
136
146
|
|
|
137
147
|
|
|
138
148
|
class HealthHandler(_SpecialRequestHandler):
|
|
139
|
-
def initialize(self, callback):
|
|
149
|
+
def initialize(self, callback: Callable[[], Awaitable[tuple[bool, str]]]) -> None:
|
|
140
150
|
"""Initialize the handler.
|
|
141
151
|
|
|
142
152
|
Parameters
|
|
@@ -147,15 +157,15 @@ class HealthHandler(_SpecialRequestHandler):
|
|
|
147
157
|
"""
|
|
148
158
|
self._callback = callback
|
|
149
159
|
|
|
150
|
-
async def get(self):
|
|
160
|
+
async def get(self) -> None:
|
|
151
161
|
await self.handle_request()
|
|
152
162
|
|
|
153
163
|
# Some monitoring services only support the HTTP HEAD method for requests to
|
|
154
164
|
# healthcheck endpoints, so we support HEAD as well to play nicely with them.
|
|
155
|
-
async def head(self):
|
|
165
|
+
async def head(self) -> None:
|
|
156
166
|
await self.handle_request()
|
|
157
167
|
|
|
158
|
-
async def handle_request(self):
|
|
168
|
+
async def handle_request(self) -> None:
|
|
159
169
|
if self.request.uri and "_stcore/" not in self.request.uri:
|
|
160
170
|
new_path = (
|
|
161
171
|
"/_stcore/script-health-check"
|
|
@@ -212,7 +222,7 @@ _DEFAULT_ALLOWED_MESSAGE_ORIGINS = [
|
|
|
212
222
|
|
|
213
223
|
|
|
214
224
|
class HostConfigHandler(_SpecialRequestHandler):
|
|
215
|
-
def initialize(self):
|
|
225
|
+
def initialize(self) -> None:
|
|
216
226
|
# Make a copy of the allowedOrigins list, since we might modify it later:
|
|
217
227
|
self._allowed_origins = _DEFAULT_ALLOWED_MESSAGE_ORIGINS.copy()
|
|
218
228
|
|
streamlit/web/server/server.py
CHANGED
|
@@ -53,8 +53,8 @@ from streamlit.web.server.routes import (
|
|
|
53
53
|
StaticFileHandler,
|
|
54
54
|
)
|
|
55
55
|
from streamlit.web.server.server_util import (
|
|
56
|
-
DEVELOPMENT_PORT,
|
|
57
56
|
get_cookie_secret,
|
|
57
|
+
is_tornado_version_less_than,
|
|
58
58
|
is_xsrf_enabled,
|
|
59
59
|
make_url_path_regex,
|
|
60
60
|
)
|
|
@@ -70,12 +70,12 @@ _LOGGER: Final = get_logger(__name__)
|
|
|
70
70
|
TORNADO_SETTINGS = {
|
|
71
71
|
# Gzip HTTP responses.
|
|
72
72
|
"compress_response": True,
|
|
73
|
-
# Ping every
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
"websocket_ping_interval": 1,
|
|
73
|
+
# Ping every 30s to keep WS alive.
|
|
74
|
+
# With recent versions of Tornado, this value must be greater than or
|
|
75
|
+
# equal to websocket_ping_timeout.
|
|
76
|
+
# For details, see https://github.com/tornadoweb/tornado/pull/3376
|
|
77
|
+
# For compatibility with older versions of Tornado, we set the value to 1.
|
|
78
|
+
"websocket_ping_interval": 1 if is_tornado_version_less_than("6.5.0") else 30,
|
|
79
79
|
# If we don't get a ping response within 30s, the connection
|
|
80
80
|
# is timed out.
|
|
81
81
|
"websocket_ping_timeout": 30,
|
|
@@ -112,7 +112,7 @@ AUTH_LOGIN_ENDPOINT: Final = "/auth/login"
|
|
|
112
112
|
AUTH_LOGOUT_ENDPOINT: Final = "/auth/logout"
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
class
|
|
115
|
+
class RetriesExceededError(Exception):
|
|
116
116
|
pass
|
|
117
117
|
|
|
118
118
|
|
|
@@ -175,7 +175,7 @@ def _get_ssl_options(cert_file: str | None, key_file: str | None) -> SSLContext
|
|
|
175
175
|
try:
|
|
176
176
|
ssl_ctx.load_cert_chain(cert_file, key_file)
|
|
177
177
|
except ssl.SSLError:
|
|
178
|
-
_LOGGER.
|
|
178
|
+
_LOGGER.exception(
|
|
179
179
|
"Failed to load SSL certificate. Make sure "
|
|
180
180
|
"cert file '%s' and key file '%s' are correct.",
|
|
181
181
|
cert_file,
|
|
@@ -203,14 +203,6 @@ def start_listening_tcp_socket(http_server: HTTPServer) -> None:
|
|
|
203
203
|
address = config.get_option("server.address")
|
|
204
204
|
port = config.get_option("server.port")
|
|
205
205
|
|
|
206
|
-
if int(port) == DEVELOPMENT_PORT:
|
|
207
|
-
_LOGGER.warning(
|
|
208
|
-
"Port %s is reserved for internal development. "
|
|
209
|
-
"It is strongly recommended to select an alternative port "
|
|
210
|
-
"for `server.port`.",
|
|
211
|
-
DEVELOPMENT_PORT,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
206
|
try:
|
|
215
207
|
http_server.listen(port, address)
|
|
216
208
|
break # It worked! So let's break out of the loop.
|
|
@@ -218,16 +210,13 @@ def start_listening_tcp_socket(http_server: HTTPServer) -> None:
|
|
|
218
210
|
except OSError as e:
|
|
219
211
|
if e.errno == errno.EADDRINUSE:
|
|
220
212
|
if server_port_is_manually_set():
|
|
221
|
-
_LOGGER.error("Port %s is already in use", port)
|
|
213
|
+
_LOGGER.error("Port %s is already in use", port) # noqa: TRY400
|
|
222
214
|
sys.exit(1)
|
|
223
215
|
else:
|
|
224
216
|
_LOGGER.debug(
|
|
225
217
|
"Port %s already in use, trying to use the next one.", port
|
|
226
218
|
)
|
|
227
219
|
port += 1
|
|
228
|
-
# Don't use the development port here:
|
|
229
|
-
if port == DEVELOPMENT_PORT:
|
|
230
|
-
port += 1
|
|
231
220
|
|
|
232
221
|
config.set_option(
|
|
233
222
|
"server.port", port, ConfigOption.STREAMLIT_DEFINITION
|
|
@@ -237,14 +226,14 @@ def start_listening_tcp_socket(http_server: HTTPServer) -> None:
|
|
|
237
226
|
raise
|
|
238
227
|
|
|
239
228
|
if call_count >= MAX_PORT_SEARCH_RETRIES:
|
|
240
|
-
raise
|
|
229
|
+
raise RetriesExceededError(
|
|
241
230
|
f"Cannot start Streamlit server. Port {port} is already in use, and "
|
|
242
231
|
f"Streamlit was unable to find a free port after {MAX_PORT_SEARCH_RETRIES} attempts.",
|
|
243
232
|
)
|
|
244
233
|
|
|
245
234
|
|
|
246
235
|
class Server:
|
|
247
|
-
def __init__(self, main_script_path: str, is_hello: bool):
|
|
236
|
+
def __init__(self, main_script_path: str, is_hello: bool) -> None:
|
|
248
237
|
"""Create the server. It won't be started yet."""
|
|
249
238
|
_set_tornado_log_levels()
|
|
250
239
|
self.initialize_mimetypes()
|
|
@@ -428,7 +417,7 @@ class Server:
|
|
|
428
417
|
make_url_path_regex(base, "(.*)"),
|
|
429
418
|
StaticFileHandler,
|
|
430
419
|
{
|
|
431
|
-
"path": "
|
|
420
|
+
"path": f"{static_path}/",
|
|
432
421
|
"default_filename": "index.html",
|
|
433
422
|
"reserved_paths": [
|
|
434
423
|
# These paths are required for identifying
|
|
@@ -16,21 +16,49 @@
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
from typing import TYPE_CHECKING, Callable, Final, Literal
|
|
19
|
+
from typing import TYPE_CHECKING, Callable, Final, Literal, cast
|
|
20
20
|
from urllib.parse import urljoin
|
|
21
21
|
|
|
22
22
|
from streamlit import config, net_util, url_util
|
|
23
23
|
from streamlit.runtime.secrets import secrets_singleton
|
|
24
|
+
from streamlit.type_util import is_version_less_than
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
26
27
|
from tornado.web import RequestHandler
|
|
27
28
|
|
|
28
|
-
# The port
|
|
29
|
+
# The port used for internal development.
|
|
29
30
|
DEVELOPMENT_PORT: Final = 3000
|
|
30
31
|
|
|
31
32
|
AUTH_COOKIE_NAME: Final = "_streamlit_user"
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def allowlisted_origins() -> set[str]:
|
|
36
|
+
return {origin.strip() for origin in config.get_option("server.corsAllowedOrigins")}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_tornado_version_less_than(v: str) -> bool:
|
|
40
|
+
"""Return True if the current Tornado version is less than the input version.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
v : str
|
|
45
|
+
Version string, e.g. "0.25.0"
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
InvalidVersion
|
|
55
|
+
If the version strings are not valid.
|
|
56
|
+
"""
|
|
57
|
+
import tornado
|
|
58
|
+
|
|
59
|
+
return is_version_less_than(tornado.version, v)
|
|
60
|
+
|
|
61
|
+
|
|
34
62
|
def is_url_from_allowed_origins(url: str) -> bool:
|
|
35
63
|
"""Return True if URL is from allowed origins (for CORS purpose).
|
|
36
64
|
|
|
@@ -47,10 +75,14 @@ def is_url_from_allowed_origins(url: str) -> bool:
|
|
|
47
75
|
|
|
48
76
|
hostname = url_util.get_hostname(url)
|
|
49
77
|
|
|
50
|
-
|
|
78
|
+
allowlisted_domains = [
|
|
79
|
+
url_util.get_hostname(origin) for origin in allowlisted_origins()
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
allowed_domains: list[str | None | Callable[[], str | None]] = [
|
|
51
83
|
# Check localhost first.
|
|
52
84
|
"localhost",
|
|
53
|
-
"0.0.0.0",
|
|
85
|
+
"0.0.0.0", # noqa: S104
|
|
54
86
|
"127.0.0.1",
|
|
55
87
|
# Try to avoid making unnecessary HTTP requests by checking if the user
|
|
56
88
|
# manually specified a server address.
|
|
@@ -58,6 +90,7 @@ def is_url_from_allowed_origins(url: str) -> bool:
|
|
|
58
90
|
# Then try the options that depend on HTTP requests or opening sockets.
|
|
59
91
|
net_util.get_internal_ip,
|
|
60
92
|
net_util.get_external_ip,
|
|
93
|
+
*allowlisted_domains,
|
|
61
94
|
]
|
|
62
95
|
|
|
63
96
|
for allowed_domain in allowed_domains:
|
|
@@ -87,12 +120,12 @@ def get_cookie_secret() -> str:
|
|
|
87
120
|
return cookie_secret
|
|
88
121
|
|
|
89
122
|
|
|
90
|
-
def is_xsrf_enabled():
|
|
123
|
+
def is_xsrf_enabled() -> bool:
|
|
91
124
|
csrf_enabled = config.get_option("server.enableXsrfProtection")
|
|
92
125
|
if not csrf_enabled and secrets_singleton.load_if_toml_exists():
|
|
93
126
|
auth_section = secrets_singleton.get("auth", None)
|
|
94
127
|
csrf_enabled = csrf_enabled or auth_section is not None
|
|
95
|
-
return csrf_enabled
|
|
128
|
+
return cast("bool", csrf_enabled)
|
|
96
129
|
|
|
97
130
|
|
|
98
131
|
def _get_server_address_if_manually_set() -> str | None:
|
|
@@ -102,17 +135,18 @@ def _get_server_address_if_manually_set() -> str | None:
|
|
|
102
135
|
|
|
103
136
|
|
|
104
137
|
def make_url_path_regex(
|
|
105
|
-
*path
|
|
138
|
+
*path: str,
|
|
139
|
+
trailing_slash: Literal["optional", "required", "prohibited"] = "optional",
|
|
106
140
|
) -> str:
|
|
107
141
|
"""Get a regex of the form ^/foo/bar/baz/?$ for a path (foo, bar, baz)."""
|
|
108
|
-
|
|
142
|
+
filtered_paths = [x.strip("/") for x in path if x] # Filter out falsely components.
|
|
109
143
|
path_format = r"^/%s$"
|
|
110
144
|
if trailing_slash == "optional":
|
|
111
145
|
path_format = r"^/%s/?$"
|
|
112
146
|
elif trailing_slash == "required":
|
|
113
147
|
path_format = r"^/%s/$"
|
|
114
148
|
|
|
115
|
-
return path_format % "/".join(
|
|
149
|
+
return path_format % "/".join(filtered_paths)
|
|
116
150
|
|
|
117
151
|
|
|
118
152
|
def get_url(host_ip: str) -> str:
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
from typing import TYPE_CHECKING
|
|
17
|
+
from typing import TYPE_CHECKING, cast
|
|
18
18
|
|
|
19
19
|
import tornado.web
|
|
20
20
|
|
|
21
|
-
from streamlit.web.server import
|
|
21
|
+
from streamlit.web.server import allow_all_cross_origin_requests, is_allowed_origin
|
|
22
22
|
from streamlit.web.server.server_util import emit_endpoint_deprecation_notice
|
|
23
23
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
@@ -30,11 +30,13 @@ class StatsRequestHandler(tornado.web.RequestHandler):
|
|
|
30
30
|
def initialize(self, stats_manager: StatsManager) -> None:
|
|
31
31
|
self._manager = stats_manager
|
|
32
32
|
|
|
33
|
-
def set_default_headers(self):
|
|
34
|
-
if
|
|
33
|
+
def set_default_headers(self) -> None:
|
|
34
|
+
if allow_all_cross_origin_requests():
|
|
35
35
|
self.set_header("Access-Control-Allow-Origin", "*")
|
|
36
|
+
elif is_allowed_origin(origin := self.request.headers.get("Origin")):
|
|
37
|
+
self.set_header("Access-Control-Allow-Origin", cast("str", origin))
|
|
36
38
|
|
|
37
|
-
def options(self):
|
|
39
|
+
def options(self) -> None:
|
|
38
40
|
"""/OPTIONS handler for preflight CORS checks."""
|
|
39
41
|
self.set_status(204)
|
|
40
42
|
self.finish()
|