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.
- streamlit/components/v2/__init__.py +3 -3
- streamlit/components/v2/types.py +4 -4
- streamlit/config.py +14 -0
- streamlit/elements/lib/options_selector_utils.py +98 -0
- streamlit/elements/widgets/multiselect.py +19 -9
- streamlit/elements/widgets/selectbox.py +12 -24
- streamlit/proto/MetricsEvent_pb2.py +4 -4
- streamlit/proto/MetricsEvent_pb2.pyi +4 -1
- streamlit/proto/PageProfile_pb2.py +6 -6
- streamlit/proto/PageProfile_pb2.pyi +11 -1
- streamlit/runtime/metrics_util.py +4 -0
- streamlit/runtime/runtime.py +3 -0
- streamlit/starlette.py +34 -0
- streamlit/static/index.html +1 -1
- streamlit/static/manifest.json +300 -300
- streamlit/static/static/js/{ErrorOutline.esm.j3b3OjAK.js → ErrorOutline.esm.BcqUpfNe.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.DCizXv6Q.js → FileDownload.esm.CtJWBuub.js} +1 -1
- streamlit/static/static/js/{FileHelper.EpMV5UVe.js → FileHelper.D0dQPhOs.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.lF7Ran5M.js → FormClearHelper.Cm3GDSk6.js} +1 -1
- streamlit/static/static/js/{InputInstructions.CMvqhPhy.js → InputInstructions.D7Hxdzwv.js} +1 -1
- streamlit/static/static/js/{Particles.DsGe8psi.js → Particles.vAUDtAR8.js} +1 -1
- streamlit/static/static/js/{ProgressBar.DzoKn4D-.js → ProgressBar.Dp2CGRba.js} +2 -2
- streamlit/static/static/js/{StreamlitSyntaxHighlighter.CFab0b_b.js → StreamlitSyntaxHighlighter.DC0000nJ.js} +1 -1
- streamlit/static/static/js/{TableChart.esm.nZsTq1Sb.js → TableChart.esm.DDVoSKOT.js} +1 -1
- streamlit/static/static/js/{Toolbar.CFMvwQYl.js → Toolbar.CuMH-Gqe.js} +1 -1
- streamlit/static/static/js/{WidgetLabelHelpIconInline.D2EEUEQX.js → WidgetLabelHelpIconInline.WAReecol.js} +1 -1
- streamlit/static/static/js/{base-input.DKTA2QNz.js → base-input.BX9Jll5o.js} +4 -4
- streamlit/static/static/js/{checkbox.D9H9J-_W.js → checkbox.BzMHUSz1.js} +1 -1
- streamlit/static/static/js/{createDownloadLinkElement.DCk6EhPM.js → createDownloadLinkElement.CviG5BQx.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.DExrGdqs.js → data-grid-overlay-editor.Dzo9A4l6.js} +1 -1
- streamlit/static/static/js/{downloader.CLJ7BreF.js → downloader.DIDvj0d5.js} +1 -1
- streamlit/static/static/js/{embed.CxOHZWx2.js → embed.C8qeQ38b.js} +6 -6
- streamlit/static/static/js/{es6.C99ebre4.js → es6.Dpcc-U7U.js} +2 -2
- streamlit/static/static/js/{formatNumber.D_w4fBsk.js → formatNumber.CPvuaBa8.js} +1 -1
- streamlit/static/static/js/{iconPosition.Cfhw1RkE.js → iconPosition.y0q-Rqem.js} +1 -1
- streamlit/static/static/js/{iframeResizer.contentWindow.BcWUIYOe.js → iframeResizer.contentWindow.DblExdXF.js} +1 -1
- streamlit/static/static/js/{index.BWK_h3IL.js → index.B-g6lwYa.js} +1 -1
- streamlit/static/static/js/{index.DoLorXMA.js → index.B10MmI2m.js} +1 -1
- streamlit/static/static/js/{index.Dx8TcTHV.js → index.B6vCS66f.js} +6 -6
- streamlit/static/static/js/{index.Bp_LrAiI.js → index.B9TG5Ah8.js} +1 -1
- streamlit/static/static/js/{index.CrJ9KZpt.js → index.BDb0PRvK.js} +1 -1
- streamlit/static/static/js/{index.1AemKTSK.js → index.BGufEmCz.js} +1 -1
- streamlit/static/static/js/{index.7Q3Iaebc.js → index.BItU4jMo.js} +1 -1
- streamlit/static/static/js/{index.D18KqoUa.js → index.BPMqWDef.js} +1 -1
- streamlit/static/static/js/index.BTHi5W25.js +1 -0
- streamlit/static/static/js/{index.BHx4Qw7z.js → index.BVN9cI-k.js} +1 -1
- streamlit/static/static/js/index.BWOP7HFT.js +1 -0
- streamlit/static/static/js/{index.DDx6TP95.js → index.BXYgO5B8.js} +1 -1
- streamlit/static/static/js/{index.DkSjHoXw.js → index.BZBWLU1C.js} +8 -8
- streamlit/static/static/js/{index.f_s01aPm.js → index.BfZdZpv-.js} +2 -2
- streamlit/static/static/js/{index.C7_5JMRC.js → index.Bxe7fKbw.js} +1 -1
- streamlit/static/static/js/{index.BcbR2mbc.js → index.C1ElSZEy.js} +1 -1
- streamlit/static/static/js/index.C26ZOVFL.js +1 -0
- streamlit/static/static/js/{index.aJ3XRx8R.js → index.C7lwRBvF.js} +1 -1
- streamlit/static/static/js/{index.CGX2fllG.js → index.C93QGPyk.js} +1 -1
- streamlit/static/static/js/{index.COjurlZk.js → index.CCDejIvL.js} +2 -2
- streamlit/static/static/js/index.CcMmNHAq.js +1 -0
- streamlit/static/static/js/index.D42y-GeO.js +1 -0
- streamlit/static/static/js/{index.C0VFHmJN.js → index.D69ULFWq.js} +1 -1
- streamlit/static/static/js/{index.V4C1Oi-F.js → index.DB4MbQ40.js} +3 -3
- streamlit/static/static/js/{index.DJ4GBc1k.js → index.DRFJgBf-.js} +1 -1
- streamlit/static/static/js/{index.BXQNt1hj.js → index.DTK-btqV.js} +1 -1
- streamlit/static/static/js/{index.CFE-yHdT.js → index.DZEQ0G7H.js} +1 -1
- streamlit/static/static/js/{index.Bu3Lto_G.js → index.DaU1ayM7.js} +1 -1
- streamlit/static/static/js/{index.DSSQzzPk.js → index.Dla9XiNe.js} +2 -2
- streamlit/static/static/js/{index.DiZfOR0A.js → index.DmQqT9OM.js} +1 -1
- streamlit/static/static/js/{index.DuFqxjbN.js → index.DppScppA.js} +1 -1
- streamlit/static/static/js/{index.Dqphk1ee.js → index.Dr8b3Vn6.js} +1 -1
- streamlit/static/static/js/{index.CPo5dtx7.js → index.DvQ7-afx.js} +1 -1
- streamlit/static/static/js/{index.ATP5607r.js → index.F-NmmwfK.js} +1 -1
- streamlit/static/static/js/{index.gnFSTAhI.js → index.MKI8t7l2.js} +1 -1
- streamlit/static/static/js/{index.BXnQdCa5.js → index.RRAKXgBZ.js} +1 -1
- streamlit/static/static/js/{index.CyVBY8PG.js → index.VSsf6tsu.js} +2 -2
- streamlit/static/static/js/{index.DcudoGfL.js → index.aEeM0ekc.js} +1 -1
- streamlit/static/static/js/{index.Ds-w0zIo.js → index.eweE9HKU.js} +1 -1
- streamlit/static/static/js/{index.CBZQ_6AF.js → index.gr6yGiCL.js} +1 -1
- streamlit/static/static/js/{index.mZ1qbnKs.js → index.kKkHk9Mc.js} +1 -1
- streamlit/static/static/js/index.pMvzHC7z.js +1 -0
- streamlit/static/static/js/{index.DIIdzDwK.js → index.pgifwCIr.js} +1 -1
- streamlit/static/static/js/index.q83b8_8b.js +1 -0
- streamlit/static/static/js/{index.BzQChe4y.js → index.u2AvcQQT.js} +1 -1
- streamlit/static/static/js/{index.CrzXL2V8.js → index.w2_VrtVw.js} +1 -1
- streamlit/static/static/js/{index.DbmtfcDm.js → index.wv1DYVsn.js} +1 -1
- streamlit/static/static/js/{index.COZICZL6.js → index.x4j6nnNb.js} +1 -1
- streamlit/static/static/js/{input.CZPD7mCu.js → input.BoJqOS7a.js} +1 -1
- streamlit/static/static/js/{main.CX1jAiMw.js → main.B5XmUwmQ.js} +1 -1
- streamlit/static/static/js/{memory.m0jC5ULx.js → memory.9izgq54V.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.Dy0iTeCo.js → number-overlay-editor.PqOGQcLn.js} +1 -1
- streamlit/static/static/js/{pandasStylerUtils.D9jj-wHU.js → pandasStylerUtils.DUtF2t3R.js} +1 -1
- streamlit/static/static/js/{sandbox.CbvG1iAz.js → sandbox.DgKTHPLO.js} +1 -1
- streamlit/static/static/js/{styled-components.Cw3ioniY.js → styled-components.D4x8jmOI.js} +1 -1
- streamlit/static/static/js/{throttle.CAwhGpn0.js → throttle.DxEHIIp7.js} +1 -1
- streamlit/static/static/js/{timepicker.Bh3m6Pjp.js → timepicker.CDks5DWI.js} +1 -1
- streamlit/static/static/js/{toConsumableArray.DM0o32JS.js → toConsumableArray.CvTQRsV-.js} +1 -1
- streamlit/static/static/js/uniqueId.RScLN3St.js +1 -0
- streamlit/static/static/js/{useBasicWidgetState.C9zOVP8a.js → useBasicWidgetState.D6_b0IqA.js} +1 -1
- streamlit/static/static/js/{useIntlLocale.Bo42aN1U.js → useIntlLocale.Ctz17A7g.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.DmyHLDxl.js → useTextInputAutoExpand.PAFB5o1T.js} +1 -1
- streamlit/static/static/js/{useUpdateUiValue.aWXWpqmw.js → useUpdateUiValue.CcTvmGlb.js} +1 -1
- streamlit/static/static/js/{useWaveformController.DlE14M1X.js → useWaveformController.vLi36Ir4.js} +1 -1
- streamlit/static/static/js/{withCalculatedWidth.B5JFJSmG.js → withCalculatedWidth.BtA2jL5-.js} +1 -1
- streamlit/static/static/js/{withFullScreenWrapper.dgVioHk1.js → withFullScreenWrapper.CeeoYBpc.js} +1 -1
- streamlit/web/bootstrap.py +105 -10
- streamlit/web/cli.py +21 -4
- streamlit/web/server/app_discovery.py +421 -0
- streamlit/web/server/server.py +0 -13
- streamlit/web/server/starlette/__init__.py +2 -1
- streamlit/web/server/starlette/starlette_app.py +326 -3
- streamlit/web/server/starlette/starlette_routes.py +27 -8
- {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/RECORD +115 -113
- streamlit/static/static/js/index.BOGNGR9a.js +0 -1
- streamlit/static/static/js/index.C-lnh8pI.js +0 -1
- streamlit/static/static/js/index.C98anBCM.js +0 -1
- streamlit/static/static/js/index.CFtGP8pH.js +0 -1
- streamlit/static/static/js/index.Cg59Loqx.js +0 -1
- streamlit/static/static/js/index.CiU2Tdcl.js +0 -1
- streamlit/static/static/js/index.DpnqUQVD.js +0 -1
- streamlit/static/static/js/uniqueId.DtV_RZzG.js +0 -1
- {streamlit_nightly-1.52.3.dev20260108.data → streamlit_nightly-1.52.3.dev20260110.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
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 = "
|
|
70
|
-
_ROUTE_SCRIPT_HEALTH: Final = "
|
|
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 = "
|
|
78
|
+
_ROUTE_METRICS: Final = f"{BASE_ROUTE_CORE}/metrics"
|
|
74
79
|
|
|
75
80
|
# Host configuration
|
|
76
|
-
_ROUTE_HOST_CONFIG: Final = "
|
|
81
|
+
_ROUTE_HOST_CONFIG: Final = f"{BASE_ROUTE_CORE}/host-config"
|
|
77
82
|
|
|
78
83
|
# Media and file routes
|
|
79
|
-
_ROUTE_MEDIA: Final = "
|
|
80
|
-
_ROUTE_UPLOAD_FILE: Final = "
|
|
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 = "
|
|
84
|
-
_ROUTE_COMPONENTS_V2: Final = "
|
|
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
|