streamlit-nightly 1.52.3.dev20260106__py3-none-any.whl → 1.52.3.dev20260107__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 +33 -16
- streamlit/components/v2/bidi_component/main.py +0 -8
- streamlit/components/v2/types.py +13 -20
- streamlit/config.py +10 -0
- streamlit/elements/widgets/number_input.py +27 -24
- streamlit/errors.py +0 -12
- streamlit/runtime/context.py +22 -30
- streamlit/runtime/session_manager.py +35 -2
- streamlit/web/server/browser_websocket_handler.py +48 -5
- streamlit/web/server/server.py +43 -8
- streamlit/web/server/starlette/__init__.py +9 -0
- streamlit/web/server/starlette/starlette_routes.py +4 -0
- streamlit/web/server/starlette/starlette_server.py +496 -0
- streamlit/web/server/starlette/starlette_websocket.py +39 -0
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/RECORD +20 -19
- {streamlit_nightly-1.52.3.dev20260106.data → streamlit_nightly-1.52.3.dev20260107.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/top_level.txt +0 -0
streamlit/web/server/server.py
CHANGED
|
@@ -65,6 +65,8 @@ if TYPE_CHECKING:
|
|
|
65
65
|
from collections.abc import Awaitable
|
|
66
66
|
from ssl import SSLContext
|
|
67
67
|
|
|
68
|
+
from streamlit.web.server.starlette import UvicornServer
|
|
69
|
+
|
|
68
70
|
_LOGGER: Final = get_logger(__name__)
|
|
69
71
|
|
|
70
72
|
|
|
@@ -131,9 +133,11 @@ MAX_PORT_SEARCH_RETRIES: Final = 100
|
|
|
131
133
|
# to an unix socket.
|
|
132
134
|
UNIX_SOCKET_PREFIX: Final = "unix://"
|
|
133
135
|
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
#
|
|
136
|
+
# Server endpoint paths for the Streamlit API.
|
|
137
|
+
|
|
138
|
+
# IMPORTANT: Keep these in sync with:
|
|
139
|
+
# - frontend/app/vite.config.ts (dev server proxy configuration)
|
|
140
|
+
# - frontend/connection/src/DefaultStreamlitEndpoints.ts
|
|
137
141
|
MEDIA_ENDPOINT: Final = "/media"
|
|
138
142
|
COMPONENT_ENDPOINT: Final = "/component"
|
|
139
143
|
BIDI_COMPONENT_ENDPOINT: Final = "/_stcore/bidi-components"
|
|
@@ -258,13 +262,15 @@ def start_listening_tcp_socket(http_server: HTTPServer) -> None:
|
|
|
258
262
|
break # It worked! So let's break out of the loop.
|
|
259
263
|
|
|
260
264
|
except OSError as e:
|
|
261
|
-
|
|
265
|
+
# EADDRINUSE: port in use by another process
|
|
266
|
+
# EACCES: port reserved by system (common on Windows, see #13521)
|
|
267
|
+
if e.errno in (errno.EADDRINUSE, errno.EACCES):
|
|
262
268
|
if server_port_is_manually_set():
|
|
263
|
-
_LOGGER.error("Port %s is
|
|
269
|
+
_LOGGER.error("Port %s is not available", port) # noqa: TRY400
|
|
264
270
|
sys.exit(1)
|
|
265
271
|
else:
|
|
266
272
|
_LOGGER.debug(
|
|
267
|
-
"Port %s
|
|
273
|
+
"Port %s not available, trying to use the next one.", port
|
|
268
274
|
)
|
|
269
275
|
port += 1
|
|
270
276
|
|
|
@@ -277,7 +283,7 @@ def start_listening_tcp_socket(http_server: HTTPServer) -> None:
|
|
|
277
283
|
|
|
278
284
|
if call_count >= MAX_PORT_SEARCH_RETRIES:
|
|
279
285
|
raise RetriesExceededError(
|
|
280
|
-
f"Cannot start Streamlit server. Port {port} is
|
|
286
|
+
f"Cannot start Streamlit server. Port {port} is not available, and "
|
|
281
287
|
f"Streamlit was unable to find a free port after {MAX_PORT_SEARCH_RETRIES} attempts.",
|
|
282
288
|
)
|
|
283
289
|
|
|
@@ -289,6 +295,8 @@ class Server:
|
|
|
289
295
|
self.initialize_mimetypes()
|
|
290
296
|
|
|
291
297
|
self._main_script_path = main_script_path
|
|
298
|
+
self._use_starlette = bool(config.get_option("server.useStarlette"))
|
|
299
|
+
self._starlette_server: UvicornServer | None = None
|
|
292
300
|
|
|
293
301
|
# The task that runs the server if an event loop is already running.
|
|
294
302
|
# We need to save a reference to it so that it doesn't get
|
|
@@ -341,6 +349,11 @@ class Server:
|
|
|
341
349
|
|
|
342
350
|
_LOGGER.debug("Starting server...")
|
|
343
351
|
|
|
352
|
+
if self._use_starlette:
|
|
353
|
+
# Use starlette+uvicorn instead of tornado:
|
|
354
|
+
await self._start_starlette()
|
|
355
|
+
return
|
|
356
|
+
|
|
344
357
|
app = self._create_app()
|
|
345
358
|
start_listening(app)
|
|
346
359
|
|
|
@@ -352,6 +365,17 @@ class Server:
|
|
|
352
365
|
@property
|
|
353
366
|
def stopped(self) -> Awaitable[None]:
|
|
354
367
|
"""A Future that completes when the Server's run loop has exited."""
|
|
368
|
+
|
|
369
|
+
if self._starlette_server is not None:
|
|
370
|
+
|
|
371
|
+
async def _wait_for_starlette_stop() -> None:
|
|
372
|
+
if self._starlette_server is not None:
|
|
373
|
+
await self._starlette_server.stopped.wait()
|
|
374
|
+
# Also wait for the runtime to complete its shutdown
|
|
375
|
+
# (session cleanup, etc.) to ensure graceful shutdown.
|
|
376
|
+
await self._runtime.stopped
|
|
377
|
+
|
|
378
|
+
return _wait_for_starlette_stop()
|
|
355
379
|
return self._runtime.stopped
|
|
356
380
|
|
|
357
381
|
def _create_app(self) -> tornado.web.Application:
|
|
@@ -516,7 +540,18 @@ class Server:
|
|
|
516
540
|
|
|
517
541
|
def stop(self) -> None:
|
|
518
542
|
cli_util.print_to_cli(" Stopping...", fg="blue")
|
|
519
|
-
self.
|
|
543
|
+
if self._starlette_server is not None:
|
|
544
|
+
# Starlette's lifespan handler calls runtime.stop() during shutdown
|
|
545
|
+
self._starlette_server.stop()
|
|
546
|
+
else:
|
|
547
|
+
# Tornado mode: stop runtime directly
|
|
548
|
+
self._runtime.stop()
|
|
549
|
+
|
|
550
|
+
async def _start_starlette(self) -> None:
|
|
551
|
+
from streamlit.web.server.starlette import UvicornServer
|
|
552
|
+
|
|
553
|
+
self._starlette_server = UvicornServer(self._runtime)
|
|
554
|
+
await self._starlette_server.start()
|
|
520
555
|
|
|
521
556
|
|
|
522
557
|
def _set_tornado_log_levels() -> None:
|
|
@@ -11,3 +11,12 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from streamlit.web.server.starlette.starlette_app import create_starlette_app
|
|
16
|
+
from streamlit.web.server.starlette.starlette_server import UvicornRunner, UvicornServer
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"UvicornRunner",
|
|
20
|
+
"UvicornServer",
|
|
21
|
+
"create_starlette_app",
|
|
22
|
+
]
|
|
@@ -61,6 +61,10 @@ _LOGGER: Final = get_logger(__name__)
|
|
|
61
61
|
# Route path constants (without base URL prefix)
|
|
62
62
|
# These define the canonical paths for all Starlette server endpoints.
|
|
63
63
|
|
|
64
|
+
# IMPORTANT: Keep these in sync with:
|
|
65
|
+
# - frontend/app/vite.config.ts (dev server proxy configuration)
|
|
66
|
+
# - frontend/connection/src/DefaultStreamlitEndpoints.ts
|
|
67
|
+
|
|
64
68
|
# Health check routes
|
|
65
69
|
_ROUTE_HEALTH: Final = "_stcore/health"
|
|
66
70
|
_ROUTE_SCRIPT_HEALTH: Final = "_stcore/script-health-check"
|
|
@@ -0,0 +1,496 @@
|
|
|
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
|
+
"""Uvicorn server wrappers for running Streamlit applications (using Starlette).
|
|
16
|
+
|
|
17
|
+
This module provides two classes for running Streamlit apps with uvicorn:
|
|
18
|
+
|
|
19
|
+
1. **UvicornServer** (async): For embedding in an existing event loop.
|
|
20
|
+
Used by the `Server` class when `server.useStarlette=true`.
|
|
21
|
+
|
|
22
|
+
2. **UvicornRunner** (sync): For standalone CLI usage with blocking execution.
|
|
23
|
+
Used by `run_asgi_app()` when running `st.App` via `streamlit run`.
|
|
24
|
+
|
|
25
|
+
Why Two Classes?
|
|
26
|
+
----------------
|
|
27
|
+
These classes serve different architectural needs:
|
|
28
|
+
|
|
29
|
+
- **UvicornServer** integrates with Streamlit's existing `Server` class architecture,
|
|
30
|
+
which manages an event loop and coordinates multiple components (runtime, server,
|
|
31
|
+
signal handlers). It uses `uvicorn.Server` with manual socket binding for fine-grained
|
|
32
|
+
control and runs as a background task.
|
|
33
|
+
|
|
34
|
+
- **UvicornRunner** is designed for `st.App` mode where the app handles its own
|
|
35
|
+
runtime lifecycle via ASGI lifespan. It uses `uvicorn.run()` which manages its own
|
|
36
|
+
event loop and signal handlers - perfect for CLI "just run it" usage.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import asyncio
|
|
42
|
+
import errno
|
|
43
|
+
import socket
|
|
44
|
+
import sys
|
|
45
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
46
|
+
|
|
47
|
+
from streamlit import config
|
|
48
|
+
from streamlit.config_option import ConfigOption
|
|
49
|
+
from streamlit.logger import get_logger
|
|
50
|
+
from streamlit.runtime.runtime_util import get_max_message_size_bytes
|
|
51
|
+
from streamlit.web.server.starlette.starlette_app import create_starlette_app
|
|
52
|
+
from streamlit.web.server.starlette.starlette_server_config import (
|
|
53
|
+
DEFAULT_SERVER_ADDRESS,
|
|
54
|
+
DEFAULT_WEBSOCKET_PING_INTERVAL,
|
|
55
|
+
DEFAULT_WEBSOCKET_PING_TIMEOUT,
|
|
56
|
+
MAX_PORT_SEARCH_RETRIES,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if TYPE_CHECKING:
|
|
60
|
+
import uvicorn
|
|
61
|
+
|
|
62
|
+
from streamlit.runtime import Runtime
|
|
63
|
+
|
|
64
|
+
_LOGGER: Final = get_logger(__name__)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RetriesExceededError(Exception):
|
|
68
|
+
"""Raised when the server cannot find an available port after max retries."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Private utility functions for uvicorn configuration
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_server_address() -> str:
|
|
77
|
+
"""Get the server address from config, with default fallback."""
|
|
78
|
+
return config.get_option("server.address") or DEFAULT_SERVER_ADDRESS
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_server_port() -> int:
|
|
82
|
+
"""Get the server port from config."""
|
|
83
|
+
return int(config.get_option("server.port"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _is_port_manually_set() -> bool:
|
|
87
|
+
"""Check if the server port was explicitly configured by the user."""
|
|
88
|
+
return config.is_manually_set("server.port")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _server_address_is_unix_socket() -> bool:
|
|
92
|
+
"""Check if the server address is configured as a Unix socket."""
|
|
93
|
+
address = config.get_option("server.address")
|
|
94
|
+
return address is not None and address.startswith("unix://")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _validate_ssl_config() -> tuple[str | None, str | None]:
|
|
98
|
+
"""Validate and return SSL configuration.
|
|
99
|
+
|
|
100
|
+
Returns a tuple of (cert_file, key_file). Both are None if SSL is disabled,
|
|
101
|
+
or both are set if SSL is enabled. Exits if only one is set.
|
|
102
|
+
"""
|
|
103
|
+
cert_file = config.get_option("server.sslCertFile")
|
|
104
|
+
key_file = config.get_option("server.sslKeyFile")
|
|
105
|
+
|
|
106
|
+
# Validate SSL options: both must be set together or neither
|
|
107
|
+
if bool(cert_file) != bool(key_file):
|
|
108
|
+
_LOGGER.error(
|
|
109
|
+
"Options 'server.sslCertFile' and 'server.sslKeyFile' must "
|
|
110
|
+
"be set together. Set missing options or delete existing options."
|
|
111
|
+
)
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
return cert_file, key_file
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_websocket_settings() -> tuple[int, int]:
|
|
118
|
+
"""Get the WebSocket ping interval and timeout settings.
|
|
119
|
+
|
|
120
|
+
Returns a tuple of (ping_interval, ping_timeout) in seconds.
|
|
121
|
+
"""
|
|
122
|
+
configured_interval = config.get_option("server.websocketPingInterval")
|
|
123
|
+
|
|
124
|
+
if configured_interval is not None:
|
|
125
|
+
interval = int(configured_interval)
|
|
126
|
+
# For uvicorn, we set timeout equal to interval for consistency
|
|
127
|
+
return interval, interval
|
|
128
|
+
|
|
129
|
+
return DEFAULT_WEBSOCKET_PING_INTERVAL, DEFAULT_WEBSOCKET_PING_TIMEOUT
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_uvicorn_config_kwargs() -> dict[str, Any]:
|
|
133
|
+
"""Get common uvicorn configuration kwargs.
|
|
134
|
+
|
|
135
|
+
Returns a dict of kwargs that can be passed to uvicorn.Config or uvicorn.run().
|
|
136
|
+
Does NOT include app, host, or port - those must be provided separately.
|
|
137
|
+
"""
|
|
138
|
+
cert_file, key_file = _validate_ssl_config()
|
|
139
|
+
ws_ping_interval, ws_ping_timeout = _get_websocket_settings()
|
|
140
|
+
ws_max_size = get_max_message_size_bytes()
|
|
141
|
+
ws_per_message_deflate = config.get_option("server.enableWebsocketCompression")
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"ssl_certfile": cert_file,
|
|
145
|
+
"ssl_keyfile": key_file,
|
|
146
|
+
"ws": "auto",
|
|
147
|
+
"ws_ping_interval": ws_ping_interval,
|
|
148
|
+
"ws_ping_timeout": ws_ping_timeout,
|
|
149
|
+
"ws_max_size": ws_max_size,
|
|
150
|
+
"ws_per_message_deflate": ws_per_message_deflate,
|
|
151
|
+
"use_colors": False,
|
|
152
|
+
"log_config": None,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _bind_socket(address: str, port: int, backlog: int) -> socket.socket:
|
|
157
|
+
"""Bind a non-blocking TCP socket to the given address and port.
|
|
158
|
+
|
|
159
|
+
We pre-bind the socket ourselves (rather than letting uvicorn do it) to:
|
|
160
|
+
|
|
161
|
+
1. Detect port conflicts before creating the uvicorn.Server instance
|
|
162
|
+
2. Enable port retry logic when the configured port is already in use
|
|
163
|
+
3. Have explicit control over socket options (SO_REUSEADDR, IPV6_V6ONLY)
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
address
|
|
168
|
+
The IP address to bind to (e.g., "127.0.0.1" or "::").
|
|
169
|
+
|
|
170
|
+
port
|
|
171
|
+
The port number to bind to.
|
|
172
|
+
|
|
173
|
+
backlog
|
|
174
|
+
The maximum number of queued connections.
|
|
175
|
+
|
|
176
|
+
Returns
|
|
177
|
+
-------
|
|
178
|
+
socket.socket
|
|
179
|
+
A bound, listening, non-blocking socket.
|
|
180
|
+
"""
|
|
181
|
+
if ":" in address:
|
|
182
|
+
family = socket.AF_INET6
|
|
183
|
+
else:
|
|
184
|
+
family = socket.AF_INET
|
|
185
|
+
|
|
186
|
+
sock = socket.socket(family=family)
|
|
187
|
+
try:
|
|
188
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
189
|
+
|
|
190
|
+
if family == socket.AF_INET6:
|
|
191
|
+
# Allow both IPv4 and IPv6 clients when binding to "::".
|
|
192
|
+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
193
|
+
|
|
194
|
+
sock.bind((address, port))
|
|
195
|
+
sock.listen(backlog)
|
|
196
|
+
sock.setblocking(False)
|
|
197
|
+
sock.set_inheritable(True)
|
|
198
|
+
return sock
|
|
199
|
+
except BaseException:
|
|
200
|
+
sock.close()
|
|
201
|
+
raise
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# Server classes
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class UvicornServer:
|
|
210
|
+
"""Async uvicorn server for embedding in an existing event loop.
|
|
211
|
+
|
|
212
|
+
This class is used by Streamlit's `Server` class when `server.useStarlette=true`.
|
|
213
|
+
It wraps `uvicorn.Server` and provides:
|
|
214
|
+
|
|
215
|
+
- `start()`: Async method that returns when the server is ready to accept connections
|
|
216
|
+
- Background task execution: Server runs in background while caller continues
|
|
217
|
+
- `stop()`: Gracefully signal the server to shut down
|
|
218
|
+
- `stopped`: Event that fires when the server has fully stopped
|
|
219
|
+
|
|
220
|
+
This async design allows the `Server` class to coordinate multiple components
|
|
221
|
+
(runtime lifecycle, signal handlers, stop/stopped semantics) in its event loop.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
runtime
|
|
226
|
+
The Streamlit Runtime instance. Used to create the Starlette application
|
|
227
|
+
via `create_starlette_app(runtime)`.
|
|
228
|
+
|
|
229
|
+
Examples
|
|
230
|
+
--------
|
|
231
|
+
Used internally by Server._start_starlette():
|
|
232
|
+
|
|
233
|
+
>>> server = UvicornServer(runtime)
|
|
234
|
+
>>> await server.start() # Returns when ready
|
|
235
|
+
>>> # ... server running in background ...
|
|
236
|
+
>>> server.stop()
|
|
237
|
+
>>> await server.stopped.wait()
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(self, runtime: Runtime) -> None:
|
|
241
|
+
self._runtime = runtime
|
|
242
|
+
self._server: uvicorn.Server | None = None
|
|
243
|
+
self._server_task: asyncio.Task[None] | None = None
|
|
244
|
+
self._stopped_event = asyncio.Event()
|
|
245
|
+
self._socket: socket.socket | None = None
|
|
246
|
+
|
|
247
|
+
async def start(self) -> None:
|
|
248
|
+
"""Start the server and return when ready to accept connections."""
|
|
249
|
+
try:
|
|
250
|
+
import uvicorn
|
|
251
|
+
except ModuleNotFoundError as exc: # pragma: no cover
|
|
252
|
+
raise RuntimeError(
|
|
253
|
+
"uvicorn is required for server.useStarlette but is not installed. "
|
|
254
|
+
"Install it via `pip install streamlit[starlette]`."
|
|
255
|
+
) from exc
|
|
256
|
+
|
|
257
|
+
if _server_address_is_unix_socket():
|
|
258
|
+
raise RuntimeError(
|
|
259
|
+
"Unix sockets are not supported with Starlette currently."
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
app = create_starlette_app(self._runtime)
|
|
263
|
+
|
|
264
|
+
# Get server configuration
|
|
265
|
+
configured_address = _get_server_address()
|
|
266
|
+
configured_port = _get_server_port()
|
|
267
|
+
uvicorn_kwargs = _get_uvicorn_config_kwargs()
|
|
268
|
+
|
|
269
|
+
last_exception: BaseException | None = None
|
|
270
|
+
|
|
271
|
+
for attempt in range(MAX_PORT_SEARCH_RETRIES + 1):
|
|
272
|
+
port = configured_port + attempt
|
|
273
|
+
|
|
274
|
+
uvicorn_config = uvicorn.Config(
|
|
275
|
+
app,
|
|
276
|
+
host=configured_address,
|
|
277
|
+
port=port,
|
|
278
|
+
**uvicorn_kwargs,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
self._socket = _bind_socket(
|
|
283
|
+
configured_address,
|
|
284
|
+
port,
|
|
285
|
+
uvicorn_config.backlog,
|
|
286
|
+
)
|
|
287
|
+
except OSError as exc:
|
|
288
|
+
last_exception = exc
|
|
289
|
+
# EADDRINUSE: port in use by another process
|
|
290
|
+
# EACCES: port reserved by system (common on Windows, see #13521)
|
|
291
|
+
if exc.errno in (errno.EADDRINUSE, errno.EACCES):
|
|
292
|
+
if _is_port_manually_set():
|
|
293
|
+
_LOGGER.error("Port %s is not available", port) # noqa: TRY400
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
_LOGGER.debug(
|
|
296
|
+
"Port %s not available, trying to use the next one.", port
|
|
297
|
+
)
|
|
298
|
+
if attempt == MAX_PORT_SEARCH_RETRIES:
|
|
299
|
+
raise RetriesExceededError(
|
|
300
|
+
f"Cannot start Streamlit server. Port {port} is not available, "
|
|
301
|
+
f"and Streamlit was unable to find a free port after "
|
|
302
|
+
f"{MAX_PORT_SEARCH_RETRIES} attempts."
|
|
303
|
+
) from exc
|
|
304
|
+
continue
|
|
305
|
+
raise
|
|
306
|
+
|
|
307
|
+
self._server = uvicorn.Server(uvicorn_config)
|
|
308
|
+
config.set_option("server.port", port, ConfigOption.STREAMLIT_DEFINITION)
|
|
309
|
+
_LOGGER.debug(
|
|
310
|
+
"Starting uvicorn server on %s:%s",
|
|
311
|
+
configured_address,
|
|
312
|
+
port,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
startup_complete = asyncio.Event()
|
|
316
|
+
startup_exception: BaseException | None = None
|
|
317
|
+
|
|
318
|
+
async def serve_with_signal() -> None:
|
|
319
|
+
"""Serve the application with proper lifecycle management.
|
|
320
|
+
|
|
321
|
+
This ensures the server is shut down gracefully when the task is
|
|
322
|
+
cancelled or an exception occurs.
|
|
323
|
+
"""
|
|
324
|
+
nonlocal startup_exception
|
|
325
|
+
if self._server is None or self._socket is None:
|
|
326
|
+
raise RuntimeError("Server or socket not initialized")
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
# Initialize config and lifespan (normally done in _serve)
|
|
330
|
+
server_config = self._server.config
|
|
331
|
+
if not server_config.loaded:
|
|
332
|
+
server_config.load()
|
|
333
|
+
self._server.lifespan = server_config.lifespan_class(server_config)
|
|
334
|
+
|
|
335
|
+
await self._server.startup(sockets=[self._socket])
|
|
336
|
+
if self._server.should_exit:
|
|
337
|
+
startup_exception = RuntimeError("Server startup failed")
|
|
338
|
+
startup_complete.set() # noqa: B023
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
startup_complete.set() # noqa: B023
|
|
342
|
+
|
|
343
|
+
await self._server.main_loop()
|
|
344
|
+
except BaseException as e:
|
|
345
|
+
# Catch BaseException to handle CancelledError (which is not
|
|
346
|
+
# an Exception). This ensures startup_complete is set even if
|
|
347
|
+
# the task is cancelled before startup completes, preventing
|
|
348
|
+
# a deadlock in start() which awaits startup_complete.
|
|
349
|
+
startup_exception = e
|
|
350
|
+
raise
|
|
351
|
+
finally:
|
|
352
|
+
try:
|
|
353
|
+
if self._server is not None:
|
|
354
|
+
await self._server.shutdown(sockets=[self._socket])
|
|
355
|
+
finally:
|
|
356
|
+
# Ensure socket cleanup and stopped event are always set,
|
|
357
|
+
# even if shutdown raises an exception.
|
|
358
|
+
if self._socket is not None:
|
|
359
|
+
self._socket.close()
|
|
360
|
+
self._socket = None
|
|
361
|
+
self._stopped_event.set()
|
|
362
|
+
# Always set startup_complete to prevent deadlock in start()
|
|
363
|
+
# if task is cancelled before normal startup_complete.set().
|
|
364
|
+
startup_complete.set() # noqa: B023
|
|
365
|
+
|
|
366
|
+
self._server_task = asyncio.create_task(
|
|
367
|
+
serve_with_signal(), name="uvicorn-server"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
await startup_complete.wait()
|
|
371
|
+
|
|
372
|
+
if startup_exception is not None:
|
|
373
|
+
raise startup_exception
|
|
374
|
+
|
|
375
|
+
_LOGGER.info(
|
|
376
|
+
"Uvicorn server started on %s:%s",
|
|
377
|
+
configured_address,
|
|
378
|
+
port,
|
|
379
|
+
)
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
if last_exception is not None:
|
|
383
|
+
raise last_exception
|
|
384
|
+
|
|
385
|
+
def stop(self) -> None:
|
|
386
|
+
"""Signal the server to stop."""
|
|
387
|
+
if self._server is not None:
|
|
388
|
+
self._server.should_exit = True
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def stopped(self) -> asyncio.Event:
|
|
392
|
+
"""An event that is set when the server has fully stopped."""
|
|
393
|
+
return self._stopped_event
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class UvicornRunner:
|
|
397
|
+
"""Sync uvicorn runner for standalone CLI usage.
|
|
398
|
+
|
|
399
|
+
This class is used by `run_asgi_app()` when running `st.App` via `streamlit run`.
|
|
400
|
+
It wraps `uvicorn.run()` which is a blocking call that:
|
|
401
|
+
|
|
402
|
+
- Creates and manages its own event loop
|
|
403
|
+
- Handles OS signals (SIGINT, SIGTERM) for graceful shutdown
|
|
404
|
+
- Runs until the server exits
|
|
405
|
+
|
|
406
|
+
This is ideal for `st.App` mode because:
|
|
407
|
+
|
|
408
|
+
- The `st.App` handles its own runtime lifecycle via ASGI lifespan hooks
|
|
409
|
+
- No external coordination is needed - uvicorn manages everything
|
|
410
|
+
- Simple "run and block" semantics for CLI usage
|
|
411
|
+
|
|
412
|
+
Parameters
|
|
413
|
+
----------
|
|
414
|
+
app
|
|
415
|
+
Either an ASGI app instance or an import string (e.g., "myapp:app").
|
|
416
|
+
Import strings are preferred as they allow uvicorn to handle the import.
|
|
417
|
+
|
|
418
|
+
Examples
|
|
419
|
+
--------
|
|
420
|
+
Used by bootstrap.run_asgi_app():
|
|
421
|
+
|
|
422
|
+
>>> runner = UvicornRunner("myapp:app")
|
|
423
|
+
>>> runner.run() # Blocks until server exits
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
def __init__(self, app: str) -> None:
|
|
427
|
+
self._app = app
|
|
428
|
+
|
|
429
|
+
def run(self) -> None:
|
|
430
|
+
"""Run the server synchronously (blocking until exit).
|
|
431
|
+
|
|
432
|
+
This method blocks until the server exits, either from a signal
|
|
433
|
+
(Ctrl+C, SIGTERM) or an error. It handles port retry automatically
|
|
434
|
+
if the configured port is not available.
|
|
435
|
+
"""
|
|
436
|
+
try:
|
|
437
|
+
import uvicorn
|
|
438
|
+
except ModuleNotFoundError as exc: # pragma: no cover
|
|
439
|
+
raise RuntimeError(
|
|
440
|
+
"uvicorn is required for running st.App. "
|
|
441
|
+
"Install it with: pip install uvicorn"
|
|
442
|
+
) from exc
|
|
443
|
+
|
|
444
|
+
if _server_address_is_unix_socket():
|
|
445
|
+
raise RuntimeError("Unix sockets are not supported with st.App currently.")
|
|
446
|
+
|
|
447
|
+
# Get server configuration
|
|
448
|
+
configured_address = _get_server_address()
|
|
449
|
+
configured_port = _get_server_port()
|
|
450
|
+
uvicorn_kwargs = _get_uvicorn_config_kwargs()
|
|
451
|
+
|
|
452
|
+
# Port retry loop - try successive ports if the configured one is busy
|
|
453
|
+
for attempt in range(MAX_PORT_SEARCH_RETRIES + 1):
|
|
454
|
+
port = configured_port + attempt
|
|
455
|
+
|
|
456
|
+
if attempt > 0:
|
|
457
|
+
config.set_option(
|
|
458
|
+
"server.port", port, ConfigOption.STREAMLIT_DEFINITION
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# TODO(lukasmasuch): Print the URL with the selected port.
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
_LOGGER.debug(
|
|
465
|
+
"Starting uvicorn runner on %s:%s",
|
|
466
|
+
configured_address,
|
|
467
|
+
port,
|
|
468
|
+
)
|
|
469
|
+
uvicorn.run(
|
|
470
|
+
self._app,
|
|
471
|
+
host=configured_address,
|
|
472
|
+
port=port,
|
|
473
|
+
**uvicorn_kwargs,
|
|
474
|
+
)
|
|
475
|
+
return # Server exited normally
|
|
476
|
+
except OSError as exc:
|
|
477
|
+
# EADDRINUSE: port in use by another process
|
|
478
|
+
# EACCES: port reserved by system (common on Windows)
|
|
479
|
+
if exc.errno in (errno.EADDRINUSE, errno.EACCES):
|
|
480
|
+
if _is_port_manually_set():
|
|
481
|
+
_LOGGER.error("Port %s is not available", port) # noqa: TRY400
|
|
482
|
+
sys.exit(1)
|
|
483
|
+
_LOGGER.debug(
|
|
484
|
+
"Port %s not available, trying to use the next one.", port
|
|
485
|
+
)
|
|
486
|
+
if attempt == MAX_PORT_SEARCH_RETRIES:
|
|
487
|
+
_LOGGER.error( # noqa: TRY400
|
|
488
|
+
"Cannot start Streamlit server. Port %s is not available, "
|
|
489
|
+
"and Streamlit was unable to find a free port after "
|
|
490
|
+
"%s attempts.",
|
|
491
|
+
port,
|
|
492
|
+
MAX_PORT_SEARCH_RETRIES,
|
|
493
|
+
)
|
|
494
|
+
sys.exit(1)
|
|
495
|
+
continue
|
|
496
|
+
raise
|