streamlit-nightly 1.52.3.dev20260108__py3-none-any.whl → 1.52.3.dev20260110__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 (123) hide show
  1. streamlit/components/v2/__init__.py +3 -3
  2. streamlit/components/v2/types.py +4 -4
  3. streamlit/config.py +14 -0
  4. streamlit/elements/lib/options_selector_utils.py +98 -0
  5. streamlit/elements/widgets/multiselect.py +19 -9
  6. streamlit/elements/widgets/selectbox.py +12 -24
  7. streamlit/proto/MetricsEvent_pb2.py +4 -4
  8. streamlit/proto/MetricsEvent_pb2.pyi +4 -1
  9. streamlit/proto/PageProfile_pb2.py +6 -6
  10. streamlit/proto/PageProfile_pb2.pyi +11 -1
  11. streamlit/runtime/metrics_util.py +4 -0
  12. streamlit/runtime/runtime.py +3 -0
  13. streamlit/starlette.py +34 -0
  14. streamlit/static/index.html +1 -1
  15. streamlit/static/manifest.json +300 -300
  16. streamlit/static/static/js/{ErrorOutline.esm.j3b3OjAK.js → ErrorOutline.esm.BcqUpfNe.js} +1 -1
  17. streamlit/static/static/js/{FileDownload.esm.DCizXv6Q.js → FileDownload.esm.CtJWBuub.js} +1 -1
  18. streamlit/static/static/js/{FileHelper.EpMV5UVe.js → FileHelper.D0dQPhOs.js} +1 -1
  19. streamlit/static/static/js/{FormClearHelper.lF7Ran5M.js → FormClearHelper.Cm3GDSk6.js} +1 -1
  20. streamlit/static/static/js/{InputInstructions.CMvqhPhy.js → InputInstructions.D7Hxdzwv.js} +1 -1
  21. streamlit/static/static/js/{Particles.DsGe8psi.js → Particles.vAUDtAR8.js} +1 -1
  22. streamlit/static/static/js/{ProgressBar.DzoKn4D-.js → ProgressBar.Dp2CGRba.js} +2 -2
  23. streamlit/static/static/js/{StreamlitSyntaxHighlighter.CFab0b_b.js → StreamlitSyntaxHighlighter.DC0000nJ.js} +1 -1
  24. streamlit/static/static/js/{TableChart.esm.nZsTq1Sb.js → TableChart.esm.DDVoSKOT.js} +1 -1
  25. streamlit/static/static/js/{Toolbar.CFMvwQYl.js → Toolbar.CuMH-Gqe.js} +1 -1
  26. streamlit/static/static/js/{WidgetLabelHelpIconInline.D2EEUEQX.js → WidgetLabelHelpIconInline.WAReecol.js} +1 -1
  27. streamlit/static/static/js/{base-input.DKTA2QNz.js → base-input.BX9Jll5o.js} +4 -4
  28. streamlit/static/static/js/{checkbox.D9H9J-_W.js → checkbox.BzMHUSz1.js} +1 -1
  29. streamlit/static/static/js/{createDownloadLinkElement.DCk6EhPM.js → createDownloadLinkElement.CviG5BQx.js} +1 -1
  30. streamlit/static/static/js/{data-grid-overlay-editor.DExrGdqs.js → data-grid-overlay-editor.Dzo9A4l6.js} +1 -1
  31. streamlit/static/static/js/{downloader.CLJ7BreF.js → downloader.DIDvj0d5.js} +1 -1
  32. streamlit/static/static/js/{embed.CxOHZWx2.js → embed.C8qeQ38b.js} +6 -6
  33. streamlit/static/static/js/{es6.C99ebre4.js → es6.Dpcc-U7U.js} +2 -2
  34. streamlit/static/static/js/{formatNumber.D_w4fBsk.js → formatNumber.CPvuaBa8.js} +1 -1
  35. streamlit/static/static/js/{iconPosition.Cfhw1RkE.js → iconPosition.y0q-Rqem.js} +1 -1
  36. streamlit/static/static/js/{iframeResizer.contentWindow.BcWUIYOe.js → iframeResizer.contentWindow.DblExdXF.js} +1 -1
  37. streamlit/static/static/js/{index.BWK_h3IL.js → index.B-g6lwYa.js} +1 -1
  38. streamlit/static/static/js/{index.DoLorXMA.js → index.B10MmI2m.js} +1 -1
  39. streamlit/static/static/js/{index.Dx8TcTHV.js → index.B6vCS66f.js} +6 -6
  40. streamlit/static/static/js/{index.Bp_LrAiI.js → index.B9TG5Ah8.js} +1 -1
  41. streamlit/static/static/js/{index.CrJ9KZpt.js → index.BDb0PRvK.js} +1 -1
  42. streamlit/static/static/js/{index.1AemKTSK.js → index.BGufEmCz.js} +1 -1
  43. streamlit/static/static/js/{index.7Q3Iaebc.js → index.BItU4jMo.js} +1 -1
  44. streamlit/static/static/js/{index.D18KqoUa.js → index.BPMqWDef.js} +1 -1
  45. streamlit/static/static/js/index.BTHi5W25.js +1 -0
  46. streamlit/static/static/js/{index.BHx4Qw7z.js → index.BVN9cI-k.js} +1 -1
  47. streamlit/static/static/js/index.BWOP7HFT.js +1 -0
  48. streamlit/static/static/js/{index.DDx6TP95.js → index.BXYgO5B8.js} +1 -1
  49. streamlit/static/static/js/{index.DkSjHoXw.js → index.BZBWLU1C.js} +8 -8
  50. streamlit/static/static/js/{index.f_s01aPm.js → index.BfZdZpv-.js} +2 -2
  51. streamlit/static/static/js/{index.C7_5JMRC.js → index.Bxe7fKbw.js} +1 -1
  52. streamlit/static/static/js/{index.BcbR2mbc.js → index.C1ElSZEy.js} +1 -1
  53. streamlit/static/static/js/index.C26ZOVFL.js +1 -0
  54. streamlit/static/static/js/{index.aJ3XRx8R.js → index.C7lwRBvF.js} +1 -1
  55. streamlit/static/static/js/{index.CGX2fllG.js → index.C93QGPyk.js} +1 -1
  56. streamlit/static/static/js/{index.COjurlZk.js → index.CCDejIvL.js} +2 -2
  57. streamlit/static/static/js/index.CcMmNHAq.js +1 -0
  58. streamlit/static/static/js/index.D42y-GeO.js +1 -0
  59. streamlit/static/static/js/{index.C0VFHmJN.js → index.D69ULFWq.js} +1 -1
  60. streamlit/static/static/js/{index.V4C1Oi-F.js → index.DB4MbQ40.js} +3 -3
  61. streamlit/static/static/js/{index.DJ4GBc1k.js → index.DRFJgBf-.js} +1 -1
  62. streamlit/static/static/js/{index.BXQNt1hj.js → index.DTK-btqV.js} +1 -1
  63. streamlit/static/static/js/{index.CFE-yHdT.js → index.DZEQ0G7H.js} +1 -1
  64. streamlit/static/static/js/{index.Bu3Lto_G.js → index.DaU1ayM7.js} +1 -1
  65. streamlit/static/static/js/{index.DSSQzzPk.js → index.Dla9XiNe.js} +2 -2
  66. streamlit/static/static/js/{index.DiZfOR0A.js → index.DmQqT9OM.js} +1 -1
  67. streamlit/static/static/js/{index.DuFqxjbN.js → index.DppScppA.js} +1 -1
  68. streamlit/static/static/js/{index.Dqphk1ee.js → index.Dr8b3Vn6.js} +1 -1
  69. streamlit/static/static/js/{index.CPo5dtx7.js → index.DvQ7-afx.js} +1 -1
  70. streamlit/static/static/js/{index.ATP5607r.js → index.F-NmmwfK.js} +1 -1
  71. streamlit/static/static/js/{index.gnFSTAhI.js → index.MKI8t7l2.js} +1 -1
  72. streamlit/static/static/js/{index.BXnQdCa5.js → index.RRAKXgBZ.js} +1 -1
  73. streamlit/static/static/js/{index.CyVBY8PG.js → index.VSsf6tsu.js} +2 -2
  74. streamlit/static/static/js/{index.DcudoGfL.js → index.aEeM0ekc.js} +1 -1
  75. streamlit/static/static/js/{index.Ds-w0zIo.js → index.eweE9HKU.js} +1 -1
  76. streamlit/static/static/js/{index.CBZQ_6AF.js → index.gr6yGiCL.js} +1 -1
  77. streamlit/static/static/js/{index.mZ1qbnKs.js → index.kKkHk9Mc.js} +1 -1
  78. streamlit/static/static/js/index.pMvzHC7z.js +1 -0
  79. streamlit/static/static/js/{index.DIIdzDwK.js → index.pgifwCIr.js} +1 -1
  80. streamlit/static/static/js/index.q83b8_8b.js +1 -0
  81. streamlit/static/static/js/{index.BzQChe4y.js → index.u2AvcQQT.js} +1 -1
  82. streamlit/static/static/js/{index.CrzXL2V8.js → index.w2_VrtVw.js} +1 -1
  83. streamlit/static/static/js/{index.DbmtfcDm.js → index.wv1DYVsn.js} +1 -1
  84. streamlit/static/static/js/{index.COZICZL6.js → index.x4j6nnNb.js} +1 -1
  85. streamlit/static/static/js/{input.CZPD7mCu.js → input.BoJqOS7a.js} +1 -1
  86. streamlit/static/static/js/{main.CX1jAiMw.js → main.B5XmUwmQ.js} +1 -1
  87. streamlit/static/static/js/{memory.m0jC5ULx.js → memory.9izgq54V.js} +1 -1
  88. streamlit/static/static/js/{number-overlay-editor.Dy0iTeCo.js → number-overlay-editor.PqOGQcLn.js} +1 -1
  89. streamlit/static/static/js/{pandasStylerUtils.D9jj-wHU.js → pandasStylerUtils.DUtF2t3R.js} +1 -1
  90. streamlit/static/static/js/{sandbox.CbvG1iAz.js → sandbox.DgKTHPLO.js} +1 -1
  91. streamlit/static/static/js/{styled-components.Cw3ioniY.js → styled-components.D4x8jmOI.js} +1 -1
  92. streamlit/static/static/js/{throttle.CAwhGpn0.js → throttle.DxEHIIp7.js} +1 -1
  93. streamlit/static/static/js/{timepicker.Bh3m6Pjp.js → timepicker.CDks5DWI.js} +1 -1
  94. streamlit/static/static/js/{toConsumableArray.DM0o32JS.js → toConsumableArray.CvTQRsV-.js} +1 -1
  95. streamlit/static/static/js/uniqueId.RScLN3St.js +1 -0
  96. streamlit/static/static/js/{useBasicWidgetState.C9zOVP8a.js → useBasicWidgetState.D6_b0IqA.js} +1 -1
  97. streamlit/static/static/js/{useIntlLocale.Bo42aN1U.js → useIntlLocale.Ctz17A7g.js} +1 -1
  98. streamlit/static/static/js/{useTextInputAutoExpand.DmyHLDxl.js → useTextInputAutoExpand.PAFB5o1T.js} +1 -1
  99. streamlit/static/static/js/{useUpdateUiValue.aWXWpqmw.js → useUpdateUiValue.CcTvmGlb.js} +1 -1
  100. streamlit/static/static/js/{useWaveformController.DlE14M1X.js → useWaveformController.vLi36Ir4.js} +1 -1
  101. streamlit/static/static/js/{withCalculatedWidth.B5JFJSmG.js → withCalculatedWidth.BtA2jL5-.js} +1 -1
  102. streamlit/static/static/js/{withFullScreenWrapper.dgVioHk1.js → withFullScreenWrapper.CeeoYBpc.js} +1 -1
  103. streamlit/web/bootstrap.py +105 -10
  104. streamlit/web/cli.py +21 -4
  105. streamlit/web/server/app_discovery.py +421 -0
  106. streamlit/web/server/server.py +0 -13
  107. streamlit/web/server/starlette/__init__.py +2 -1
  108. streamlit/web/server/starlette/starlette_app.py +326 -3
  109. streamlit/web/server/starlette/starlette_routes.py +27 -8
  110. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/METADATA +1 -1
  111. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/RECORD +115 -113
  112. streamlit/static/static/js/index.BOGNGR9a.js +0 -1
  113. streamlit/static/static/js/index.C-lnh8pI.js +0 -1
  114. streamlit/static/static/js/index.C98anBCM.js +0 -1
  115. streamlit/static/static/js/index.CFtGP8pH.js +0 -1
  116. streamlit/static/static/js/index.Cg59Loqx.js +0 -1
  117. streamlit/static/static/js/index.CiU2Tdcl.js +0 -1
  118. streamlit/static/static/js/index.DpnqUQVD.js +0 -1
  119. streamlit/static/static/js/uniqueId.DtV_RZzG.js +0 -1
  120. {streamlit_nightly-1.52.3.dev20260108.data → streamlit_nightly-1.52.3.dev20260110.data}/scripts/streamlit.cmd +0 -0
  121. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/WHEEL +0 -0
  122. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/entry_points.txt +0 -0
  123. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,8 @@
17
17
  from __future__ import annotations
18
18
 
19
19
  from contextlib import asynccontextmanager
20
- from typing import TYPE_CHECKING, Any
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING, Any, Final
21
22
 
22
23
  from streamlit import config
23
24
  from streamlit.web.server.server_util import get_cookie_secret
@@ -26,6 +27,10 @@ from streamlit.web.server.starlette.starlette_app_utils import (
26
27
  )
27
28
  from streamlit.web.server.starlette.starlette_auth_routes import create_auth_routes
28
29
  from streamlit.web.server.starlette.starlette_routes import (
30
+ BASE_ROUTE_COMPONENT,
31
+ BASE_ROUTE_CORE,
32
+ BASE_ROUTE_MEDIA,
33
+ BASE_ROUTE_UPLOAD_FILE,
29
34
  create_app_static_serving_routes,
30
35
  create_bidi_component_routes,
31
36
  create_component_routes,
@@ -47,17 +52,26 @@ from streamlit.web.server.starlette.starlette_static_routes import (
47
52
  from streamlit.web.server.starlette.starlette_websocket import create_websocket_routes
48
53
 
49
54
  if TYPE_CHECKING:
50
- from collections.abc import AsyncIterator
55
+ from collections.abc import AsyncIterator, Callable, Mapping, Sequence
56
+ from contextlib import AbstractAsyncContextManager
51
57
 
52
58
  from starlette.applications import Starlette
53
59
  from starlette.middleware import Middleware
54
60
  from starlette.routing import BaseRoute
61
+ from starlette.types import ExceptionHandler, Receive, Scope, Send
55
62
 
56
63
  from streamlit.runtime import Runtime
57
64
  from streamlit.runtime.media_file_manager import MediaFileManager
58
65
  from streamlit.runtime.memory_media_file_storage import MemoryMediaFileStorage
59
66
  from streamlit.runtime.memory_uploaded_file_manager import MemoryUploadedFileManager
60
67
 
68
+ # Reserved route prefixes that users cannot override
69
+ _RESERVED_ROUTE_PREFIXES: Final[tuple[str, ...]] = (
70
+ f"/{BASE_ROUTE_CORE}/",
71
+ f"/{BASE_ROUTE_MEDIA}/",
72
+ f"/{BASE_ROUTE_COMPONENT}/",
73
+ )
74
+
61
75
 
62
76
  def create_streamlit_routes(runtime: Runtime) -> list[BaseRoute]:
63
77
  """Create the Streamlit-internal routes for the application.
@@ -210,4 +224,313 @@ def create_starlette_app(runtime: Runtime) -> Starlette:
210
224
  return Starlette(routes=routes, middleware=middleware, lifespan=_lifespan)
211
225
 
212
226
 
213
- __all__ = ["create_starlette_app"]
227
+ class App:
228
+ """ASGI-compatible Streamlit application.
229
+
230
+ .. warning::
231
+ This feature is experimental and may change or be removed in future
232
+ versions without warning. Use at your own risk.
233
+
234
+ This class provides a way to configure and run Streamlit applications
235
+ with custom routes, middleware, lifespan hooks, and exception handlers.
236
+
237
+ Parameters
238
+ ----------
239
+ script_path : str | Path
240
+ Path to the main Streamlit script. Can be absolute or relative. Relative
241
+ paths are resolved based on context: when started via ``streamlit run``,
242
+ they resolve relative to the main script; when started directly via uvicorn
243
+ or another ASGI server, they resolve relative to the current working directory.
244
+ lifespan : Callable[[App], AbstractAsyncContextManager[dict[str, Any] | None]] | None
245
+ Async context manager for startup/shutdown logic. The context manager
246
+ receives the App instance and can yield a dictionary of state that will
247
+ be accessible via ``app.state``.
248
+ routes : Sequence[BaseRoute] | None
249
+ Additional routes to mount alongside Streamlit. User routes are checked
250
+ against reserved Streamlit routes and will raise ValueError if they conflict.
251
+ middleware : Sequence[Middleware] | None
252
+ Middleware stack to apply to all requests. User middleware runs before
253
+ Streamlit's internal middleware.
254
+ exception_handlers : Mapping[Any, ExceptionHandler] | None
255
+ Custom exception handlers for user routes.
256
+ debug : bool
257
+ Enable debug mode for the underlying Starlette application.
258
+
259
+ Examples
260
+ --------
261
+ Basic usage:
262
+
263
+ >>> from streamlit.web.server.starlette import App
264
+ >>> app = App("main.py")
265
+
266
+ With lifespan hooks:
267
+
268
+ >>> from contextlib import asynccontextmanager
269
+ >>> from streamlit.web.server.starlette import App
270
+ >>>
271
+ >>> @asynccontextmanager
272
+ ... async def lifespan(app):
273
+ ... print("Starting up...")
274
+ ... yield {"model": "loaded"}
275
+ ... print("Shutting down...")
276
+ >>>
277
+ >>> app = App("main.py", lifespan=lifespan)
278
+
279
+ With custom routes:
280
+
281
+ >>> from starlette.routing import Route
282
+ >>> from starlette.responses import JSONResponse
283
+ >>> from streamlit.web.server.starlette import App
284
+ >>>
285
+ >>> async def health(request):
286
+ ... return JSONResponse({"status": "ok"})
287
+ >>>
288
+ >>> app = App("main.py", routes=[Route("/health", health)])
289
+ """
290
+
291
+ def __init__(
292
+ self,
293
+ script_path: str | Path,
294
+ *,
295
+ lifespan: (
296
+ Callable[[App], AbstractAsyncContextManager[dict[str, Any] | None]] | None
297
+ ) = None,
298
+ routes: Sequence[BaseRoute] | None = None,
299
+ middleware: Sequence[Middleware] | None = None,
300
+ exception_handlers: Mapping[Any, ExceptionHandler] | None = None,
301
+ debug: bool = False,
302
+ ) -> None:
303
+ self._script_path = Path(script_path)
304
+ self._user_lifespan = lifespan
305
+ self._user_routes = list(routes) if routes else []
306
+ self._user_middleware = list(middleware) if middleware else []
307
+ self._exception_handlers = (
308
+ dict(exception_handlers) if exception_handlers else {}
309
+ )
310
+ self._debug = debug
311
+
312
+ self._runtime: Runtime | None = None
313
+ self._starlette_app: Starlette | None = None
314
+ self._state: dict[str, Any] = {}
315
+ self._external_lifespan: bool = False
316
+
317
+ # Validate user routes don't conflict with reserved routes
318
+ self._validate_routes()
319
+
320
+ def _validate_routes(self) -> None:
321
+ """Validate that user routes don't conflict with reserved Streamlit routes."""
322
+ for route in self._user_routes:
323
+ path = getattr(route, "path", None)
324
+ if path:
325
+ for reserved in _RESERVED_ROUTE_PREFIXES:
326
+ if path.startswith(reserved) or path == reserved.rstrip("/"):
327
+ raise ValueError(
328
+ f"Route '{path}' conflicts with reserved Streamlit route "
329
+ f"prefix '{reserved}'. Use a different path like '/api/...'."
330
+ )
331
+
332
+ @property
333
+ def script_path(self) -> Path:
334
+ """The entry point script path."""
335
+ return self._script_path
336
+
337
+ @property
338
+ def state(self) -> dict[str, Any]:
339
+ """Application state, populated by lifespan context manager."""
340
+ return self._state
341
+
342
+ def lifespan(self) -> Callable[[Any], AbstractAsyncContextManager[None]]:
343
+ """Get a lifespan context manager for mounting on external ASGI frameworks.
344
+
345
+ Use this when mounting st.App as a sub-application on another framework
346
+ like FastAPI. The Streamlit runtime lifecycle will be managed by the
347
+ parent framework's lifespan instead of st.App's internal lifespan.
348
+
349
+ Returns
350
+ -------
351
+ Callable[[Any], AbstractAsyncContextManager[None]]
352
+ A lifespan context manager compatible with Starlette/FastAPI.
353
+
354
+ Examples
355
+ --------
356
+ Mount st.App on FastAPI:
357
+
358
+ >>> from fastapi import FastAPI
359
+ >>> from streamlit.starlette import App
360
+ >>>
361
+ >>> streamlit_app = App("dashboard.py")
362
+ >>> fastapi_app = FastAPI(lifespan=streamlit_app.lifespan())
363
+ >>> fastapi_app.mount("/dashboard", streamlit_app)
364
+ """
365
+ # Create runtime now (but don't start it - lifespan will do that)
366
+ if self._runtime is None:
367
+ self._runtime = self._create_runtime()
368
+
369
+ # Mark that lifespan is externally managed
370
+ self._external_lifespan = True
371
+
372
+ return self._combined_lifespan
373
+
374
+ def _resolve_script_path(self) -> Path:
375
+ """Resolve the script path to an absolute path.
376
+
377
+ Resolution order:
378
+ 1. If already absolute, return as-is
379
+ 2. If CLI set main_script_path (via `streamlit run`), resolve relative to it
380
+ 3. Otherwise, resolve relative to current working directory (e.g. when started via uvicorn)
381
+ """
382
+ if self._script_path.is_absolute():
383
+ return self._script_path
384
+
385
+ # Check if CLI set the main script path (streamlit run)
386
+ # This is set in cli.py before config is loaded
387
+ if config._main_script_path:
388
+ return (Path(config._main_script_path).parent / self._script_path).resolve()
389
+
390
+ # Fallback: resolve relative to cwd (direct uvicorn usage)
391
+ return self._script_path.resolve()
392
+
393
+ def _create_runtime(self) -> Runtime:
394
+ """Create the Streamlit runtime (but don't start it yet)."""
395
+ from streamlit.runtime import Runtime, RuntimeConfig
396
+ from streamlit.runtime.memory_media_file_storage import MemoryMediaFileStorage
397
+ from streamlit.runtime.memory_session_storage import MemorySessionStorage
398
+ from streamlit.runtime.memory_uploaded_file_manager import (
399
+ MemoryUploadedFileManager,
400
+ )
401
+ from streamlit.web.cache_storage_manager_config import (
402
+ create_default_cache_storage_manager,
403
+ )
404
+
405
+ script_path = self._resolve_script_path()
406
+
407
+ # Validate that the script file exists
408
+ if not script_path.is_file():
409
+ raise FileNotFoundError(
410
+ f"Streamlit script not found: '{script_path}'. "
411
+ f"Please verify that the path '{self._script_path}' is correct."
412
+ )
413
+
414
+ media_file_storage = MemoryMediaFileStorage(f"/{BASE_ROUTE_MEDIA}")
415
+ uploaded_file_mgr = MemoryUploadedFileManager(f"/{BASE_ROUTE_UPLOAD_FILE}")
416
+
417
+ return Runtime(
418
+ RuntimeConfig(
419
+ script_path=str(script_path),
420
+ command_line=None,
421
+ media_file_storage=media_file_storage,
422
+ uploaded_file_manager=uploaded_file_mgr,
423
+ cache_storage_manager=create_default_cache_storage_manager(),
424
+ is_hello=False,
425
+ session_storage=MemorySessionStorage(
426
+ ttl_seconds=config.get_option("server.disconnectedSessionTTL")
427
+ ),
428
+ ),
429
+ )
430
+
431
+ @asynccontextmanager
432
+ async def _combined_lifespan(self, _app: Starlette) -> AsyncIterator[None]:
433
+ """Combine Streamlit runtime lifecycle with user's lifespan.
434
+
435
+ The runtime must already be created (via _create_runtime) before this
436
+ lifespan runs. This lifespan handles starting and stopping the runtime.
437
+ """
438
+ from streamlit.web.bootstrap import prepare_streamlit_environment
439
+
440
+ if self._runtime is None:
441
+ raise RuntimeError(
442
+ "Runtime not initialized. Call _create_runtime before lifespan."
443
+ )
444
+
445
+ # Set server mode for metrics tracking.
446
+ # We need to detect if the app is mounted on another framework (FastAPI, etc.)
447
+ # based on the _external_lifespan flag, which is set when lifespan() is called.
448
+ if self._external_lifespan:
449
+ # App is mounted on another framework - this takes precedence over CLI mode
450
+ # because it reflects the actual architectural pattern being used.
451
+ config._server_mode = "asgi-mounted"
452
+ elif config._server_mode is None:
453
+ # Standalone st.App started directly via external ASGI server (not CLI)
454
+ config._server_mode = "asgi-server"
455
+ # If config._server_mode is already "starlette-app" (set by CLI) and
456
+ # _external_lifespan is False, keep it as "starlette-app"
457
+
458
+ # Prepare the Streamlit environment (secrets, pydeck, static folder check)
459
+ # Use resolved path to ensure correct directory for static folder check
460
+ prepare_streamlit_environment(str(self._resolve_script_path()))
461
+
462
+ # Start runtime (enables full cache support)
463
+ await self._runtime.start()
464
+
465
+ try:
466
+ # Run user's lifespan
467
+ if self._user_lifespan:
468
+ async with self._user_lifespan(self) as state:
469
+ if state:
470
+ self._state.update(state)
471
+ yield
472
+ else:
473
+ yield
474
+ finally:
475
+ # Stop runtime
476
+ self._runtime.stop()
477
+
478
+ def _build_starlette_app(self) -> Starlette:
479
+ """Build the Starlette application with all routes and middleware."""
480
+ from starlette.applications import Starlette
481
+
482
+ from streamlit.runtime import RuntimeState
483
+
484
+ # If lifespan() was called, the parent framework manages the lifecycle.
485
+ # Check if the runtime was actually started by the parent framework.
486
+ # If not, the user likely called lifespan() but then used the app standalone,
487
+ # which would result in the runtime never starting.
488
+ if self._external_lifespan:
489
+ runtime_not_started = (
490
+ self._runtime is None or self._runtime.state == RuntimeState.INITIAL
491
+ )
492
+ if runtime_not_started:
493
+ raise RuntimeError(
494
+ "Cannot use App as standalone ASGI application after calling "
495
+ "lifespan(). The lifespan() method should only be used when "
496
+ "mounting this App on another ASGI framework like FastAPI."
497
+ )
498
+
499
+ # Create the runtime if not already created
500
+ if self._runtime is None:
501
+ self._runtime = self._create_runtime()
502
+
503
+ # Get Streamlit's internal routes
504
+ streamlit_routes = create_streamlit_routes(self._runtime)
505
+
506
+ # User routes come first (higher priority), then Streamlit routes
507
+ # This allows users to override non-reserved routes like static files
508
+ all_routes = self._user_routes + streamlit_routes
509
+
510
+ # Get Streamlit's internal middleware
511
+ streamlit_middleware = create_streamlit_middleware()
512
+
513
+ # User middleware wraps Streamlit middleware (runs first on request,
514
+ # last on response)
515
+ all_middleware = self._user_middleware + streamlit_middleware
516
+
517
+ # If external lifespan, the parent manages lifecycle; otherwise use internal
518
+ app_lifespan = None if self._external_lifespan else self._combined_lifespan
519
+
520
+ return Starlette(
521
+ debug=self._debug,
522
+ routes=all_routes,
523
+ middleware=all_middleware,
524
+ exception_handlers=self._exception_handlers,
525
+ lifespan=app_lifespan,
526
+ )
527
+
528
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
529
+ """ASGI interface."""
530
+ if self._starlette_app is None:
531
+ self._starlette_app = self._build_starlette_app()
532
+
533
+ await self._starlette_app(scope, receive, send)
534
+
535
+
536
+ __all__ = ["App", "create_starlette_app"]
@@ -65,23 +65,28 @@ _LOGGER: Final = get_logger(__name__)
65
65
  # - frontend/app/vite.config.ts (dev server proxy configuration)
66
66
  # - frontend/connection/src/DefaultStreamlitEndpoints.ts
67
67
 
68
+ BASE_ROUTE_CORE: Final = "_stcore"
69
+ BASE_ROUTE_MEDIA: Final = "media"
70
+ BASE_ROUTE_UPLOAD_FILE: Final = f"{BASE_ROUTE_CORE}/upload_file"
71
+ BASE_ROUTE_COMPONENT: Final = "component"
72
+
68
73
  # Health check routes
69
- _ROUTE_HEALTH: Final = "_stcore/health"
70
- _ROUTE_SCRIPT_HEALTH: Final = "_stcore/script-health-check"
74
+ _ROUTE_HEALTH: Final = f"{BASE_ROUTE_CORE}/health"
75
+ _ROUTE_SCRIPT_HEALTH: Final = f"{BASE_ROUTE_CORE}/script-health-check"
71
76
 
72
77
  # Metrics routes
73
- _ROUTE_METRICS: Final = "_stcore/metrics"
78
+ _ROUTE_METRICS: Final = f"{BASE_ROUTE_CORE}/metrics"
74
79
 
75
80
  # Host configuration
76
- _ROUTE_HOST_CONFIG: Final = "_stcore/host-config"
81
+ _ROUTE_HOST_CONFIG: Final = f"{BASE_ROUTE_CORE}/host-config"
77
82
 
78
83
  # Media and file routes
79
- _ROUTE_MEDIA: Final = "media/{file_id:path}"
80
- _ROUTE_UPLOAD_FILE: Final = "_stcore/upload_file/{session_id}/{file_id}"
84
+ _ROUTE_MEDIA: Final = f"{BASE_ROUTE_MEDIA}/{{file_id:path}}"
85
+ _ROUTE_UPLOAD_FILE: Final = f"{BASE_ROUTE_UPLOAD_FILE}/{{session_id}}/{{file_id}}"
81
86
 
82
87
  # Component routes
83
- _ROUTE_COMPONENTS_V1: Final = "component/{path:path}"
84
- _ROUTE_COMPONENTS_V2: Final = "_stcore/bidi-components/{path:path}"
88
+ _ROUTE_COMPONENTS_V1: Final = f"{BASE_ROUTE_COMPONENT}/{{path:path}}"
89
+ _ROUTE_COMPONENTS_V2: Final = f"{BASE_ROUTE_CORE}/bidi-components/{{path:path}}"
85
90
 
86
91
  # App static files
87
92
  _ROUTE_APP_STATIC: Final = "app/static/{path:path}"
@@ -145,6 +150,11 @@ def _ensure_xsrf_cookie(request: Request, response: Response) -> None:
145
150
  The cookie is only set if XSRF protection is enabled in the configuration.
146
151
  The Secure flag is added when SSL is configured.
147
152
 
153
+ Note: The XSRF cookie intentionally does NOT have the HttpOnly flag. This
154
+ is required for the double-submit cookie pattern: JavaScript reads the
155
+ cookie value and includes it in the X-Xsrftoken request header, which the
156
+ server then compares against the cookie value to validate requests.
157
+
148
158
  Parameters
149
159
  ----------
150
160
  request
@@ -193,6 +203,15 @@ def _set_unquoted_cookie(
193
203
 
194
204
  If a cookie with the same name already exists, it is replaced.
195
205
 
206
+ Cookie flags set:
207
+ - Path=/: Available to all paths
208
+ - SameSite=Lax: Protects against CSRF while allowing top-level navigations
209
+ - Secure (conditional): Added when SSL is configured
210
+
211
+ HttpOnly is intentionally NOT set for XSRF cookies because JavaScript must
212
+ read the cookie value to include it in request headers (double-submit pattern).
213
+ This matches Tornado's behavior.
214
+
196
215
  Parameters
197
216
  ----------
198
217
  response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamlit-nightly
3
- Version: 1.52.3.dev20260108
3
+ Version: 1.52.3.dev20260110
4
4
  Summary: A faster way to build and share data apps
5
5
  Home-page: https://streamlit.io
6
6
  Author: Snowflake Inc