streamlit 1.47.0__py3-none-any.whl → 1.48.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 (189) hide show
  1. streamlit/cli_util.py +1 -1
  2. streamlit/commands/echo.py +2 -2
  3. streamlit/commands/execution_control.py +1 -1
  4. streamlit/commands/page_config.py +16 -16
  5. streamlit/components/v1/component_arrow.py +4 -4
  6. streamlit/config.py +23 -5
  7. streamlit/connections/base_connection.py +2 -2
  8. streamlit/connections/snowflake_connection.py +2 -2
  9. streamlit/connections/sql_connection.py +4 -3
  10. streamlit/dataframe_util.py +1 -1
  11. streamlit/deprecation_util.py +20 -5
  12. streamlit/elements/arrow.py +105 -79
  13. streamlit/elements/deck_gl_json_chart.py +6 -6
  14. streamlit/elements/dialog_decorator.py +72 -17
  15. streamlit/elements/form.py +36 -7
  16. streamlit/elements/graphviz_chart.py +7 -0
  17. streamlit/elements/json.py +3 -3
  18. streamlit/elements/layouts.py +241 -75
  19. streamlit/elements/lib/built_in_chart_utils.py +11 -3
  20. streamlit/elements/lib/dialog.py +43 -0
  21. streamlit/elements/lib/file_uploader_utils.py +21 -2
  22. streamlit/elements/lib/layout_utils.py +88 -2
  23. streamlit/elements/lib/options_selector_utils.py +1 -1
  24. streamlit/elements/lib/utils.py +23 -3
  25. streamlit/elements/map.py +12 -11
  26. streamlit/elements/plotly_chart.py +21 -21
  27. streamlit/elements/pyplot.py +8 -4
  28. streamlit/elements/vega_charts.py +237 -143
  29. streamlit/elements/widgets/audio_input.py +2 -2
  30. streamlit/elements/widgets/button.py +147 -35
  31. streamlit/elements/widgets/button_group.py +9 -8
  32. streamlit/elements/widgets/camera_input.py +2 -2
  33. streamlit/elements/widgets/chat.py +8 -2
  34. streamlit/elements/widgets/checkbox.py +4 -4
  35. streamlit/elements/widgets/color_picker.py +2 -2
  36. streamlit/elements/widgets/data_editor.py +22 -15
  37. streamlit/elements/widgets/file_uploader.py +8 -2
  38. streamlit/elements/widgets/multiselect.py +27 -17
  39. streamlit/elements/widgets/number_input.py +2 -2
  40. streamlit/elements/widgets/radio.py +5 -4
  41. streamlit/elements/widgets/select_slider.py +8 -5
  42. streamlit/elements/widgets/selectbox.py +14 -4
  43. streamlit/elements/widgets/slider.py +59 -33
  44. streamlit/elements/widgets/text_widgets.py +4 -4
  45. streamlit/elements/widgets/time_widgets.py +4 -4
  46. streamlit/elements/write.py +9 -8
  47. streamlit/env_util.py +1 -1
  48. streamlit/errors.py +20 -4
  49. streamlit/file_util.py +1 -1
  50. streamlit/git_util.py +1 -1
  51. streamlit/logger.py +2 -2
  52. streamlit/proto/Block_pb2.py +39 -31
  53. streamlit/proto/Block_pb2.pyi +54 -3
  54. streamlit/proto/Button_pb2.py +4 -2
  55. streamlit/proto/Button_pb2.pyi +1 -0
  56. streamlit/runtime/caching/cache_data_api.py +15 -2
  57. streamlit/runtime/caching/cache_errors.py +1 -1
  58. streamlit/runtime/caching/cache_resource_api.py +24 -2
  59. streamlit/runtime/caching/cache_utils.py +6 -3
  60. streamlit/runtime/caching/hashing.py +6 -9
  61. streamlit/runtime/fragment.py +3 -2
  62. streamlit/runtime/runtime.py +19 -2
  63. streamlit/runtime/scriptrunner/script_runner.py +3 -3
  64. streamlit/runtime/state/common.py +2 -1
  65. streamlit/runtime/state/query_params.py +2 -1
  66. streamlit/runtime/state/session_state.py +1 -1
  67. streamlit/static/index.html +1 -1
  68. streamlit/static/manifest.json +231 -231
  69. streamlit/static/static/css/index.CQt5TjGB.css +1 -0
  70. streamlit/static/static/js/{ErrorOutline.esm.bg5MMAri.js → ErrorOutline.esm.D_4oFNKB.js} +1 -1
  71. streamlit/static/static/js/{FileDownload.esm.ZD5PIl3I.js → FileDownload.esm.NPgaLlUE.js} +1 -1
  72. streamlit/static/static/js/{FileHelper.B6jszxAn.js → FileHelper.B2t9ikoS.js} +1 -1
  73. streamlit/static/static/js/{FormClearHelper.DuzI-rET.js → FormClearHelper.BLEIUk6L.js} +1 -1
  74. streamlit/static/static/js/{Hooks.qKXFrFZm.js → Hooks.BGm9sd4U.js} +1 -1
  75. streamlit/static/static/js/{InputInstructions.7NdlV9ze.js → InputInstructions.DtUxCBS8.js} +1 -1
  76. streamlit/static/static/js/Particles.BDRPO7r3.js +1 -0
  77. streamlit/static/static/js/ProgressBar.B64DUUqp.js +2 -0
  78. streamlit/static/static/js/Toolbar.B3FquPk5.js +1 -0
  79. streamlit/static/static/js/{base-input.Dl5fJ2xw.js → base-input.DeBqm5mN.js} +4 -4
  80. streamlit/static/static/js/{checkbox.DCoT7yTn.js → checkbox.C0odQfKb.js} +2 -2
  81. streamlit/static/static/js/createDownloadLinkElement.ZaXNnPK4.js +1 -0
  82. streamlit/static/static/js/{createSuper.DIDXPRra.js → createSuper.DqQ5L3XG.js} +1 -1
  83. streamlit/static/static/js/data-grid-overlay-editor.DbNsQa8Y.js +1 -0
  84. streamlit/static/static/js/{downloader.DKv2kGmf.js → downloader.Bx1D0jhz.js} +1 -1
  85. streamlit/static/static/js/{es6.JcohG4tM.js → es6.CbPK4m0H.js} +2 -2
  86. streamlit/static/static/js/{iframeResizer.contentWindow.3rtAterA.js → iframeResizer.contentWindow.CfLKrptA.js} +1 -1
  87. streamlit/static/static/js/{index.BO22lkaO.js → index.0XDwe9RV.js} +12 -12
  88. streamlit/static/static/js/{index.Wj4EUfiO.js → index.4lI9TuZm.js} +3 -3
  89. streamlit/static/static/js/index.6s0nVIis.js +3855 -0
  90. streamlit/static/static/js/index.9E7bRUBU.js +1 -0
  91. streamlit/static/static/js/index.B9PgeLrZ.js +1 -0
  92. streamlit/static/static/js/index.B9vzGbOt.js +1 -0
  93. streamlit/static/static/js/index.BDrQKMCm.js +1 -0
  94. streamlit/static/static/js/index.BPsoiGgP.js +1 -0
  95. streamlit/static/static/js/index.CJzdLAun.js +1 -0
  96. streamlit/static/static/js/index.CNNlC1NL.js +1 -0
  97. streamlit/static/static/js/index.CO1sClzJ.js +2 -0
  98. streamlit/static/static/js/index.CZnagxXD.js +1 -0
  99. streamlit/static/static/js/{index.CnNERm7h.js → index.Cb0xSF7V.js} +289 -289
  100. streamlit/static/static/js/index.CgUt3tz_.js +1 -0
  101. streamlit/static/static/js/index.CjImmcsV.js +1 -0
  102. streamlit/static/static/js/index.ClRTsv8m.js +2 -0
  103. streamlit/static/static/js/{index.DBhELRFO.js → index.CnfWsQzS.js} +1 -1
  104. streamlit/static/static/js/index.CrJ1XD_V.js +1 -0
  105. streamlit/static/static/js/index.CtiKsjSC.js +1 -0
  106. streamlit/static/static/js/index.CwAuytgV.js +1 -0
  107. streamlit/static/static/js/index.D7GB-kly.js +1 -0
  108. streamlit/static/static/js/index.DA5wU0mQ.js +1 -0
  109. streamlit/static/static/js/{index.BVTVGhCW.js → index.DCpyIFTV.js} +1 -1
  110. streamlit/static/static/js/{index.Brmdn6cI.js → index.DE9wNOje.js} +1 -1
  111. streamlit/static/static/js/{index.B4UgLnfD.js → index.DHnB-C8A.js} +1 -1
  112. streamlit/static/static/js/{index.DmB_1wAI.js → index.DRTn9zvD.js} +1 -1
  113. streamlit/static/static/js/index.DjMjyJl9.js +7 -0
  114. streamlit/static/static/js/{index.BTGIlECR.js → index.DvRPFfw6.js} +162 -188
  115. streamlit/static/static/js/{index.BlIoUPom.js → index.DwaoC4Zp.js} +3 -3
  116. streamlit/static/static/js/index.F9tSej94.js +1 -0
  117. streamlit/static/static/js/index.HeVbRh9H.js +73 -0
  118. streamlit/static/static/js/index.J2D_m7LY.js +197 -0
  119. streamlit/static/static/js/index.PyIqRRSR.js +1 -0
  120. streamlit/static/static/js/index.dfivzJNz.js +1 -0
  121. streamlit/static/static/js/index.mRztGO69.js +3 -0
  122. streamlit/static/static/js/index.tB1kn_7z.js +1 -0
  123. streamlit/static/static/js/index.wDYef4mQ.js +12 -0
  124. streamlit/static/static/js/{input.vCGI-j0z.js → input.BL2buuce.js} +2 -2
  125. streamlit/static/static/js/{memory.CAr59PeX.js → memory.CUxjUWS7.js} +1 -1
  126. streamlit/static/static/js/{mergeWith.KUX-f18q.js → mergeWith.C1kp1zIi.js} +1 -1
  127. streamlit/static/static/js/{number-overlay-editor.7_qeQYbF.js → number-overlay-editor.WpheGpmR.js} +1 -1
  128. streamlit/static/static/js/{possibleConstructorReturn.ChgdWjjy.js → possibleConstructorReturn.DbvQboK3.js} +1 -1
  129. streamlit/static/static/js/{sandbox.6mITyM_H.js → sandbox.6lnFVWhX.js} +1 -1
  130. streamlit/static/static/js/{timepicker.BlKW9Kob.js → timepicker.Bg4xAK95.js} +1 -1
  131. streamlit/static/static/js/{toConsumableArray.DkNUwUhg.js → toConsumableArray.D9x7Ktv4.js} +1 -1
  132. streamlit/static/static/js/{uniqueId.BW6icrs1.js → uniqueId.Bm8FHN92.js} +1 -1
  133. streamlit/static/static/js/{useBasicWidgetState.oPxte5Mj.js → useBasicWidgetState.CUSYQZpm.js} +1 -1
  134. streamlit/static/static/js/{useTextInputAutoExpand.s7ZXp9QC.js → useTextInputAutoExpand.Bf2egQOG.js} +2 -2
  135. streamlit/static/static/js/useUpdateUiValue.lE5xnYWF.js +1 -0
  136. streamlit/static/static/js/withFullScreenWrapper.CCOXR7N6.js +1 -0
  137. streamlit/temporary_directory.py +5 -3
  138. streamlit/testing/v1/app_test.py +4 -1
  139. streamlit/testing/v1/element_tree.py +4 -5
  140. streamlit/type_util.py +10 -3
  141. streamlit/user_info.py +18 -14
  142. streamlit/util.py +11 -1
  143. streamlit/watcher/local_sources_watcher.py +4 -3
  144. streamlit/web/bootstrap.py +18 -12
  145. streamlit/web/cli.py +2 -1
  146. streamlit/web/server/oauth_authlib_routes.py +59 -3
  147. streamlit/web/server/routes.py +3 -0
  148. streamlit/web/server/server.py +72 -21
  149. {streamlit-1.47.0.dist-info → streamlit-1.48.0.dist-info}/METADATA +14 -2
  150. {streamlit-1.47.0.dist-info → streamlit-1.48.0.dist-info}/RECORD +154 -154
  151. streamlit/static/static/css/index.CsLB_Bnz.css +0 -1
  152. streamlit/static/static/js/ProgressBar.d6X6RRog.js +0 -2
  153. streamlit/static/static/js/RenderInPortalIfExists.DEmedSWH.js +0 -1
  154. streamlit/static/static/js/Toolbar.D8wuuqZd.js +0 -1
  155. streamlit/static/static/js/createDownloadLinkElement.DZMwyjvU.js +0 -1
  156. streamlit/static/static/js/data-grid-overlay-editor.Bxxjm6-o.js +0 -1
  157. streamlit/static/static/js/index.0dTVhOfX.js +0 -1
  158. streamlit/static/static/js/index.4yEdNndV.js +0 -1
  159. streamlit/static/static/js/index.5n6QoSUA.js +0 -1
  160. streamlit/static/static/js/index.6nngkCAa.js +0 -2
  161. streamlit/static/static/js/index.94f3Fw7s.js +0 -1
  162. streamlit/static/static/js/index.B61tWuMK.js +0 -73
  163. streamlit/static/static/js/index.B7u9Flh8.js +0 -1
  164. streamlit/static/static/js/index.BASsihSP.js +0 -1
  165. streamlit/static/static/js/index.BL9HK8jG.js +0 -1
  166. streamlit/static/static/js/index.BQWRZ6al.js +0 -3855
  167. streamlit/static/static/js/index.BQXddtYi.js +0 -1
  168. streamlit/static/static/js/index.BUnHfDxu.js +0 -1
  169. streamlit/static/static/js/index.BWQlvt-p.js +0 -1
  170. streamlit/static/static/js/index.BhB2h-IN.js +0 -1
  171. streamlit/static/static/js/index.BjbAdGl1.js +0 -2
  172. streamlit/static/static/js/index.BqMXJWvY.js +0 -197
  173. streamlit/static/static/js/index.BteAxTUz.js +0 -1
  174. streamlit/static/static/js/index.ByH19tU7.js +0 -1
  175. streamlit/static/static/js/index.CH_JROvL.js +0 -1
  176. streamlit/static/static/js/index.CRMaL4_D.js +0 -1
  177. streamlit/static/static/js/index.CitqAWcf.js +0 -7
  178. streamlit/static/static/js/index.DNTjV0h_.js +0 -3
  179. streamlit/static/static/js/index.Df2TZ_h-.js +0 -1
  180. streamlit/static/static/js/index.GID7Wiqx.js +0 -1
  181. streamlit/static/static/js/index.hprQgsHY.js +0 -1
  182. streamlit/static/static/js/index.mJxNfDOA.js +0 -12
  183. streamlit/static/static/js/index.qkxYEW65.js +0 -1
  184. streamlit/static/static/js/useOnInputChange.CBaDeGVQ.js +0 -1
  185. streamlit/static/static/js/withFullScreenWrapper.DkugyWpW.js +0 -1
  186. {streamlit-1.47.0.data → streamlit-1.48.0.data}/scripts/streamlit.cmd +0 -0
  187. {streamlit-1.47.0.dist-info → streamlit-1.48.0.dist-info}/WHEEL +0 -0
  188. {streamlit-1.47.0.dist-info → streamlit-1.48.0.dist-info}/entry_points.txt +0 -0
  189. {streamlit-1.47.0.dist-info → streamlit-1.48.0.dist-info}/top_level.txt +0 -0
@@ -23,6 +23,7 @@ from streamlit.logger import get_logger
23
23
  from streamlit.watcher.folder_black_list import FolderBlackList
24
24
  from streamlit.watcher.path_watcher import (
25
25
  NoOpPathWatcher,
26
+ PathWatcherType,
26
27
  get_default_path_watcher_class,
27
28
  )
28
29
 
@@ -36,12 +37,12 @@ _LOGGER: Final = get_logger(__name__)
36
37
 
37
38
  class WatchedModule(NamedTuple):
38
39
  watcher: Any
39
- module_name: Any
40
+ module_name: str | None
40
41
 
41
42
 
42
43
  # This needs to be initialized lazily to avoid calling config.get_option() and
43
44
  # thus initializing config options when this file is first imported.
44
- PathWatcher = None
45
+ PathWatcher: PathWatcherType | None = None
45
46
 
46
47
 
47
48
  class LocalSourcesWatcher:
@@ -169,7 +170,7 @@ class LocalSourcesWatcher:
169
170
  glob_pattern = "**/*" if is_directory else None
170
171
 
171
172
  wm = WatchedModule(
172
- watcher=PathWatcher(
173
+ watcher=PathWatcher( # ty: ignore
173
174
  filepath,
174
175
  self.on_path_changed,
175
176
  glob_pattern=glob_pattern, # Pass as named parameter
@@ -335,23 +335,29 @@ def run(
335
335
  # by a debug websocket session.
336
336
  await server.stopped
337
337
 
338
- # Run the server. This function will not return until the server is shut down.
339
- # FIX RuntimeError: asyncio.run() cannot be called from a running event loop
340
- # asyncio.run(run_server()) # noqa: ERA001
341
-
342
338
  # Define a main function to handle the event loop logic
343
339
  async def main() -> None:
344
340
  await run_server()
345
341
 
342
+ # Handle running in existing event loop vs creating new one
343
+ running_in_event_loop = False
346
344
  try:
347
345
  # Check if we're already in an event loop
348
- if asyncio.get_running_loop().is_running():
349
- # Use `asyncio.create_task` if we're in an async context
350
- # TODO(lukasmasuch): Do we have to store a reference for the task here?
351
- asyncio.create_task(main()) # noqa: RUF006
352
- else:
353
- # Otherwise, use `asyncio.run`
354
- asyncio.run(main())
346
+ asyncio.get_running_loop()
347
+ running_in_event_loop = True
355
348
  except RuntimeError:
356
- # get_running_loop throws RuntimeError if no running event loop
349
+ # No running event loop - this is expected for normal CLI usage
350
+ pass
351
+
352
+ if running_in_event_loop:
353
+ _LOGGER.debug("Running server in existing event loop.")
354
+ # We're in an existing event loop.
355
+ task = asyncio.create_task(main(), name="bootstrap.run_server")
356
+ # Store task reference on the server to keep it alive
357
+ # This prevents the task from being garbage collected
358
+ server._bootstrap_task = task
359
+ else:
360
+ # No running event loop, so we can use asyncio.run
361
+ # This is the normal case when running streamlit from the command line
362
+ _LOGGER.debug("Starting new event loop for server")
357
363
  asyncio.run(main())
streamlit/web/cli.py CHANGED
@@ -114,13 +114,14 @@ def configurator_options(func: F) -> F:
114
114
  def _download_remote(main_script_path: str, url_path: str) -> None:
115
115
  """Fetch remote file at url_path to main_script_path."""
116
116
  import requests
117
+ from requests.exceptions import RequestException
117
118
 
118
119
  with open(main_script_path, "wb") as fp:
119
120
  try:
120
121
  resp = requests.get(url_path, timeout=30)
121
122
  resp.raise_for_status()
122
123
  fp.write(resp.content)
123
- except requests.exceptions.RequestException as e:
124
+ except RequestException as e:
124
125
  raise click.BadParameter(f"Unable to fetch {url_path}.\n{e}")
125
126
 
126
127
 
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import json
17
17
  from typing import Any, Final, cast
18
- from urllib.parse import urlparse
18
+ from urllib.parse import urlencode, urlparse
19
19
 
20
20
  import tornado.web
21
21
 
@@ -130,7 +130,63 @@ class AuthLoginHandler(AuthHandlerMixin, tornado.web.RequestHandler):
130
130
  class AuthLogoutHandler(AuthHandlerMixin, tornado.web.RequestHandler):
131
131
  def get(self) -> None:
132
132
  self.clear_auth_cookie()
133
- self.redirect_to_base()
133
+
134
+ provider_logout_url = self._get_provider_logout_url()
135
+ if provider_logout_url:
136
+ self.redirect(provider_logout_url)
137
+ else:
138
+ self.redirect_to_base()
139
+
140
+ def _get_redirect_root(self) -> str | None:
141
+ auth_section = get_secrets_auth_section()
142
+ if not auth_section or not auth_section.get("redirect_uri"):
143
+ return None
144
+
145
+ redirect_uri: str = auth_section["redirect_uri"]
146
+ if not redirect_uri.endswith("/oauth2callback"):
147
+ _LOGGER.warning("Redirect URI does not end with /oauth2callback")
148
+ return None
149
+
150
+ return redirect_uri.removesuffix("oauth2callback")
151
+
152
+ def _get_provider_logout_url(self) -> str | None:
153
+ """Get the OAuth provider's logout URL from OIDC metadata."""
154
+ try:
155
+ cookie_value = self.get_signed_cookie(AUTH_COOKIE_NAME)
156
+ except AttributeError: # Backward compatibility with Tornado < 6.3.0
157
+ cookie_value = self.get_secure_cookie(AUTH_COOKIE_NAME)
158
+
159
+ if not cookie_value:
160
+ return None
161
+
162
+ try:
163
+ user_info = json.loads(cookie_value)
164
+ provider = user_info.get("provider")
165
+ if not provider:
166
+ return None
167
+
168
+ client, _ = create_oauth_client(provider)
169
+
170
+ metadata = client.load_server_metadata()
171
+ end_session_endpoint = metadata.get("end_session_endpoint")
172
+
173
+ if not end_session_endpoint:
174
+ _LOGGER.info("No end_session_endpoint found for provider %s", provider)
175
+ return None
176
+
177
+ redirect_root = self._get_redirect_root()
178
+ if redirect_root is None:
179
+ _LOGGER.info("Redirect url could not be determined")
180
+ return None
181
+
182
+ logout_params = {"post_logout_redirect_uri": redirect_root}
183
+
184
+ # Not using id_token_hint as we don't store the id token
185
+ return f"{end_session_endpoint}?{urlencode(logout_params)}"
186
+
187
+ except Exception as e:
188
+ _LOGGER.warning("Failed to get provider logout URL: %s", e)
189
+ return None
134
190
 
135
191
 
136
192
  class AuthCallbackHandler(AuthHandlerMixin, tornado.web.RequestHandler):
@@ -172,7 +228,7 @@ class AuthCallbackHandler(AuthHandlerMixin, tornado.web.RequestHandler):
172
228
  token = client.authorize_access_token(self)
173
229
  user = cast("dict[str, Any]", token.get("userinfo"))
174
230
 
175
- cookie_value = dict(user, origin=origin, is_logged_in=True)
231
+ cookie_value = dict(user, origin=origin, is_logged_in=True, provider=provider)
176
232
  if user:
177
233
  self.set_auth_cookie(cookie_value)
178
234
  else:
@@ -243,6 +243,9 @@ class HostConfigHandler(_SpecialRequestHandler):
243
243
  "enforceDownloadInNewTab": False,
244
244
  "metricsUrl": "",
245
245
  "blockErrorDialogs": False,
246
+ # Determines whether the crossOrigin attribute is set on some elements, e.g. img, video, audio, and if
247
+ # so with which value. One of None, "anonymous", "use-credentials".
248
+ "resourceCrossOriginMode": None,
246
249
  }
247
250
  )
248
251
  self.set_status(200)
@@ -22,11 +22,7 @@ import sys
22
22
  from pathlib import Path
23
23
  from typing import TYPE_CHECKING, Any, Final
24
24
 
25
- import tornado.concurrent
26
- import tornado.locks
27
- import tornado.netutil
28
25
  import tornado.web
29
- import tornado.websocket
30
26
  from tornado.httpserver import HTTPServer
31
27
 
32
28
  from streamlit import cli_util, config, file_util, util
@@ -62,25 +58,67 @@ from streamlit.web.server.stats_request_handler import StatsRequestHandler
62
58
  from streamlit.web.server.upload_file_request_handler import UploadFileRequestHandler
63
59
 
64
60
  if TYPE_CHECKING:
61
+ import asyncio
65
62
  from collections.abc import Awaitable
66
63
  from ssl import SSLContext
67
64
 
68
65
  _LOGGER: Final = get_logger(__name__)
69
66
 
70
- TORNADO_SETTINGS = {
71
- # Gzip HTTP responses.
72
- "compress_response": True,
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
- # If we don't get a ping response within 30s, the connection
80
- # is timed out.
81
- "websocket_ping_timeout": 30,
82
- "xsrf_cookie_name": "_streamlit_xsrf",
83
- }
67
+
68
+ def _get_websocket_ping_interval_and_timeout() -> tuple[int, int]:
69
+ """Get the websocket ping interval and timeout from config or defaults.
70
+
71
+ Returns
72
+ -------
73
+ tuple: (ping_interval, ping_timeout)
74
+ """
75
+ configured_interval = config.get_option("server.websocketPingInterval")
76
+
77
+ if configured_interval is not None:
78
+ # User has explicitly set a value
79
+ interval = int(configured_interval)
80
+
81
+ # Warn if using Tornado 6.5+ with low interval
82
+ if not is_tornado_version_less_than("6.5.0") and interval < 30:
83
+ _LOGGER.warning(
84
+ "You have set server.websocketPingInterval to %s, but Tornado >= 6.5 "
85
+ "requires websocket_ping_interval >= websocket_ping_timeout. "
86
+ "To comply, we are setting both the ping interval and ping timeout to %s. "
87
+ "Depending on the specific deployment setup, this may cause connection issues.",
88
+ interval,
89
+ interval,
90
+ )
91
+
92
+ # When user configures interval, set timeout to match
93
+ return interval, interval
94
+
95
+ # Default behavior: respect Tornado version for interval, always 30s timeout
96
+ default_interval = 1 if is_tornado_version_less_than("6.5.0") else 30
97
+ return default_interval, 30
98
+
99
+
100
+ def get_tornado_settings() -> dict[str, Any]:
101
+ """Get Tornado settings for the server.
102
+
103
+ This is a function to allow for testing and dynamic configuration.
104
+ """
105
+ ping_interval, ping_timeout = _get_websocket_ping_interval_and_timeout()
106
+
107
+ return {
108
+ # Gzip HTTP responses.
109
+ "compress_response": True,
110
+ # Ping interval for websocket keepalive.
111
+ # With recent versions of Tornado, this value must be greater than or
112
+ # equal to websocket_ping_timeout.
113
+ # For details, see https://github.com/tornadoweb/tornado/pull/3376
114
+ # For compatibility with older versions of Tornado, we set the value to 1.
115
+ "websocket_ping_interval": ping_interval,
116
+ # If we don't get a ping response within this time, the connection
117
+ # is timed out.
118
+ "websocket_ping_timeout": ping_timeout,
119
+ "xsrf_cookie_name": "_streamlit_xsrf",
120
+ }
121
+
84
122
 
85
123
  # When server.port is not available it will look for the next available port
86
124
  # up to MAX_PORT_SEARCH_RETRIES.
@@ -191,8 +229,16 @@ def start_listening_unix_socket(http_server: HTTPServer) -> None:
191
229
  address = config.get_option("server.address")
192
230
  file_name = os.path.expanduser(address[len(UNIX_SOCKET_PREFIX) :])
193
231
 
194
- unix_socket = tornado.netutil.bind_unix_socket(file_name)
195
- http_server.add_socket(unix_socket)
232
+ import tornado.netutil
233
+
234
+ if hasattr(tornado.netutil, "bind_unix_socket"):
235
+ unix_socket = tornado.netutil.bind_unix_socket(file_name)
236
+ http_server.add_socket(unix_socket)
237
+ else:
238
+ _LOGGER.error(
239
+ "Unix socket support is not available in this version of Tornado."
240
+ )
241
+ sys.exit(1)
196
242
 
197
243
 
198
244
  def start_listening_tcp_socket(http_server: HTTPServer) -> None:
@@ -240,6 +286,11 @@ class Server:
240
286
 
241
287
  self._main_script_path = main_script_path
242
288
 
289
+ # The task that runs the server if an event loop is already running.
290
+ # We need to save a reference to it so that it doesn't get
291
+ # garbage collected while running.
292
+ self._bootstrap_task: asyncio.Task[None] | None = None
293
+
243
294
  # Initialize MediaFileStorage and its associated endpoint
244
295
  media_file_storage = MemoryMediaFileStorage(MEDIA_ENDPOINT)
245
296
  MediaFileHandler.initialize_storage(media_file_storage)
@@ -440,7 +491,7 @@ class Server:
440
491
  xsrf_cookies=is_xsrf_enabled(),
441
492
  # Set the websocket message size. The default value is too low.
442
493
  websocket_max_message_size=get_max_message_size_bytes(),
443
- **TORNADO_SETTINGS, # type: ignore[arg-type]
494
+ **get_tornado_settings(),
444
495
  )
445
496
 
446
497
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamlit
3
- Version: 1.47.0
3
+ Version: 1.48.0
4
4
  Summary: A faster way to build and share data apps
5
5
  Home-page: https://streamlit.io
6
6
  Author: Snowflake Inc
@@ -31,7 +31,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
31
31
  Classifier: Topic :: Software Development :: Widget Sets
32
32
  Requires-Python: >=3.9, !=3.9.7
33
33
  Description-Content-Type: text/markdown
34
- Requires-Dist: altair<6,>=4.0
34
+ Requires-Dist: altair!=5.4.0,!=5.4.1,<6,>=4.0
35
35
  Requires-Dist: blinker<2,>=1.5.0
36
36
  Requires-Dist: cachetools<7,>=4.0
37
37
  Requires-Dist: click<9,>=7.0
@@ -52,6 +52,18 @@ Requires-Dist: tornado!=6.5.0,<7,>=6.0.3
52
52
  Provides-Extra: snowflake
53
53
  Requires-Dist: snowflake-snowpark-python[modin]>=1.17.0; python_version < "3.12" and extra == "snowflake"
54
54
  Requires-Dist: snowflake-connector-python>=3.3.0; python_version < "3.12" and extra == "snowflake"
55
+ Provides-Extra: auth
56
+ Requires-Dist: Authlib>=1.3.2; extra == "auth"
57
+ Provides-Extra: charts
58
+ Requires-Dist: matplotlib>=3.0.0; extra == "charts"
59
+ Requires-Dist: graphviz>=0.19.0; extra == "charts"
60
+ Requires-Dist: plotly>=4.0.0; extra == "charts"
61
+ Requires-Dist: orjson>=3.5.0; extra == "charts"
62
+ Provides-Extra: sql
63
+ Requires-Dist: SQLAlchemy>=2.0.0; extra == "sql"
64
+ Provides-Extra: all
65
+ Requires-Dist: streamlit[auth,charts,snowflake,sql]; extra == "all"
66
+ Requires-Dist: rich>=11.0.0; extra == "all"
55
67
  Dynamic: author
56
68
  Dynamic: author-email
57
69
  Dynamic: classifier