streamlit 1.53.1__py3-none-any.whl → 1.54.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.
Files changed (309) hide show
  1. streamlit/__init__.py +1 -31
  2. streamlit/auth_util.py +91 -2
  3. streamlit/cli_util.py +3 -2
  4. streamlit/commands/echo.py +2 -2
  5. streamlit/commands/execution_control.py +1 -1
  6. streamlit/commands/logo.py +76 -24
  7. streamlit/commands/navigation.py +1 -1
  8. streamlit/components/types/base_custom_component.py +0 -2
  9. streamlit/components/v1/custom_component.py +0 -2
  10. streamlit/components/v2/bidi_component/main.py +2 -2
  11. streamlit/components/v2/component_path_utils.py +17 -29
  12. streamlit/components/v2/manifest_scanner.py +8 -3
  13. streamlit/components/v2/presentation.py +1 -1
  14. streamlit/config.py +57 -13
  15. streamlit/config_util.py +5 -5
  16. streamlit/connections/snowflake_connection.py +5 -3
  17. streamlit/dataframe_util.py +10 -10
  18. streamlit/deprecation_util.py +19 -1
  19. streamlit/elements/arrow.py +18 -8
  20. streamlit/elements/deck_gl_json_chart.py +6 -2
  21. streamlit/elements/exception.py +4 -2
  22. streamlit/elements/form.py +1 -1
  23. streamlit/elements/layouts.py +1 -1
  24. streamlit/elements/lib/built_in_chart_utils.py +36 -13
  25. streamlit/elements/lib/color_util.py +21 -2
  26. streamlit/elements/lib/column_config_utils.py +9 -7
  27. streamlit/elements/lib/dialog.py +1 -1
  28. streamlit/elements/lib/image_utils.py +5 -5
  29. streamlit/elements/lib/layout_utils.py +1 -1
  30. streamlit/elements/lib/options_selector_utils.py +72 -22
  31. streamlit/elements/lib/policies.py +1 -1
  32. streamlit/elements/lib/streamlit_plotly_theme.py +9 -11
  33. streamlit/elements/lib/utils.py +1 -1
  34. streamlit/elements/map.py +6 -6
  35. streamlit/elements/plotly_chart.py +2 -2
  36. streamlit/elements/toast.py +1 -1
  37. streamlit/elements/vega_charts.py +30 -7
  38. streamlit/elements/widgets/button.py +3 -3
  39. streamlit/elements/widgets/button_group.py +3 -3
  40. streamlit/elements/widgets/chat.py +1 -1
  41. streamlit/elements/widgets/data_editor.py +6 -6
  42. streamlit/elements/widgets/multiselect.py +1 -1
  43. streamlit/elements/widgets/number_input.py +1 -1
  44. streamlit/elements/widgets/radio.py +91 -31
  45. streamlit/elements/widgets/select_slider.py +123 -37
  46. streamlit/elements/widgets/slider.py +5 -5
  47. streamlit/elements/widgets/time_widgets.py +150 -18
  48. streamlit/elements/write.py +2 -3
  49. streamlit/env_util.py +1 -1
  50. streamlit/errors.py +2 -14
  51. streamlit/external/langchain/streamlit_callback_handler.py +1 -1
  52. streamlit/hello/dataframe_demo.py +1 -1
  53. streamlit/hello/plotting_demo.py +19 -12
  54. streamlit/path_security.py +98 -0
  55. streamlit/proto/Alert_pb2.py +2 -3
  56. streamlit/proto/AppPage_pb2.py +2 -3
  57. streamlit/proto/ArrowData_pb2.py +2 -3
  58. streamlit/proto/ArrowNamedDataSet_pb2.py +2 -3
  59. streamlit/proto/ArrowVegaLiteChart_pb2.py +2 -3
  60. streamlit/proto/Arrow_pb2.py +2 -3
  61. streamlit/proto/AudioInput_pb2.py +2 -3
  62. streamlit/proto/Audio_pb2.py +2 -3
  63. streamlit/proto/AuthRedirect_pb2.py +2 -3
  64. streamlit/proto/AutoRerun_pb2.py +2 -3
  65. streamlit/proto/BackMsg_pb2.py +2 -3
  66. streamlit/proto/Balloons_pb2.py +2 -3
  67. streamlit/proto/BidiComponent_pb2.py +2 -3
  68. streamlit/proto/Block_pb2.py +2 -3
  69. streamlit/proto/BokehChart_pb2.py +2 -3
  70. streamlit/proto/ButtonGroup_pb2.py +2 -3
  71. streamlit/proto/ButtonLikeIconPosition_pb2.py +2 -3
  72. streamlit/proto/Button_pb2.py +2 -3
  73. streamlit/proto/CameraInput_pb2.py +2 -3
  74. streamlit/proto/ChatInput_pb2.py +2 -3
  75. streamlit/proto/Checkbox_pb2.py +2 -3
  76. streamlit/proto/ClientState_pb2.py +2 -3
  77. streamlit/proto/Code_pb2.py +2 -3
  78. streamlit/proto/ColorPicker_pb2.py +2 -3
  79. streamlit/proto/Common_pb2.py +2 -3
  80. streamlit/proto/Components_pb2.py +2 -3
  81. streamlit/proto/DataFrame_pb2.py +2 -3
  82. streamlit/proto/DateInput_pb2.py +2 -3
  83. streamlit/proto/DateTimeInput_pb2.py +2 -3
  84. streamlit/proto/DeckGlJsonChart_pb2.py +2 -3
  85. streamlit/proto/Delta_pb2.py +2 -3
  86. streamlit/proto/DocString_pb2.py +2 -3
  87. streamlit/proto/DownloadButton_pb2.py +2 -3
  88. streamlit/proto/Element_pb2.py +2 -3
  89. streamlit/proto/Empty_pb2.py +2 -3
  90. streamlit/proto/Exception_pb2.py +2 -3
  91. streamlit/proto/Favicon_pb2.py +2 -3
  92. streamlit/proto/FileUploader_pb2.py +2 -3
  93. streamlit/proto/ForwardMsg_pb2.py +2 -3
  94. streamlit/proto/GapSize_pb2.py +2 -3
  95. streamlit/proto/GitInfo_pb2.py +2 -3
  96. streamlit/proto/GraphVizChart_pb2.py +2 -3
  97. streamlit/proto/Heading_pb2.py +2 -3
  98. streamlit/proto/HeightConfig_pb2.py +2 -3
  99. streamlit/proto/Html_pb2.py +2 -3
  100. streamlit/proto/IFrame_pb2.py +2 -3
  101. streamlit/proto/Image_pb2.py +2 -3
  102. streamlit/proto/Json_pb2.py +2 -3
  103. streamlit/proto/LabelVisibilityMessage_pb2.py +2 -3
  104. streamlit/proto/LinkButton_pb2.py +2 -3
  105. streamlit/proto/Logo_pb2.py +6 -5
  106. streamlit/proto/Logo_pb2.pyi +25 -1
  107. streamlit/proto/Markdown_pb2.py +2 -3
  108. streamlit/proto/Metric_pb2.py +2 -3
  109. streamlit/proto/MetricsEvent_pb2.py +2 -3
  110. streamlit/proto/MultiSelect_pb2.py +2 -3
  111. streamlit/proto/NamedDataSet_pb2.py +2 -3
  112. streamlit/proto/Navigation_pb2.py +2 -3
  113. streamlit/proto/NewSession_pb2.py +25 -24
  114. streamlit/proto/NewSession_pb2.pyi +28 -2
  115. streamlit/proto/NumberInput_pb2.py +2 -3
  116. streamlit/proto/PageConfig_pb2.py +2 -3
  117. streamlit/proto/PageInfo_pb2.py +2 -3
  118. streamlit/proto/PageLink_pb2.py +2 -3
  119. streamlit/proto/PageNotFound_pb2.py +2 -3
  120. streamlit/proto/PageProfile_pb2.py +2 -3
  121. streamlit/proto/PagesChanged_pb2.py +2 -3
  122. streamlit/proto/ParentMessage_pb2.py +2 -3
  123. streamlit/proto/PlotlyChart_pb2.py +2 -3
  124. streamlit/proto/Progress_pb2.py +2 -3
  125. streamlit/proto/Radio_pb2.py +5 -4
  126. streamlit/proto/Radio_pb2.pyi +20 -3
  127. streamlit/proto/RootContainer_pb2.py +2 -3
  128. streamlit/proto/Selectbox_pb2.py +2 -3
  129. streamlit/proto/SessionEvent_pb2.py +2 -3
  130. streamlit/proto/SessionStatus_pb2.py +2 -3
  131. streamlit/proto/Skeleton_pb2.py +2 -3
  132. streamlit/proto/Slider_pb2.py +7 -8
  133. streamlit/proto/Slider_pb2.pyi +9 -1
  134. streamlit/proto/Snow_pb2.py +2 -3
  135. streamlit/proto/Space_pb2.py +2 -3
  136. streamlit/proto/Spinner_pb2.py +2 -3
  137. streamlit/proto/TextAlignmentConfig_pb2.py +2 -3
  138. streamlit/proto/TextArea_pb2.py +2 -3
  139. streamlit/proto/TextInput_pb2.py +2 -3
  140. streamlit/proto/Text_pb2.py +2 -3
  141. streamlit/proto/TimeInput_pb2.py +2 -3
  142. streamlit/proto/Toast_pb2.py +2 -3
  143. streamlit/proto/Transient_pb2.py +2 -3
  144. streamlit/proto/VegaLiteChart_pb2.py +2 -3
  145. streamlit/proto/Video_pb2.py +2 -3
  146. streamlit/proto/WidgetStates_pb2.py +2 -3
  147. streamlit/proto/WidthConfig_pb2.py +2 -3
  148. streamlit/proto/openmetrics_data_model_pb2.py +2 -3
  149. streamlit/runtime/app_session.py +106 -60
  150. streamlit/runtime/caching/cache_data_api.py +3 -3
  151. streamlit/runtime/caching/cache_errors.py +0 -2
  152. streamlit/runtime/caching/cache_resource_api.py +1 -1
  153. streamlit/runtime/caching/cache_utils.py +2 -2
  154. streamlit/runtime/caching/hashing.py +1 -3
  155. streamlit/runtime/caching/storage/cache_storage_protocol.py +0 -3
  156. streamlit/runtime/connection_factory.py +1 -1
  157. streamlit/runtime/credentials.py +2 -2
  158. streamlit/runtime/metrics_util.py +3 -3
  159. streamlit/runtime/runtime.py +6 -6
  160. streamlit/runtime/scriptrunner/script_runner.py +17 -0
  161. streamlit/runtime/scriptrunner_utils/exceptions.py +0 -4
  162. streamlit/runtime/scriptrunner_utils/script_run_context.py +13 -31
  163. streamlit/runtime/secrets.py +3 -4
  164. streamlit/runtime/state/__init__.py +7 -1
  165. streamlit/runtime/state/common.py +13 -0
  166. streamlit/runtime/state/query_params.py +493 -24
  167. streamlit/runtime/state/session_state.py +179 -4
  168. streamlit/runtime/state/widgets.py +26 -1
  169. streamlit/runtime/stats.py +1 -10
  170. streamlit/static/index.html +1 -1
  171. streamlit/static/manifest.json +304 -304
  172. streamlit/static/static/js/{ErrorOutline.esm.CScZvf44.js → ErrorOutline.esm.BWk6F-Tz.js} +1 -1
  173. streamlit/static/static/js/{FileDownload.esm.COCxTZxP.js → FileDownload.esm.AllYUuOW.js} +1 -1
  174. streamlit/static/static/js/{FileHelper.Bhs-iVRI.js → FileHelper.BvVTNdmy.js} +1 -1
  175. streamlit/static/static/js/{FormClearHelper.CA_5b-Ut.js → FormClearHelper.C__r5Llk.js} +1 -1
  176. streamlit/static/static/js/{InputInstructions.Bzb0MCfv.js → InputInstructions.DOtkdOMV.js} +1 -1
  177. streamlit/static/static/js/Particles.DCsqQZlE.js +1 -0
  178. streamlit/static/static/js/{ProgressBar.DyQNhVsJ.js → ProgressBar.DLCRvt4m.js} +2 -2
  179. streamlit/static/static/js/{StreamlitSyntaxHighlighter.BOkJThtV.js → StreamlitSyntaxHighlighter.CYFWoZHb.js} +1 -1
  180. streamlit/static/static/js/{TableChart.esm.a60nntBC.js → TableChart.esm.D6ydHcIm.js} +1 -1
  181. streamlit/static/static/js/Toolbar.BHDNzWBx.js +1 -0
  182. streamlit/static/static/js/{WidgetLabelHelpIconInline.BjIku2ic.js → WidgetLabelHelpIconInline.DEXBrVlc.js} +1 -1
  183. streamlit/static/static/js/{base-input.avGkArOc.js → base-input.TSQjctlq.js} +4 -4
  184. streamlit/static/static/js/{checkbox.Q8mCuqps.js → checkbox.BKgfzJZV.js} +1 -1
  185. streamlit/static/static/js/{createDownloadLinkElement.CfqHRpxo.js → createDownloadLinkElement.CG7nr2a4.js} +1 -1
  186. streamlit/static/static/js/{data-grid-overlay-editor.PuoMl3yV.js → data-grid-overlay-editor.ChXO__lP.js} +1 -1
  187. streamlit/static/static/js/{downloader.CjG2csSm.js → downloader.DJ3R_zWA.js} +1 -1
  188. streamlit/static/static/js/embed.u3PPfLkw.js +193 -0
  189. streamlit/static/static/js/{es6.CQD6uUK7.js → es6.C5Mfy8nd.js} +2 -2
  190. streamlit/static/static/js/{formatNumber.CtjUO-if.js → formatNumber.CMRgW9EJ.js} +1 -1
  191. streamlit/static/static/js/{iconPosition.7Qt6oUiI.js → iconPosition.B4EEXI3E.js} +1 -1
  192. streamlit/static/static/js/{iframeResizer.contentWindow._oj2Xh0v.js → iframeResizer.contentWindow.WSvOiTW0.js} +1 -1
  193. streamlit/static/static/js/index.-FOBV3nz.js +1 -0
  194. streamlit/static/static/js/{index.BuBkymZd.js → index.-NF8OSF5.js} +1 -1
  195. streamlit/static/static/js/{index.B-XrnnK6.js → index.4cBg8kn5.js} +1 -1
  196. streamlit/static/static/js/{index.B_ylV_tl.js → index.B0pzzCsH.js} +1 -1
  197. streamlit/static/static/js/{index.BhJwyXH6.js → index.BID6ND5j.js} +2 -2
  198. streamlit/static/static/js/index.BMp5bGjh.js +1 -0
  199. streamlit/static/static/js/{index.Cptu1tS-.js → index.BQcmlvas.js} +1 -1
  200. streamlit/static/static/js/{index.DXQ_Fvpt.js → index.BRcmclgI.js} +1 -1
  201. streamlit/static/static/js/index.BaUZR4IG.js +1 -0
  202. streamlit/static/static/js/{index.CMBgAPh6.js → index.BbMJj4PN.js} +1 -1
  203. streamlit/static/static/js/{index.CVRgrLT-.js → index.BdCTJtq3.js} +2 -2
  204. streamlit/static/static/js/index.BdETLMuI.js +1 -0
  205. streamlit/static/static/js/index.BnKMWhs1.js +1 -0
  206. streamlit/static/static/js/index.Br1kXwQW.js +2 -0
  207. streamlit/static/static/js/{index.XGft6-dq.js → index.Bt2olRE4.js} +1 -1
  208. streamlit/static/static/js/{index.B2fAYU1N.js → index.Bxwsv5T8.js} +1 -1
  209. streamlit/static/static/js/index.C4KskYz6.js +1 -0
  210. streamlit/static/static/js/{index.DZE_91Ym.js → index.C6bmbXk0.js} +1 -1
  211. streamlit/static/static/js/{index.Egabyb7u.js → index.CEfKfbta.js} +1 -1
  212. streamlit/static/static/js/index.CIuaA8q0.js +2 -0
  213. streamlit/static/static/js/{index.DVtfSohT.js → index.CV1sObFX.js} +1 -1
  214. streamlit/static/static/js/{index.BlJhnb4M.js → index.CbR6dgaV.js} +1 -1
  215. streamlit/static/static/js/index.Cq6szKqJ.js +1 -0
  216. streamlit/static/static/js/index.CyouXqCz.js +1 -0
  217. streamlit/static/static/js/{index.B5wmZkRW.js → index.D1NUgMFI.js} +1 -1
  218. streamlit/static/static/js/{index.euRMkmNi.js → index.D7SWG4Om.js} +1 -1
  219. streamlit/static/static/js/{index.Bg-9YNUa.js → index.DAYPEwLI.js} +1 -1
  220. streamlit/static/static/js/index.DKS75Vfg.js +11 -0
  221. streamlit/static/static/js/{index.CIizdLeb.js → index.DOXrMIxB.js} +1 -1
  222. streamlit/static/static/js/{index.BRegnbUa.js → index.DOzYX8yS.js} +3 -3
  223. streamlit/static/static/js/{index.BksGMsW0.js → index.DRFMYcC4.js} +4 -4
  224. streamlit/static/static/js/{index.B8PovXCX.js → index.Divl5FCY.js} +1 -1
  225. streamlit/static/static/js/{index.DxQuXlXH.js → index.DjAJ_CUa.js} +1 -1
  226. streamlit/static/static/js/{index.BrRuSP42.js → index.Dncue2pm.js} +33 -33
  227. streamlit/static/static/js/{index.DSTThs-t.js → index.Drusyo5m.js} +47 -47
  228. streamlit/static/static/js/{index.BOafPwIE.js → index.DuUyDGnP.js} +1 -1
  229. streamlit/static/static/js/{index.D1bkwsLT.js → index.DvgT2rB2.js} +223 -223
  230. streamlit/static/static/js/{index.BmDXWfgx.js → index.DzutABu5.js} +2 -2
  231. streamlit/static/static/js/index.Dzw2iPzi.js +3 -0
  232. streamlit/static/static/js/{index.DJsqD2Sc.js → index.FsTmxLbT.js} +1 -1
  233. streamlit/static/static/js/{index.BOTEMJfV.js → index.OIwPqGYN.js} +1 -1
  234. streamlit/static/static/js/{index.CBqST2Yj.js → index.RXLN7YFT.js} +2 -2
  235. streamlit/static/static/js/{index.Ft2Zxbhr.js → index.YYb2u0jk.js} +2 -2
  236. streamlit/static/static/js/{index.BWCFtBS4.js → index.h8ejt-W3.js} +1 -1
  237. streamlit/static/static/js/{index.KuLql7H0.js → index.lFMCi9am.js} +1 -1
  238. streamlit/static/static/js/{index.D8t7R4QQ.js → index.pOgf4cEj.js} +1 -1
  239. streamlit/static/static/js/{index.CsoN0h7K.js → index.s_E0s7LB.js} +51 -51
  240. streamlit/static/static/js/{index.BVX_bqnf.js → index.xLCbzoqj.js} +1 -1
  241. streamlit/static/static/js/{input.Cf97CQME.js → input.BLG7kWaj.js} +2 -2
  242. streamlit/static/static/js/{main.Ccuk53yQ.js → main.D_CmqChN.js} +1 -1
  243. streamlit/static/static/js/{memory.Bng6Ij0g.js → memory.T8u9KqIQ.js} +1 -1
  244. streamlit/static/static/js/{number-overlay-editor.CFLv-CWC.js → number-overlay-editor.BKBSXkAM.js} +2 -2
  245. streamlit/static/static/js/{pandasStylerUtils.C2hcAKiv.js → pandasStylerUtils.B4tLYMwS.js} +1 -1
  246. streamlit/static/static/js/{sandbox.BXdeD-wA.js → sandbox.jRlkcPem.js} +1 -1
  247. streamlit/static/static/js/{styled-components.Br04Ogac.js → styled-components.D2QhNwzd.js} +1 -1
  248. streamlit/static/static/js/{throttle.mI9ItGre.js → throttle.Cyw_V0Dq.js} +1 -1
  249. streamlit/static/static/js/{timepicker.poFdB0sd.js → timepicker.PzyuDDWl.js} +1 -1
  250. streamlit/static/static/js/{toConsumableArray.92-fANS-.js → toConsumableArray.gE9fMkLj.js} +1 -1
  251. streamlit/static/static/js/uniqueId.B1GeHnT1.js +1 -0
  252. streamlit/static/static/js/{useBasicWidgetState.DzKGLAv_.js → useBasicWidgetState.DFklfao0.js} +1 -1
  253. streamlit/static/static/js/{useIntlLocale.BMma2iiY.js → useIntlLocale.C3tUGWTU.js} +8 -8
  254. streamlit/static/static/js/{useTextInputAutoExpand.DQbIhdma.js → useTextInputAutoExpand.D9nU_y-e.js} +1 -1
  255. streamlit/static/static/js/useUpdateUiValue.ClTdrkJN.js +1 -0
  256. streamlit/static/static/js/{useWaveformController.AH0ggRyc.js → useWaveformController.lzTbjMW2.js} +1 -1
  257. streamlit/static/static/js/{withCalculatedWidth.G5xJ-MbS.js → withCalculatedWidth.Dxs9I5Oe.js} +1 -1
  258. streamlit/static/static/js/{withFullScreenWrapper.rdRu6zZ4.js → withFullScreenWrapper.DfpAcJxf.js} +1 -1
  259. streamlit/string_util.py +2 -2
  260. streamlit/testing/v1/app_test.py +1 -1
  261. streamlit/testing/v1/element_tree.py +33 -20
  262. streamlit/type_util.py +2 -2
  263. streamlit/url_util.py +2 -2
  264. streamlit/user_info.py +2 -41
  265. streamlit/util.py +1 -1
  266. streamlit/watcher/event_based_path_watcher.py +37 -7
  267. streamlit/watcher/path_watcher.py +61 -2
  268. streamlit/watcher/util.py +26 -10
  269. streamlit/web/bootstrap.py +16 -4
  270. streamlit/web/cli.py +1 -4
  271. streamlit/web/server/app_discovery.py +2 -1
  272. streamlit/web/server/app_static_file_handler.py +9 -0
  273. streamlit/web/server/bidi_component_request_handler.py +4 -4
  274. streamlit/web/server/component_file_utils.py +14 -6
  275. streamlit/web/server/component_request_handler.py +2 -2
  276. streamlit/web/server/oauth_authlib_routes.py +14 -42
  277. streamlit/web/server/server.py +1 -1
  278. streamlit/web/server/server_util.py +23 -1
  279. streamlit/web/server/starlette/starlette_app.py +7 -1
  280. streamlit/web/server/starlette/starlette_auth_routes.py +94 -16
  281. streamlit/web/server/starlette/starlette_path_security_middleware.py +97 -0
  282. streamlit/web/server/starlette/starlette_routes.py +16 -9
  283. streamlit/web/server/starlette/starlette_server.py +2 -2
  284. streamlit/web/server/starlette/starlette_static_routes.py +14 -4
  285. streamlit/web/server/stats_request_handler.py +1 -3
  286. {streamlit-1.53.1.dist-info → streamlit-1.54.0.dist-info}/METADATA +10 -25
  287. {streamlit-1.53.1.dist-info → streamlit-1.54.0.dist-info}/RECORD +290 -290
  288. {streamlit-1.53.1.dist-info → streamlit-1.54.0.dist-info}/WHEEL +1 -1
  289. streamlit/commands/experimental_query_params.py +0 -169
  290. streamlit/static/static/js/Particles.ix5_l22I.js +0 -1
  291. streamlit/static/static/js/Toolbar.CxkcuBQ8.js +0 -1
  292. streamlit/static/static/js/embed.DZ-CLCPz.js +0 -195
  293. streamlit/static/static/js/index.B6ZAXv47.js +0 -1
  294. streamlit/static/static/js/index.BDm-Ia27.js +0 -1
  295. streamlit/static/static/js/index.BeCZLkzg.js +0 -1
  296. streamlit/static/static/js/index.BuEBeckn.js +0 -11
  297. streamlit/static/static/js/index.CL2eCR01.js +0 -1
  298. streamlit/static/static/js/index.CdLlbsiN.js +0 -1
  299. streamlit/static/static/js/index.CwIIk90V.js +0 -1
  300. streamlit/static/static/js/index.DDk0U8rh.js +0 -2
  301. streamlit/static/static/js/index.DNB79dOd.js +0 -3
  302. streamlit/static/static/js/index.DNj5S4tY.js +0 -1
  303. streamlit/static/static/js/index.DOY0ZriT.js +0 -2
  304. streamlit/static/static/js/index.r0gCrMFP.js +0 -1
  305. streamlit/static/static/js/uniqueId.BUj-C6GA.js +0 -1
  306. streamlit/static/static/js/useUpdateUiValue.Bk5OIXup.js +0 -1
  307. streamlit-1.53.1.data/scripts/streamlit.cmd +0 -16
  308. {streamlit-1.53.1.dist-info → streamlit-1.54.0.dist-info}/entry_points.txt +0 -0
  309. {streamlit-1.53.1.dist-info → streamlit-1.54.0.dist-info}/top_level.txt +0 -0
@@ -21,6 +21,7 @@ from typing import Final
21
21
  import tornado.web
22
22
 
23
23
  from streamlit.logger import get_logger
24
+ from streamlit.path_security import is_unsafe_path_pattern
24
25
 
25
26
  _LOGGER: Final = get_logger(__name__)
26
27
 
@@ -53,6 +54,14 @@ class AppStaticFileHandler(tornado.web.StaticFileHandler):
53
54
  def initialize(self, path: str, default_filename: str | None = None) -> None:
54
55
  super().initialize(path, default_filename)
55
56
 
57
+ @classmethod
58
+ def get_absolute_path(cls, root: str, path: str) -> str:
59
+ # SECURITY: Validate path pattern BEFORE any filesystem operations.
60
+ # See is_unsafe_path_pattern() docstring for details.
61
+ if is_unsafe_path_pattern(path):
62
+ raise tornado.web.HTTPError(400, "Bad Request")
63
+ return super().get_absolute_path(root, path)
64
+
56
65
  def validate_absolute_path(self, root: str, absolute_path: str) -> str | None:
57
66
  full_path = os.path.abspath(absolute_path)
58
67
 
@@ -77,8 +77,8 @@ class BidiComponentRequestHandler(tornado.web.RequestHandler):
77
77
  Notes
78
78
  -----
79
79
  This method writes directly to the response and sets appropriate HTTP
80
- status codes on error (``404`` for missing components/files, ``403`` for
81
- forbidden paths).
80
+ status codes on error (``404`` for missing components/files, ``400`` for
81
+ unsafe paths).
82
82
  """
83
83
  parts = path.split("/")
84
84
  component_name = parts[0]
@@ -105,8 +105,8 @@ class BidiComponentRequestHandler(tornado.web.RequestHandler):
105
105
  return
106
106
  abspath = build_safe_abspath(component_path, filename)
107
107
  if abspath is None:
108
- self.write("forbidden")
109
- self.set_status(403)
108
+ self.write("Bad Request")
109
+ self.set_status(400)
110
110
  return
111
111
 
112
112
  # If the resolved path is a directory, return 404 not found.
@@ -27,15 +27,18 @@ import mimetypes
27
27
  import os
28
28
  from typing import Final
29
29
 
30
+ from streamlit.path_security import is_unsafe_path_pattern
31
+
30
32
  _OCTET_STREAM: Final[str] = "application/octet-stream"
31
33
 
32
34
 
33
35
  def build_safe_abspath(component_root: str, relative_url_path: str) -> str | None:
34
- """Build an absolute path inside ``component_root`` if safe.
36
+ r"""Build an absolute path inside ``component_root`` if safe.
35
37
 
36
- The function joins ``relative_url_path`` with ``component_root`` and
37
- normalizes and resolves symlinks. If the resulting path escapes the
38
- component root, ``None`` is returned to indicate a forbidden traversal.
38
+ The function first validates that ``relative_url_path`` does not contain
39
+ dangerous patterns using :func:`~streamlit.path_security.is_unsafe_path_pattern`,
40
+ then joins it with ``component_root`` and resolves symlinks.
41
+ Returns ``None`` if the path is rejected by security checks or escapes the root.
39
42
 
40
43
  Parameters
41
44
  ----------
@@ -43,13 +46,18 @@ def build_safe_abspath(component_root: str, relative_url_path: str) -> str | Non
43
46
  Absolute path to the component's root directory.
44
47
  relative_url_path : str
45
48
  Relative URL path from the component root to the requested file.
49
+ Must be a simple relative path without dangerous patterns.
46
50
 
47
51
  Returns
48
52
  -------
49
53
  str or None
50
- The resolved absolute path if it stays within ``component_root``;
51
- otherwise ``None`` when the path would traverse outside the root.
54
+ The resolved absolute path if it passes all validation and stays
55
+ within ``component_root``; otherwise ``None``.
52
56
  """
57
+ # See is_unsafe_path_pattern() for security details.
58
+ if is_unsafe_path_pattern(relative_url_path):
59
+ return None
60
+
53
61
  root_real = os.path.realpath(component_root)
54
62
  candidate = os.path.normpath(os.path.join(root_real, relative_url_path))
55
63
  candidate_real = os.path.realpath(candidate)
@@ -48,8 +48,8 @@ class ComponentRequestHandler(tornado.web.RequestHandler):
48
48
  filename = "/".join(parts[1:])
49
49
  abspath = build_safe_abspath(component_root, filename)
50
50
  if abspath is None:
51
- self.write("forbidden")
52
- self.set_status(403)
51
+ self.write("Bad Request")
52
+ self.set_status(400)
53
53
  return
54
54
  try:
55
55
  with open(abspath, "rb") as file:
@@ -15,18 +15,20 @@ from __future__ import annotations
15
15
 
16
16
  import json
17
17
  from typing import Any, Final, cast
18
- from urllib.parse import urlencode, urlparse
19
18
 
20
19
  import tornado.web
21
20
 
22
21
  from streamlit.auth_util import (
23
22
  AuthCache,
23
+ build_logout_url,
24
24
  clear_cookie_and_chunks,
25
25
  decode_provider_token,
26
26
  generate_default_provider_section,
27
27
  get_cookie_with_chunks,
28
+ get_origin_from_redirect_uri,
28
29
  get_redirect_uri,
29
30
  get_secrets_auth_section,
31
+ get_validated_redirect_uri,
30
32
  set_cookie_with_chunks,
31
33
  )
32
34
  from streamlit.errors import StreamlitAuthError
@@ -179,21 +181,6 @@ class AuthLogoutHandler(AuthHandlerMixin, tornado.web.RequestHandler):
179
181
  else:
180
182
  self.redirect_to_base()
181
183
 
182
- def _get_redirect_uri(self) -> str | None:
183
- auth_section = get_secrets_auth_section()
184
- if not auth_section:
185
- return None
186
-
187
- redirect_uri = get_redirect_uri(auth_section)
188
- if not redirect_uri:
189
- return None
190
-
191
- if not redirect_uri.endswith("/oauth2callback"):
192
- _LOGGER.warning("Redirect URI does not end with /oauth2callback")
193
- return None
194
-
195
- return redirect_uri
196
-
197
184
  def _get_provider_logout_url(self) -> str | None:
198
185
  """Get the OAuth provider's logout URL from OIDC metadata."""
199
186
  cookie_value = get_cookie_with_chunks(self._get_signed_cookie, AUTH_COOKIE_NAME)
@@ -219,17 +206,13 @@ class AuthLogoutHandler(AuthHandlerMixin, tornado.web.RequestHandler):
219
206
  # Use redirect_uri (i.e. /oauth2callback) for post_logout_redirect_uri
220
207
  # This is safer than redirecting to root as some providers seem to
221
208
  # require url to be in a whitelist /oauth2callback should be whitelisted
222
- redirect_uri = self._get_redirect_uri()
209
+ redirect_uri = get_validated_redirect_uri()
223
210
  if redirect_uri is None:
224
211
  _LOGGER.info("Redirect url could not be determined")
225
212
  return None
226
213
 
227
- logout_params = {
228
- "client_id": client.client_id,
229
- "post_logout_redirect_uri": redirect_uri,
230
- }
231
-
232
- # Add id_token_hint to logout params if it is available
214
+ # Get id_token_hint from tokens cookie if available
215
+ id_token: str | None = None
233
216
  tokens_cookie_value = get_cookie_with_chunks(
234
217
  self._get_signed_cookie, TOKENS_COOKIE_NAME
235
218
  )
@@ -237,15 +220,16 @@ class AuthLogoutHandler(AuthHandlerMixin, tornado.web.RequestHandler):
237
220
  try:
238
221
  tokens = json.loads(tokens_cookie_value)
239
222
  id_token = tokens.get("id_token")
240
- if id_token:
241
- logout_params["id_token_hint"] = id_token
242
223
  except (json.JSONDecodeError, TypeError):
243
- _LOGGER.exception(
244
- "Error, invalid tokens cookie value.",
245
- )
224
+ _LOGGER.exception("Error, invalid tokens cookie value.")
246
225
  return None
247
226
 
248
- return f"{end_session_endpoint}?{urlencode(logout_params)}"
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
+ )
249
233
 
250
234
  except Exception as e:
251
235
  _LOGGER.warning("Failed to get provider logout URL: %s", e)
@@ -327,16 +311,4 @@ class AuthCallbackHandler(AuthHandlerMixin, tornado.web.RequestHandler):
327
311
  return provider
328
312
 
329
313
  def _get_origin_from_secrets(self) -> str | None:
330
- redirect_uri = None
331
- auth_section = get_secrets_auth_section()
332
- if auth_section:
333
- redirect_uri = get_redirect_uri(auth_section)
334
-
335
- if not redirect_uri:
336
- return None
337
-
338
- redirect_uri_parsed = urlparse(redirect_uri)
339
- origin_from_redirect_uri: str = (
340
- redirect_uri_parsed.scheme + "://" + redirect_uri_parsed.netloc
341
- )
342
- return origin_from_redirect_uri
314
+ return get_origin_from_redirect_uri()
@@ -263,7 +263,7 @@ def start_listening_tcp_socket(http_server: HTTPServer) -> None:
263
263
  except OSError as e:
264
264
  # EADDRINUSE: port in use by another process
265
265
  # EACCES: port reserved by system (common on Windows, see #13521)
266
- if e.errno in (errno.EADDRINUSE, errno.EACCES):
266
+ if e.errno in {errno.EADDRINUSE, errno.EACCES}:
267
267
  if server_port_is_manually_set():
268
268
  _LOGGER.error("Port %s is not available", port) # noqa: TRY400
269
269
  sys.exit(1)
@@ -82,7 +82,7 @@ def is_url_from_allowed_origins(url: str) -> bool:
82
82
  url_util.get_hostname(origin) for origin in allowlisted_origins()
83
83
  ]
84
84
 
85
- allowed_domains: list[str | None | Callable[[], str | None]] = [
85
+ allowed_domains: list[str | Callable[[], str | None] | None] = [
86
86
  # Check localhost first.
87
87
  "localhost",
88
88
  "0.0.0.0", # noqa: S104
@@ -137,6 +137,28 @@ def _get_server_address_if_manually_set() -> str | None:
137
137
  return None
138
138
 
139
139
 
140
+ def get_display_address(address: str) -> str:
141
+ """Get a display-friendly address for URLs shown to users.
142
+
143
+ Wildcard addresses like "0.0.0.0" (all IPv4) or "::" (all interfaces)
144
+ are not valid browser addresses on all platforms. This translates
145
+ them to "localhost" for display purposes.
146
+
147
+ Parameters
148
+ ----------
149
+ address
150
+ The server address (IP or hostname).
151
+
152
+ Returns
153
+ -------
154
+ str
155
+ Address suitable for display. Wildcards become "localhost".
156
+ """
157
+ if address in {"0.0.0.0", "::"}: # noqa: S104
158
+ return "localhost"
159
+ return address
160
+
161
+
140
162
  def make_url_path_regex(
141
163
  *path: str,
142
164
  trailing_slash: Literal["optional", "required", "prohibited"] = "optional",
@@ -140,7 +140,7 @@ def create_streamlit_middleware() -> list[Middleware]:
140
140
  """Create the Streamlit-internal middleware stack.
141
141
 
142
142
  This function creates the middleware required for Streamlit's core functionality
143
- including session management and GZip compression.
143
+ including path security, session management, and GZip compression.
144
144
 
145
145
  Returns
146
146
  -------
@@ -153,9 +153,15 @@ def create_streamlit_middleware() -> list[Middleware]:
153
153
  from streamlit.web.server.starlette.starlette_gzip_middleware import (
154
154
  MediaAwareGZipMiddleware,
155
155
  )
156
+ from streamlit.web.server.starlette.starlette_path_security_middleware import (
157
+ PathSecurityMiddleware,
158
+ )
156
159
 
157
160
  middleware: list[Middleware] = []
158
161
 
162
+ # FIRST: Path security middleware to block dangerous paths before any other processing.
163
+ middleware.append(Middleware(PathSecurityMiddleware))
164
+
159
165
  # Add session middleware
160
166
  middleware.append(
161
167
  Middleware(
@@ -12,19 +12,26 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ # ruff: noqa: RUF029 # Async route handlers are idiomatic even without await
16
+
15
17
  """Starlette app authentication routes."""
16
18
 
17
19
  from __future__ import annotations
18
20
 
21
+ import json
19
22
  import time
20
23
  from typing import TYPE_CHECKING, Any, Final, cast
21
- from urllib.parse import urlparse
22
24
 
23
25
  from streamlit.auth_util import (
26
+ build_logout_url,
24
27
  clear_cookie_and_chunks,
25
28
  decode_provider_token,
26
29
  generate_default_provider_section,
30
+ get_cookie_with_chunks,
31
+ get_origin_from_redirect_uri,
32
+ get_redirect_uri,
27
33
  get_secrets_auth_section,
34
+ get_validated_redirect_uri,
28
35
  set_cookie_with_chunks,
29
36
  )
30
37
  from streamlit.errors import StreamlitAuthError
@@ -139,7 +146,7 @@ def _looks_like_provider_section(value: dict[str, Any]) -> bool:
139
146
  return any(key in value for key in provider_keys)
140
147
 
141
148
 
142
- class _AuthlibConfig(dict[str, Any]):
149
+ class _AuthlibConfig(dict[str, Any]): # noqa: FURB189
143
150
  """Config adapter that exposes provider data via Authlib's flat lookup.
144
151
 
145
152
  Authlib expects a flat configuration dictionary (e.g. "GOOGLE_CLIENT_ID").
@@ -307,7 +314,7 @@ def _create_oauth_client(provider: str) -> tuple[Any, str]:
307
314
 
308
315
  auth_section = get_secrets_auth_section()
309
316
  if auth_section:
310
- redirect_uri = auth_section.get("redirect_uri", "/")
317
+ redirect_uri = get_redirect_uri(auth_section) or "/"
311
318
  config = auth_section.to_dict()
312
319
  else:
313
320
  config = {}
@@ -385,20 +392,80 @@ def _get_provider_by_state(state_code_from_url: str | None) -> str | None:
385
392
 
386
393
  def _get_origin_from_secrets() -> str | None:
387
394
  """Extract the origin from the redirect URI in the secrets."""
395
+ return get_origin_from_redirect_uri()
388
396
 
389
- redirect_uri = None
390
- auth_section = get_secrets_auth_section()
391
- if auth_section:
392
- redirect_uri = auth_section.get("redirect_uri", None)
393
397
 
394
- if not redirect_uri:
398
+ def _get_cookie_value_from_request(request: Request, cookie_name: str) -> bytes | None:
399
+ """Get a signed cookie value from the request, handling chunked cookies."""
400
+
401
+ def get_single_cookie(name: str) -> bytes | None:
402
+ return _get_signed_cookie_from_request(request, name)
403
+
404
+ return get_cookie_with_chunks(get_single_cookie, cookie_name)
405
+
406
+
407
+ def _get_provider_logout_url(request: Request) -> str | None:
408
+ """Get the OAuth provider's logout URL from OIDC metadata.
409
+
410
+ Returns the end_session_endpoint URL with proper parameters for OIDC logout,
411
+ or None if the provider doesn't support it or required data is unavailable.
412
+
413
+ This function returns None (rather than raising exceptions) to allow graceful
414
+ fallback to a simple base URL redirect when OIDC logout isn't possible.
415
+ """
416
+ cookie_value = _get_cookie_value_from_request(request, USER_COOKIE_NAME)
417
+
418
+ if not cookie_value:
395
419
  return None
396
420
 
397
- redirect_uri_parsed = urlparse(redirect_uri)
398
- origin_from_redirect_uri: str = (
399
- redirect_uri_parsed.scheme + "://" + redirect_uri_parsed.netloc
400
- )
401
- return origin_from_redirect_uri
421
+ try:
422
+ user_info = json.loads(cookie_value)
423
+ provider = user_info.get("provider")
424
+ if not provider:
425
+ return None
426
+
427
+ client, _ = _create_oauth_client(provider)
428
+
429
+ # Load OIDC metadata - Authlib's Starlette client uses async methods
430
+ # but load_server_metadata is synchronous in both implementations
431
+ metadata = client.load_server_metadata()
432
+ end_session_endpoint = metadata.get("end_session_endpoint")
433
+
434
+ if not end_session_endpoint:
435
+ _LOGGER.info("No end_session_endpoint found for provider %s", provider)
436
+ return None
437
+
438
+ # Use redirect_uri (i.e. /oauth2callback) for post_logout_redirect_uri
439
+ # This is safer than redirecting to root as some providers seem to
440
+ # require URL to be in a whitelist - /oauth2callback should be whitelisted
441
+ redirect_uri = get_validated_redirect_uri()
442
+ if redirect_uri is None:
443
+ _LOGGER.info("Redirect url could not be determined")
444
+ return None
445
+
446
+ # Get id_token_hint from tokens cookie if available
447
+ id_token: str | None = None
448
+ tokens_cookie_value = _get_cookie_value_from_request(
449
+ request, TOKENS_COOKIE_NAME
450
+ )
451
+ if tokens_cookie_value:
452
+ try:
453
+ tokens = json.loads(tokens_cookie_value)
454
+ id_token = tokens.get("id_token")
455
+ except (json.JSONDecodeError, TypeError):
456
+ _LOGGER.exception("Error, invalid tokens cookie value.")
457
+ return None
458
+
459
+ return build_logout_url(
460
+ end_session_endpoint=end_session_endpoint,
461
+ client_id=client.client_id,
462
+ post_logout_redirect_uri=redirect_uri,
463
+ id_token=id_token,
464
+ )
465
+
466
+ except Exception as e:
467
+ _LOGGER.warning("Failed to get provider logout URL: %s", e)
468
+ return None
402
469
 
403
470
 
404
471
  async def _auth_login(request: Request, base_url: str) -> Response:
@@ -421,9 +488,20 @@ async def _auth_login(request: Request, base_url: str) -> Response:
421
488
 
422
489
 
423
490
  async def _auth_logout(request: Request, base_url: str) -> Response:
424
- """Logout the user by clearing the auth cookie and redirecting to the base URL."""
491
+ """Logout the user by clearing the auth cookie and redirecting.
492
+
493
+ If the OAuth provider supports end_session_endpoint, redirects there for
494
+ proper OIDC logout. Otherwise, redirects to the base URL.
495
+ """
496
+ from starlette.responses import RedirectResponse
497
+
498
+ provider_logout_url = _get_provider_logout_url(request)
499
+
500
+ if provider_logout_url:
501
+ response = RedirectResponse(provider_logout_url, status_code=302)
502
+ else:
503
+ response = await _redirect_to_base(base_url)
425
504
 
426
- response = await _redirect_to_base(base_url)
427
505
  _clear_auth_cookie(response, request)
428
506
  return response
429
507
 
@@ -471,7 +549,7 @@ async def _auth_callback(request: Request, base_url: str) -> Response:
471
549
 
472
550
  response = await _redirect_to_base(base_url)
473
551
 
474
- cookie_value = dict(user, origin=origin, is_logged_in=True)
552
+ cookie_value = dict(user, origin=origin, is_logged_in=True, provider=provider)
475
553
  tokens = {k: token[k] for k in ["id_token", "access_token"] if k in token}
476
554
  if user:
477
555
  await _set_auth_cookie(response, cookie_value, tokens)
@@ -0,0 +1,97 @@
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
+ """Path security middleware for blocking unsafe path patterns.
16
+
17
+ This middleware implements the "Swiss Cheese" defense model - it provides
18
+ an additional layer of protection that catches dangerous path patterns even
19
+ if individual route handlers forget to validate paths. This is especially
20
+ important for preventing SSRF attacks via Windows UNC paths.
21
+
22
+ Defense Layers
23
+ --------------
24
+ Layer 1 (this middleware): Catch-all for any route, including future routes
25
+ Layer 2 (route handlers): Defense-in-depth via build_safe_abspath() and
26
+ explicit is_unsafe_path_pattern() checks
27
+
28
+ Each layer has potential "holes" (ways it could fail):
29
+ - Middleware: Could be accidentally removed, misconfigured, or bypassed
30
+ - Route handlers: Developer could forget to add checks to new routes
31
+
32
+ By keeping both layers, an attack only succeeds if BOTH fail simultaneously.
33
+
34
+ See Also
35
+ --------
36
+ streamlit.path_security : Core path validation functions used by this middleware
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from typing import TYPE_CHECKING
42
+
43
+ from starlette.responses import Response
44
+
45
+ from streamlit.path_security import is_unsafe_path_pattern
46
+
47
+ if TYPE_CHECKING:
48
+ from starlette.types import ASGIApp, Receive, Scope, Send
49
+
50
+
51
+ class PathSecurityMiddleware:
52
+ """ASGI middleware that blocks requests with unsafe path patterns.
53
+
54
+ Implements Swiss Cheese defense - catches dangerous patterns even if
55
+ route handlers forget to validate paths. This prevents SSRF attacks
56
+ via Windows UNC paths and other path traversal vulnerabilities.
57
+
58
+ Parameters
59
+ ----------
60
+ app
61
+ The ASGI application to wrap.
62
+ """
63
+
64
+ def __init__(self, app: ASGIApp) -> None:
65
+ self.app = app
66
+
67
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
68
+ """Process incoming requests and block unsafe paths.
69
+
70
+ Only validates HTTP requests; WebSocket and lifespan scopes are
71
+ passed through without validation since they don't serve file content.
72
+ """
73
+ # Only validate HTTP requests (skip WebSocket, lifespan)
74
+ if scope["type"] != "http":
75
+ await self.app(scope, receive, send)
76
+ return
77
+
78
+ path = scope.get("path", "")
79
+
80
+ # SECURITY: Check for double-slash patterns BEFORE stripping slashes.
81
+ # UNC paths like "//server/share" would be normalized to "server/share"
82
+ # by lstrip("/"), making them look safe. We must reject these early.
83
+ if path.startswith(("//", "\\\\")):
84
+ response = Response(content="Bad Request", status_code=400)
85
+ await response(scope, receive, send)
86
+ return
87
+
88
+ # Strip leading slash to get the relative path for validation
89
+ relative_path = path.lstrip("/")
90
+
91
+ # Check if the path contains unsafe patterns
92
+ if relative_path and is_unsafe_path_pattern(relative_path):
93
+ response = Response(content="Bad Request", status_code=400)
94
+ await response(scope, receive, send)
95
+ return
96
+
97
+ await self.app(scope, receive, send)
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ # ruff: noqa: RUF029 # Async route handlers are idiomatic even without await
16
+
15
17
  """Route handlers for the Starlette server."""
16
18
 
17
19
  from __future__ import annotations
@@ -341,9 +343,7 @@ def create_metrics_routes(runtime: Runtime, base_url: str | None) -> list[BaseRo
341
343
 
342
344
  async def _metrics_endpoint(request: Request) -> Response:
343
345
  requested_families = request.query_params.getlist("families")
344
- stats = runtime.stats_mgr.get_stats(
345
- family_names=requested_families if requested_families else None
346
- )
346
+ stats = runtime.stats_mgr.get_stats(family_names=requested_families or None)
347
347
  accept = request.headers.get("Accept", "")
348
348
  if "application/x-protobuf" in accept:
349
349
  payload = StatsRequestHandler._stats_to_proto(stats).SerializeToString()
@@ -701,7 +701,8 @@ def create_component_routes(
701
701
  # Use build_safe_abspath to properly resolve symlinks and prevent traversal
702
702
  abspath = build_safe_abspath(component_root, filename)
703
703
  if abspath is None:
704
- raise HTTPException(status_code=403, detail="Forbidden")
704
+ # Return 400 for malicious paths (consistent with middleware behavior)
705
+ raise HTTPException(status_code=400, detail="Bad Request")
705
706
 
706
707
  try:
707
708
  async with await anyio.open_file(abspath, "rb") as file:
@@ -743,6 +744,7 @@ def create_bidi_component_routes(
743
744
  ) -> list[BaseRoute]:
744
745
  """Create bidirectional component route handlers."""
745
746
  import anyio
747
+ from anyio import Path as AsyncPath
746
748
  from starlette.responses import PlainTextResponse, Response
747
749
  from starlette.routing import Route
748
750
 
@@ -771,9 +773,10 @@ def create_bidi_component_routes(
771
773
 
772
774
  abspath = build_safe_abspath(component_root, filename)
773
775
  if abspath is None:
774
- return await _text_response("forbidden", 403)
776
+ # Return 400 for unsafe paths (matches Tornado behavior for opacity)
777
+ return await _text_response("Bad Request", 400)
775
778
 
776
- if os.path.isdir(abspath):
779
+ if await AsyncPath(abspath).is_dir():
777
780
  return await _text_response("not found", 404)
778
781
 
779
782
  try:
@@ -819,6 +822,7 @@ def create_app_static_serving_routes(
819
822
  main_script_path: str | None, base_url: str | None
820
823
  ) -> list[BaseRoute]:
821
824
  """Create app static serving file route handlers."""
825
+ from anyio import Path as AsyncPath
822
826
  from starlette.exceptions import HTTPException
823
827
  from starlette.responses import FileResponse, Response
824
828
  from starlette.routing import Route
@@ -836,12 +840,15 @@ def create_app_static_serving_routes(
836
840
  relative_path = request.path_params.get("path", "")
837
841
  safe_path = build_safe_abspath(app_static_root, relative_path)
838
842
  if safe_path is None:
839
- raise HTTPException(status_code=404, detail="File not found")
843
+ # Return 400 for malicious paths (consistent with middleware behavior)
844
+ raise HTTPException(status_code=400, detail="Bad Request")
840
845
 
841
- if not os.path.exists(safe_path) or os.path.isdir(safe_path):
846
+ async_path = AsyncPath(safe_path)
847
+ if not await async_path.exists() or await async_path.is_dir():
842
848
  raise HTTPException(status_code=404, detail="File not found")
843
849
 
844
- if os.path.getsize(safe_path) > MAX_APP_STATIC_FILE_SIZE:
850
+ file_stat = await async_path.stat()
851
+ if file_stat.st_size > MAX_APP_STATIC_FILE_SIZE:
845
852
  raise HTTPException(
846
853
  status_code=404,
847
854
  detail="File is too large",
@@ -288,7 +288,7 @@ class UvicornServer:
288
288
  last_exception = exc
289
289
  # EADDRINUSE: port in use by another process
290
290
  # EACCES: port reserved by system (common on Windows, see #13521)
291
- if exc.errno in (errno.EADDRINUSE, errno.EACCES):
291
+ if exc.errno in {errno.EADDRINUSE, errno.EACCES}:
292
292
  if _is_port_manually_set():
293
293
  _LOGGER.error("Port %s is not available", port) # noqa: TRY400
294
294
  sys.exit(1)
@@ -476,7 +476,7 @@ class UvicornRunner:
476
476
  except OSError as exc:
477
477
  # EADDRINUSE: port in use by another process
478
478
  # EACCES: port reserved by system (common on Windows)
479
- if exc.errno in (errno.EADDRINUSE, errno.EACCES):
479
+ if exc.errno in {errno.EADDRINUSE, errno.EACCES}:
480
480
  if _is_port_manually_set():
481
481
  _LOGGER.error("Port %s is not available", port) # noqa: TRY400
482
482
  sys.exit(1)