85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
"""Uvicorn server runner with multi-endpoint support."""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from contextlib import suppress
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import uvicorn
|
|
from uvicorn import Config, Server
|
|
|
|
from .hostutil import parse_endpoints
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def run(
|
|
app: str,
|
|
*,
|
|
listen: str | list[str] | None = None,
|
|
default_port: int = 8000,
|
|
reload: bool | Path = False,
|
|
workers: int | None = None,
|
|
**uvicorn_config: Any, # noqa: ANN401
|
|
) -> None:
|
|
"""Run uvicorn server(s) for the given app.
|
|
|
|
Args:
|
|
app: The ASGI application path (e.g., "myapp.main:app")
|
|
listen: Endpoint string(s) (see parse_endpoint for formats).
|
|
default_port: Port to use when not specified in listen args.
|
|
reload: Enable auto-reload. If a Path is given, reload watches that
|
|
directory. True enables reload without setting a reload directory.
|
|
False disables reload and clears any reload_dirs.
|
|
workers: Number of worker processes (requires uvicorn.run, single endpoint only).
|
|
**uvicorn_config: Additional uvicorn config options (overrides all other settings).
|
|
|
|
"""
|
|
endpoints = parse_endpoints(listen, default_port)
|
|
if not endpoints:
|
|
msg = "No endpoints to serve; check listen configuration"
|
|
raise ValueError(msg)
|
|
|
|
if isinstance(reload, Path):
|
|
uvicorn_config["reload_dirs"] = [str(reload)]
|
|
elif not reload:
|
|
uvicorn_config.pop("reload_dirs", None)
|
|
|
|
conf: dict[str, object] = {"app": app, "reload": bool(reload), "workers": workers}
|
|
proxy = os.getenv("FORWARDED_ALLOW_IPS", "127.0.0.1,::1")
|
|
if proxy:
|
|
conf["proxy_headers"] = True
|
|
conf["forwarded_allow_ips"] = proxy
|
|
conf.update(uvicorn_config)
|
|
|
|
with suppress(KeyboardInterrupt, asyncio.CancelledError):
|
|
if reload or workers:
|
|
serve_multiprocess(endpoints, **conf)
|
|
else:
|
|
asyncio.run(serve(endpoints, **conf))
|
|
|
|
|
|
async def serve(endpoints: list[dict], **kwargs: Any) -> None: # noqa: ANN401
|
|
"""Serve the given endpoints in current process/loop. Does not spawn extra processes."""
|
|
forbidden = {"reload", "workers"} & {k for k, v in kwargs.items() if v}
|
|
if forbidden:
|
|
logger.warning(
|
|
"Options %s have no effect in simple mode (multiple endpoints)",
|
|
", ".join(sorted(forbidden)),
|
|
)
|
|
await asyncio.gather(*(Server(Config(**kwargs, **ep)).serve() for ep in endpoints))
|
|
|
|
|
|
def serve_multiprocess(endpoints: list[dict], **kwargs: Any) -> None: # noqa: ANN401
|
|
"""Serve using uvicorn.run() for reload/workers support. Only first endpoint is used."""
|
|
if len(endpoints) > 1:
|
|
eps = [ep["uds"] if "uds" in ep else f"{ep['host']}:{ep['port']}" for ep in endpoints]
|
|
logger.warning(
|
|
"Current mode supports only one endpoint. Listening: %s, skipped: %s",
|
|
eps[0],
|
|
" ".join(eps[1:]),
|
|
)
|
|
uvicorn.run(**kwargs, **endpoints[0])
|