Startup box: server.run() prints app name, version and connect URL in a rounded unicode box on stderr. Shared termwidth module for ANSI-aware unicode display width and padding.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Uvicorn server runner with multi-endpoint support."""
|
||||
|
||||
import asyncio
|
||||
import importlib.metadata
|
||||
import logging
|
||||
import os
|
||||
from contextlib import suppress
|
||||
@@ -13,10 +14,55 @@ from uvicorn import Config, Server
|
||||
|
||||
from .hostutil import parse_endpoints
|
||||
from .logging import install_access_log, patch_log_config
|
||||
from .startupbox import print_box
|
||||
|
||||
tracerite.load() # Early load on CLI load (import server); uvicorn workers reload via log config
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_WILDCARD_HOSTS = frozenset({"0.0.0.0", "::"}) # noqa: S104
|
||||
|
||||
|
||||
def _connect_url(endpoints: list[dict]) -> str:
|
||||
"""Return a URL the user can connect to for the first TCP endpoint.
|
||||
|
||||
Wildcard binds (0.0.0.0, ::) are shown as localhost, as that is the
|
||||
address a user can actually open. Returns "" for unix-socket-only setups.
|
||||
"""
|
||||
for endpoint in endpoints:
|
||||
host = endpoint.get("host")
|
||||
if host is None:
|
||||
continue
|
||||
if host in _WILDCARD_HOSTS:
|
||||
host = "localhost"
|
||||
elif ":" in host: # IPv6 literal
|
||||
host = f"[{host}]"
|
||||
return f"http://{host}:{endpoint['port']}"
|
||||
return ""
|
||||
|
||||
|
||||
def _print_startup_box(template: str, app: str, endpoints: list[dict]) -> None:
|
||||
"""Format the startup box template and print it.
|
||||
|
||||
Available fields: ``{module}`` (top-level package of the app path),
|
||||
``{name}`` (module with spaces instead of underscores), ``{Name}``
|
||||
(also capitalized), ``{version}`` (from installed package metadata,
|
||||
"dev" when not installed) and ``{url}``.
|
||||
"""
|
||||
module = app.split(":", 1)[0].split(".", 1)[0]
|
||||
name = module.replace("_", " ")
|
||||
try:
|
||||
version = importlib.metadata.version(module)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
version = ""
|
||||
values = {
|
||||
"module": module,
|
||||
"name": name,
|
||||
"Name": name.title(),
|
||||
"version": version,
|
||||
"url": _connect_url(endpoints),
|
||||
}
|
||||
print_box(template.format_map(values))
|
||||
|
||||
|
||||
def run( # noqa: PLR0913
|
||||
app: str,
|
||||
@@ -26,6 +72,7 @@ def run( # noqa: PLR0913
|
||||
reload: bool | Path = False,
|
||||
workers: int | None = None,
|
||||
access_log: bool = True,
|
||||
startup_box: str | None = "{Name} {version}\n{url}",
|
||||
log_config: Any = uvicorn.config.LOGGING_CONFIG, # noqa: ANN401
|
||||
**uvicorn_config: Any, # noqa: ANN401
|
||||
) -> None:
|
||||
@@ -41,6 +88,8 @@ def run( # noqa: PLR0913
|
||||
workers: Number of worker processes (requires uvicorn.run, single endpoint only).
|
||||
access_log: Enable our colored HTTP/WebSocket access logging middleware
|
||||
(uvicorn's own access logging is always bypassed).
|
||||
startup_box: Template for the startup box printed to stderr before
|
||||
serving (see _print_startup_box for fields), None to not print it.
|
||||
log_config: Logging config passed to uvicorn. Dict configs are patched
|
||||
best-effort (see fastapi_vue.logging.patch_log_config): tracerite
|
||||
loading and WebSocket chatter filtering are always installed, and
|
||||
@@ -53,6 +102,9 @@ def run( # noqa: PLR0913
|
||||
msg = "No endpoints to serve; check listen configuration"
|
||||
raise ValueError(msg)
|
||||
|
||||
if startup_box:
|
||||
_print_startup_box(startup_box, app, endpoints)
|
||||
|
||||
if isinstance(reload, Path):
|
||||
uvicorn_config["reload_dirs"] = [str(reload)]
|
||||
elif not reload:
|
||||
|
||||
Reference in New Issue
Block a user