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.
@@ -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
- # Please make sure to also update frontend/app/vite.config.ts
135
- # dev server proxy when changing or updating these endpoints as well
136
- # as the endpoints in frontend/connection/src/DefaultStreamlitEndpoints
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
- if e.errno == errno.EADDRINUSE:
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 already in use", port) # noqa: TRY400
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 already in use, trying to use the next one.", port
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 already in use, and "
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._runtime.stop()
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