drop pydantic for dataclass & add uvicorn prod/dev

This commit is contained in:
KronosDev-Pro 2024-11-09 21:02:39 +00:00
parent 1076847c68
commit 7ecabafdd0
7 changed files with 857 additions and 1264 deletions

View File

@ -697,28 +697,13 @@ class Config(Base):
env_file: Optional[str] = None env_file: Optional[str] = None
# Custom Backend Server # Custom Backend Server
# backend_server_prod: server.CustomBackendServer = server.GunicornBackendServer( backend_server_prod: server.CustomBackendServer = server.GunicornBackendServer(
# worker_class="uvicorn.workers.UvicornH11Worker", # type: ignore
# max_requests=100,
# max_requests_jitter=25,
# timeout=120,
# )
backend_server_prod: server.CustomBackendServer = server.GranianBackendServer(
threads=2, threads=2,
workers=4, workers=4,
) )
backend_server_dev: server.CustomBackendServer = server.GranianBackendServer( backend_server_dev: server.CustomBackendServer = server.UvicornBackendServer(
threads=1,
workers=1, workers=1,
) )
# backend_server_dev: server.CustomBackendServer = server.GunicornBackendServer(
# worker_class="uvicorn.workers.UvicornH11Worker", # type: ignore
# max_requests=100,
# max_requests_jitter=25,
# timeout=120,
# threads=1,
# workers=1,
# )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize the config values. """Initialize the config values.
@ -730,7 +715,6 @@ class Config(Base):
Raises: Raises:
ConfigError: If some values in the config are invalid. ConfigError: If some values in the config are invalid.
""" """
print("[reflex.config::Config] start")
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Update the config from environment variables. # Update the config from environment variables.

View File

@ -4,3 +4,10 @@ from .base import CustomBackendServer
from .granian import GranianBackendServer from .granian import GranianBackendServer
from .gunicorn import GunicornBackendServer from .gunicorn import GunicornBackendServer
from .uvicorn import UvicornBackendServer from .uvicorn import UvicornBackendServer
__all__ = [
"CustomBackendServer",
"GranianBackendServer",
"GunicornBackendServer",
"UvicornBackendServer",
]

View File

@ -4,16 +4,160 @@ from __future__ import annotations
import os import os
from abc import abstractmethod from abc import abstractmethod
from dataclasses import Field, dataclass
from dataclasses import field as dc_field
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Sequence
from reflex import constants from reflex import constants
from reflex.base import Base
from reflex.constants.base import Env, LogLevel from reflex.constants.base import Env, LogLevel
ReturnCliTypeFn = Callable[[Any], str]
class CustomBackendServer(Base):
class CliType:
"""Cli type transformer."""
@staticmethod
def default(fmt: str) -> ReturnCliTypeFn:
"""Default cli transformer.
Example:
fmt: `'--env-file {value}'`
value: `'/config.conf'`
result => `'--env-file /config.conf'`
"""
def wrapper(value: bool) -> str:
return fmt.format(value=value)
return wrapper
@staticmethod
def boolean(fmt: str, bool_value: bool = True) -> ReturnCliTypeFn:
"""When cli mode args only show when we want to activate it.
Example:
fmt: `'--reload'`
value: `False`
result => `''`
Example:
fmt: `'--reload'`
value: `True`
result => `'--reload'`
"""
def wrapper(value: bool) -> str:
return fmt if value is bool_value else ""
return wrapper
@staticmethod
def boolean_toggle(
fmt: str,
toggle_kw: str = "no",
toggle_sep: str = "-",
toggle_value: bool = False,
**kwargs,
) -> ReturnCliTypeFn:
"""When the cli mode is a boolean toggle `--access-log`/`--no-access-log`.
Example:
fmt: `'--{toggle_kw}{toggle_sep}access-log'`
value: `False`
toggle_value: `False` (default)
result => `'--no-access-log'`
Example:
fmt: `'--{toggle_kw}{toggle_sep}access-log'`
value: `True`
toggle_value: `False` (default)
result => `'--access-log'`
Example:
fmt: `'--{toggle_kw}{toggle_sep}access-log'`
value: `True`
toggle_value: `True`
result => `'--no-access-log'`
"""
def wrapper(value: bool) -> str:
return fmt.format(
**kwargs,
toggle_kw=(toggle_kw if value is toggle_value else ""),
toggle_sep=(toggle_sep if value is toggle_value else ""),
)
return wrapper
@staticmethod
def multiple(
fmt: str,
join_sep: str | None = None,
value_transformer: Callable[[Any], str] = lambda value: str(value),
) -> ReturnCliTypeFn:
r"""When the cli mode need multiple args or single args from an sequence.
Example (Multiple args mode):
fmt: `'--header {value}'`.
data_list: `['X-Forwarded-Proto=https', 'X-Forwarded-For=0.0.0.0']`
result => `'--header \"X-Forwarded-Proto=https\" --header \"X-Forwarded-For=0.0.0.0\"'`
Example (Single args mode):
fmt: `--headers {values}`
data_list: `['X-Forwarded-Proto=https', 'X-Forwarded-For=0.0.0.0']`
join_sep (required): `';'`
result => `--headers \"X-Forwarded-Proto=https;X-Forwarded-For=0.0.0.0\"`
Example (Single args mode):
fmt: `--headers {values}`
data_list: `[('X-Forwarded-Proto', 'https'), ('X-Forwarded-For', '0.0.0.0')]`
join_sep (required): `';'`
value_transformer: `lambda value: f'{value[0]}:{value[1]}'`
result => `--headers \"X-Forwarded-Proto:https;X-Forwarded-For:0.0.0.0\"`
"""
def wrapper(values: Sequence[str]) -> str:
return (
fmt.format(
values=join_sep.join(value_transformer(value) for value in values)
)
if join_sep
else " ".join(
[fmt.format(value=value_transformer(value)) for value in values]
)
)
return wrapper
def field_(
*,
default: Any = None,
metadata_cli: ReturnCliTypeFn | None = None,
exclude: bool = False,
**kwargs,
):
"""Custom dataclass field builder."""
params_ = {
"default": default,
"metadata": {"cli": metadata_cli, "exclude": exclude},
**kwargs,
}
if kwargs.get("default_factory", False):
params_.pop("default", None)
return dc_field(**params_)
@dataclass
class CustomBackendServer:
"""BackendServer base.""" """BackendServer base."""
_app_uri: str = field_(default="", metadata_cli=None, exclude=True)
@staticmethod @staticmethod
def get_app_module(for_granian_target: bool = False, add_extra_api: bool = False): def get_app_module(for_granian_target: bool = False, add_extra_api: bool = False):
"""Get the app module for the backend. """Get the app module for the backend.
@ -74,8 +218,36 @@ class CustomBackendServer(Base):
else need_threads else need_threads
) )
def get_fields(self) -> dict[str, Field]:
"""Return all the fields."""
return self.__dataclass_fields__
def get_values(self) -> dict[str, Any]:
"""Return all values."""
return {
key: getattr(self, key)
for key, field in self.__dataclass_fields__.items()
if field.metadata["exclude"] is False
}
def is_default_value(self, key, value: Any | None = None) -> bool:
"""Check if the `value` is the same value from default context."""
from dataclasses import MISSING
field = self.get_fields()[key]
if value is None:
value = getattr(self, key, None)
if field.default != MISSING:
return value == field.default
else:
if field.default_factory != MISSING:
return value == field.default_factory()
return False
@abstractmethod @abstractmethod
def check_import(self, extra: bool = False): def check_import(self):
"""Check package importation.""" """Check package importation."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -6,10 +6,10 @@ import sys
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field as dc_field from dataclasses import field as dc_field
from pathlib import Path from pathlib import Path
from typing import Any, Literal, Type from typing import Any, Literal
from reflex.constants.base import Env, LogLevel from reflex.constants.base import Env, LogLevel
from reflex.server.base import CustomBackendServer from reflex.server.base import CliType, CustomBackendServer, field_
from reflex.utils import console from reflex.utils import console
@ -38,122 +38,160 @@ class HTTP2Settings: # https://github.com/emmett-framework/granian/blob/261ceba
max_send_buffer_size: int = dc_field(default=1024 * 400) max_send_buffer_size: int = dc_field(default=1024 * 400)
try: @dataclass
import watchfiles # type: ignore
except ImportError:
watchfiles = None
_mapping_attr_to_cli: dict[str, str] = {
"address": "--host",
"port": "--port",
"interface": "--interface",
"http": "--http",
"websockets": "--ws", # NOTE: when `websockets` True: `--ws`; False: `--no-ws`
"workers": "--workers",
"threads": "--threads",
"blocking_threads": "--blocking-threads",
"threading_mode": "--threading-mode",
"loop": "--loop",
"loop_opt": "--opt", # NOTE: when `loop_opt` True: `--opt`; False: `--no-opt`
"backlog": "--backlog",
"backpressure": "--backpressure",
"http1_keep_alive": "--http1-keep-alive",
"http1_max_buffer_size": "--http1-max-buffer-size",
"http1_pipeline_flush": "--http1-pipeline-flush",
"http2_adaptive_window": "--http2-adaptive-window",
"http2_initial_connection_window_size": "--http2-initial-connection-window-size",
"http2_initial_stream_window_size": "--http2-initial-stream-window-size",
"http2_keep_alive_interval": "--http2-keep-alive-interval",
"http2_keep_alive_timeout": "--http2-keep-alive-timeout",
"http2_max_concurrent_streams": "--http2-max-concurrent-streams",
"http2_max_frame_size": "--http2-max-frame-size",
"http2_max_headers_size": "--http2-max-headers-size",
"http2_max_send_buffer_size": "--http2-max-send-buffer-size",
"log_enabled": "--log", # NOTE: when `log_enabled` True: `--log`; False: `--no-log`
"log_level": "--log-level",
"log_access": "--log-access", # NOTE: when `log_access` True: `--log-access`; False: `--no-log-access`
"log_access_format": "--access-log-fmt",
"ssl_cert": "--ssl-certificate",
"ssl_key": "--ssl-keyfile",
"ssl_key_password": "--ssl-keyfile-password",
"url_path_prefix": "--url-path-prefix",
"respawn_failed_workers": "--respawn-failed-workers", # NOTE: when `respawn_failed_workers` True: `--respawn-failed-workers`; False: `--no-respawn-failed-workers`
"respawn_interval": "--respawn-interval",
"workers_lifetime": "--workers-lifetime",
"factory": "--factory", # NOTE: when `factory` True: `--factory`; False: `--no-factory`
"reload": "--reload", # NOTE: when `reload` True: `--reload`; False: `--no-reload`
"reload_paths": "--reload-paths",
"reload_ignore_dirs": "--reload-ignore-dirs",
"reload_ignore_patterns": "--reload-ignore-patterns",
"reload_ignore_paths": "--reload-ignore-paths",
"process_name": "--process-name",
"pid_file": "--pid-file",
}
class GranianBackendServer(CustomBackendServer): class GranianBackendServer(CustomBackendServer):
"""Granian backendServer.""" """Granian backendServer.
# https://github.com/emmett-framework/granian/blob/fc11808ed177362fcd9359a455a733065ddbc505/granian/server.py#L69 https://github.com/emmett-framework/granian/blob/fc11808ed177362fcd9359a455a733065ddbc505/granian/cli.py#L52 (until Granian has the proper documentation)
target: str | None = None """
address: str = "127.0.0.1"
port: int = 8000
interface: Literal["asgi", "asginl", "rsgi", "wsgi"] = "rsgi"
workers: int = 0
threads: int = 0
blocking_threads: int | None = None
threading_mode: Literal["runtime", "workers"] = "workers"
loop: Literal["auto", "asyncio", "uvloop"] = "auto"
loop_opt: bool = False
http: Literal["auto", "1", "2"] = "auto"
websockets: bool = True
backlog: int = 1024
backpressure: int | None = None
# http1_settings: HTTP1Settings | None = None address: str = field_(
# NOTE: child of http1_settings, needed only for cli mode default="127.0.0.1", metadata_cli=CliType.default("--host {value}")
http1_keep_alive: bool = HTTP1Settings.keep_alive )
http1_max_buffer_size: int = HTTP1Settings.max_buffer_size port: int = field_(default=8000, metadata_cli=CliType.default("--port {value}"))
http1_pipeline_flush: bool = HTTP1Settings.pipeline_flush interface: Literal["asgi", "asginl", "rsgi", "wsgi"] = field_(
default="rsgi", metadata_cli=CliType.default("--interface {value}")
# http2_settings: HTTP2Settings | None = None )
# NOTE: child of http2_settings, needed only for cli mode workers: int = field_(default=0, metadata_cli=CliType.default("--workers {value}"))
http2_adaptive_window: bool = HTTP2Settings.adaptive_window threads: int = field_(default=0, metadata_cli=CliType.default("--threads {value}"))
http2_initial_connection_window_size: int = ( blocking_threads: int | None = field_(
HTTP2Settings.initial_connection_window_size default=None, metadata_cli=CliType.default("--blocking-threads {value}")
)
threading_mode: Literal["runtime", "workers"] = field_(
default="workers", metadata_cli=CliType.default("--threading-mode {value}")
)
loop: Literal["auto", "asyncio", "uvloop"] = field_(
default="auto", metadata_cli=CliType.default("--loop {value}")
)
loop_opt: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}opt"),
)
http: Literal["auto", "1", "2"] = field_(
default="auto", metadata_cli=CliType.default("--http {value}")
)
websockets: bool = field_(
default=True, metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}ws")
)
backlog: int = field_(
default=1024, metadata_cli=CliType.default("--backlog {value}")
)
backpressure: int | None = field_(
default=None, metadata_cli=CliType.default("--backpressure {value}")
)
http1_keep_alive: bool = field_(
default=True, metadata_cli=CliType.default("--http1-keep-alive {value}")
)
http1_max_buffer_size: int = field_(
default=417792, metadata_cli=CliType.default("--http1-max-buffer-size {value}")
)
http1_pipeline_flush: bool = field_(
default=False, metadata_cli=CliType.default("--http1-pipeline-flush {value}")
)
http2_adaptive_window: bool = field_(
default=False, metadata_cli=CliType.default("--http2-adaptive-window {value}")
)
http2_initial_connection_window_size: int = field_(
default=1048576,
metadata_cli=CliType.default("--http2-initial-connection-window-size {value}"),
)
http2_initial_stream_window_size: int = field_(
default=1048576,
metadata_cli=CliType.default("--http2-initial-stream-window-size {value}"),
)
http2_keep_alive_interval: int | None = field_(
default=None,
metadata_cli=CliType.default("--http2-keep-alive-interval {value}"),
)
http2_keep_alive_timeout: int = field_(
default=20, metadata_cli=CliType.default("--http2-keep-alive-timeout {value}")
)
http2_max_concurrent_streams: int = field_(
default=200,
metadata_cli=CliType.default("--http2-max-concurrent-streams {value}"),
)
http2_max_frame_size: int = field_(
default=16384, metadata_cli=CliType.default("--http2-max-frame-size {value}")
)
http2_max_headers_size: int = field_(
default=16777216,
metadata_cli=CliType.default("--http2-max-headers-size {value}"),
)
http2_max_send_buffer_size: int = field_(
default=409600,
metadata_cli=CliType.default("--http2-max-send-buffer-size {value}"),
)
log_enabled: bool = field_(
default=True,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}log"),
)
log_level: Literal["critical", "error", "warning", "warn", "info", "debug"] = (
field_(default="info", metadata_cli=CliType.default("--log-level {value}"))
)
log_dictconfig: dict[str, Any] | None = field_(default=None, metadata_cli=None)
log_access: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}log-access"),
)
log_access_format: str | None = field_(
default=None, metadata_cli=CliType.default("--access-log-fmt {value}")
)
ssl_cert: Path | None = field_(
default=None, metadata_cli=CliType.default("--ssl-certificate {value}")
)
ssl_key: Path | None = field_(
default=None, metadata_cli=CliType.default("--ssl-keyfile {value}")
)
ssl_key_password: str | None = field_(
default=None, metadata_cli=CliType.default("--ssl-keyfile-password {value}")
)
url_path_prefix: str | None = field_(
default=None, metadata_cli=CliType.default("--url-path-prefix {value}")
)
respawn_failed_workers: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle(
"--{toggle_kw}{toggle_sep}respawn-failed-workers"
),
)
respawn_interval: float = field_(
default=3.5, metadata_cli=CliType.default("--respawn-interval {value}")
)
workers_lifetime: int | None = field_(
default=None, metadata_cli=CliType.default("--workers-lifetime {value}")
)
factory: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}factory"),
)
reload: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}reload"),
)
reload_paths: list[Path] | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-paths {value}")
)
reload_ignore_dirs: list[str] | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-ignore-dirs {value}")
)
reload_ignore_patterns: list[str] | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-ignore-patterns {value}")
)
reload_ignore_paths: list[Path] | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-ignore-paths {value}")
)
reload_filter: object | None = field_( # type: ignore
default=None, metadata_cli=None
)
process_name: str | None = field_(
default=None, metadata_cli=CliType.default("--process-name {value}")
)
pid_file: Path | None = field_(
default=None, metadata_cli=CliType.default("--pid-file {value}")
) )
http2_initial_stream_window_size: int = HTTP2Settings.initial_stream_window_size
http2_keep_alive_interval: int | None = HTTP2Settings.keep_alive_interval
http2_keep_alive_timeout: int = HTTP2Settings.keep_alive_timeout
http2_max_concurrent_streams: int = HTTP2Settings.max_concurrent_streams
http2_max_frame_size: int = HTTP2Settings.max_frame_size
http2_max_headers_size: int = HTTP2Settings.max_headers_size
http2_max_send_buffer_size: int = HTTP2Settings.max_send_buffer_size
log_enabled: bool = True def check_import(self):
log_level: Literal["critical", "error", "warning", "warn", "info", "debug"] = "info"
log_dictconfig: dict[str, Any] | None = None
log_access: bool = False
log_access_format: str | None = None
ssl_cert: Path | None = None
ssl_key: Path | None = None
ssl_key_password: str | None = None
url_path_prefix: str | None = None
respawn_failed_workers: bool = False
respawn_interval: float = 3.5
workers_lifetime: int | None = None
factory: bool = False
reload: bool = False
reload_paths: list[Path] | None = None
reload_ignore_dirs: list[str] | None = None
reload_ignore_patterns: list[str] | None = None
reload_ignore_paths: list[Path] | None = None
reload_filter: Type[getattr(watchfiles, "BaseFilter", None)] | None = None # type: ignore
process_name: str | None = None
pid_file: Path | None = None
def check_import(self, extra: bool = False):
"""Check package importation.""" """Check package importation."""
from importlib.util import find_spec from importlib.util import find_spec
@ -164,11 +202,10 @@ class GranianBackendServer(CustomBackendServer):
'The `granian` package is required to run `GranianBackendServer`. Run `pip install "granian>=1.6.0"`.' 'The `granian` package is required to run `GranianBackendServer`. Run `pip install "granian>=1.6.0"`.'
) )
if find_spec("watchfiles") is None and extra: if find_spec("watchfiles") is None and self.reload:
# NOTE: the `\[` is for force `rich.Console` to not consider it like a color or anything else which he not printing `[.*]`
errors.append( errors.append(
r'Using --reload in `GranianBackendServer` requires the granian\[reload] extra. Run `pip install "granian\[reload]>=1.6.0"`.' 'Using `--reload` in `GranianBackendServer` requires the `watchfiles` extra. Run `pip install "watchfiles~=0.21"`.'
) # type: ignore )
if errors: if errors:
console.error("\n".join(errors)) console.error("\n".join(errors))
@ -176,18 +213,18 @@ class GranianBackendServer(CustomBackendServer):
def setup(self, host: str, port: int, loglevel: LogLevel, env: Env): def setup(self, host: str, port: int, loglevel: LogLevel, env: Env):
"""Setup.""" """Setup."""
self.target = self.get_app_module(for_granian_target=True, add_extra_api=True) self._app_uri = self.get_app_module(for_granian_target=True, add_extra_api=True)
self.log_level = loglevel.value # type: ignore self.log_level = loglevel.value # type: ignore
self.address = host self.address = host
self.port = port self.port = port
self.interface = "asgi" # prevent obvious error self.interface = "asgi" # NOTE: prevent obvious error
if env == Env.PROD: if env == Env.PROD:
if self.workers == self.get_fields()["workers"].default: if self.workers == self.get_fields()["workers"].default:
self.workers = self.get_recommended_workers() self.workers = self.get_recommended_workers()
else: else:
if self.workers > (max_threads := self.get_max_workers()): if self.workers > (max_workers := self.get_max_workers()):
self.workers = max_threads self.workers = max_workers
if self.threads == self.get_fields()["threads"].default: if self.threads == self.get_fields()["threads"].default:
self.threads = self.get_recommended_threads() self.threads = self.get_recommended_threads()
@ -208,26 +245,18 @@ class GranianBackendServer(CustomBackendServer):
command = ["granian"] command = ["granian"]
for key, field in self.get_fields().items(): for key, field in self.get_fields().items():
if key != "target": if (
value = getattr(self, key) field.metadata["exclude"] is False
if _mapping_attr_to_cli.get(key) and value != field.default: and field.metadata["cli"]
if isinstance(value, list): and not self.is_default_value(key, (value := getattr(self, key)))
for v in value: ):
command += [_mapping_attr_to_cli[key], str(v)] command += field.metadata["cli"](value).split(" ")
elif isinstance(value, bool):
command.append(
f"--{'no-' if value is False else ''}{_mapping_attr_to_cli[key][2:]}"
)
else:
command += [_mapping_attr_to_cli[key], str(value)]
return command + [ return command + [self._app_uri]
self.get_app_module(for_granian_target=True, add_extra_api=True)
]
def run_dev(self): def run_dev(self):
"""Run in development mode.""" """Run in development mode."""
self.check_import(extra=self.reload) self.check_import()
from granian import Granian from granian import Granian
exclude_keys = ( exclude_keys = (
@ -244,14 +273,17 @@ class GranianBackendServer(CustomBackendServer):
"http2_max_headers_size", "http2_max_headers_size",
"http2_max_send_buffer_size", "http2_max_send_buffer_size",
) )
model = self.get_fields()
Granian( Granian(
**{ **{
**{ **{
key: value key: value
for key, value in self.dict().items() for key, value in self.get_values().items()
if key not in exclude_keys and value != model[key].default if (
key not in exclude_keys
and not self.is_default_value(key, value)
)
}, },
"target": self._app_uri,
"http1_settings": HTTP1Settings( "http1_settings": HTTP1Settings(
self.http1_keep_alive, self.http1_keep_alive,
self.http1_max_buffer_size, self.http1_max_buffer_size,

File diff suppressed because it is too large Load Diff

View File

@ -2,18 +2,188 @@
from __future__ import annotations from __future__ import annotations
# `UvicornBackendServer` defer from other `*BackendServer`, because `uvicorn` he is natively integrated inside the reflex project via Fastapi (same for asyncio)
import asyncio
import os
import ssl
import sys import sys
from configparser import RawConfigParser
from dataclasses import dataclass
from pathlib import Path
from typing import IO, Any, Awaitable, Callable
from uvicorn import Config, Server
from uvicorn.config import (
LOGGING_CONFIG,
SSL_PROTOCOL_VERSION,
HTTPProtocolType,
InterfaceType,
LifespanType,
LoopSetupType,
WSProtocolType,
)
from reflex.constants.base import Env, LogLevel from reflex.constants.base import Env, LogLevel
from reflex.server.base import CustomBackendServer from reflex.server.base import CliType, CustomBackendServer, field_
from reflex.utils import console from reflex.utils import console
# TODO @dataclass
class UvicornBackendServer(CustomBackendServer): class UvicornBackendServer(CustomBackendServer):
"""Uvicorn backendServer.""" """Uvicorn backendServer.
def check_import(self, extra: bool = False): https://www.uvicorn.org/settings/
"""
host: str = field_(
default="127.0.0.1", metadata_cli=CliType.default("--host {value}")
)
port: int = field_(default=8000, metadata_cli=CliType.default("--port {value}"))
uds: str | None = field_(
default=None, metadata_cli=CliType.default("--uds {value}")
)
fd: int | None = field_(default=None, metadata_cli=CliType.default("--fd {value}"))
loop: LoopSetupType = field_(
default="auto", metadata_cli=CliType.default("--loop {value}")
)
http: type[asyncio.Protocol] | HTTPProtocolType = field_(
default="auto", metadata_cli=CliType.default("--http {value}")
)
ws: type[asyncio.Protocol] | WSProtocolType = field_(
default="auto", metadata_cli=CliType.default("--ws {value}")
)
ws_max_size: int = field_(
default=16777216, metadata_cli=CliType.default("--ws-max-size {value}")
)
ws_max_queue: int = field_(
default=32, metadata_cli=CliType.default("--ws-max-queue {value}")
)
ws_ping_interval: float | None = field_(
default=20.0, metadata_cli=CliType.default("--ws-ping-interval {value}")
)
ws_ping_timeout: float | None = field_(
default=20.0, metadata_cli=CliType.default("--ws-ping-timeout {value}")
)
ws_per_message_deflate: bool = field_(
default=True, metadata_cli=CliType.default("--ws-per-message-deflate {value}")
)
lifespan: LifespanType = field_(
default="auto", metadata_cli=CliType.default("--lifespan {value}")
)
env_file: str | os.PathLike[str] | None = field_(
default=None, metadata_cli=CliType.default("--env-file {value}")
)
log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = field_(
default=None,
default_factory=lambda: LOGGING_CONFIG,
metadata_cli=CliType.default("--log-config {value}"),
)
log_level: str | int | None = field_(
default=None, metadata_cli=CliType.default("--log-level {value}")
)
access_log: bool = field_(
default=True,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}access-log"),
)
use_colors: bool | None = field_(
default=None,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}use-colors"),
)
interface: InterfaceType = field_(
default="auto", metadata_cli=CliType.default("--interface {value}")
)
reload: bool = field_(
default=False, metadata_cli=CliType.default("--reload {value}")
)
reload_dirs: list[str] | str | None = field_(
default=None, metadata_cli=CliType.multiple("--reload_dir {value}")
)
reload_delay: float = field_(
default=0.25, metadata_cli=CliType.default("--reload-delay {value}")
)
reload_includes: list[str] | str | None = field_(
default=None, metadata_cli=CliType.multiple("----reload-include {value}")
)
reload_excludes: list[str] | str | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-exclude {value}")
)
workers: int = field_(default=0, metadata_cli=CliType.default("--workers {value}"))
proxy_headers: bool = field_(
default=True,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}proxy-headers"),
)
server_header: bool = field_(
default=True,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}server-header"),
)
date_header: bool = field_(
default=True,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}date-header"),
)
forwarded_allow_ips: list[str] | str | None = field_(
default=None,
metadata_cli=CliType.multiple("--forwarded-allow-ips {value}", join_sep=","),
)
root_path: str = field_(
default="", metadata_cli=CliType.default("--root-path {value}")
)
limit_concurrency: int | None = field_(
default=None, metadata_cli=CliType.default("--limit-concurrency {value}")
)
limit_max_requests: int | None = field_(
default=None, metadata_cli=CliType.default("--limit-max-requests {value}")
)
backlog: int = field_(
default=2048, metadata_cli=CliType.default("--backlog {value}")
)
timeout_keep_alive: int = field_(
default=5, metadata_cli=CliType.default("--timeout-keep-alive {value}")
)
timeout_notify: int = field_(default=30, metadata_cli=None)
timeout_graceful_shutdown: int | None = field_(
default=None,
metadata_cli=CliType.default("--timeout-graceful-shutdown {value}"),
)
callback_notify: Callable[..., Awaitable[None]] | None = field_(
default=None, metadata_cli=None
)
ssl_keyfile: str | os.PathLike[str] | None = field_(
default=None, metadata_cli=CliType.default("--ssl-keyfile {value}")
)
ssl_certfile: str | os.PathLike[str] | None = field_(
default=None, metadata_cli=CliType.default("--ssl-certfile {value}")
)
ssl_keyfile_password: str | None = field_(
default=None, metadata_cli=CliType.default("--ssl-keyfile-password {value}")
)
ssl_version: int = field_(
default=SSL_PROTOCOL_VERSION,
metadata_cli=CliType.default("--ssl-version {value}"),
)
ssl_cert_reqs: int = field_(
default=ssl.CERT_NONE, metadata_cli=CliType.default("--ssl-cert-reqs {value}")
)
ssl_ca_certs: str | None = field_(
default=None, metadata_cli=CliType.default("--ssl-ca-certs {value}")
)
ssl_ciphers: str = field_(
default="TLSv1", metadata_cli=CliType.default("--ssl-ciphers {value}")
)
headers: list[tuple[str, str]] | None = field_(
default=None,
metadata_cli=CliType.multiple(
"--header {value}", value_transformer=lambda value: f"{value[0]}:{value[1]}"
),
)
factory: bool = field_(
default=False, metadata_cli=CliType.default("--factory {value}")
)
h11_max_incomplete_event_size: int | None = field_(
default=None,
metadata_cli=CliType.default("--h11-max-incomplete-event-size {value}"),
)
def check_import(self):
"""Check package importation.""" """Check package importation."""
from importlib.util import find_spec from importlib.util import find_spec
@ -24,18 +194,62 @@ class UvicornBackendServer(CustomBackendServer):
'The `uvicorn` package is required to run `UvicornBackendServer`. Run `pip install "uvicorn>=0.20.0"`.' 'The `uvicorn` package is required to run `UvicornBackendServer`. Run `pip install "uvicorn>=0.20.0"`.'
) )
if find_spec("watchfiles") is None and (
self.reload_includes and self.reload_excludes
):
errors.append(
'Using `--reload-include` and `--reload-exclude` in `UvicornBackendServer` requires the `watchfiles` extra. Run `pip install "watchfiles>=0.13"`.'
)
if errors: if errors:
console.error("\n".join(errors)) console.error("\n".join(errors))
sys.exit() sys.exit()
def setup(self, host: str, port: int, loglevel: LogLevel, env: Env): def setup(self, host: str, port: int, loglevel: LogLevel, env: Env):
"""Setup.""" """Setup."""
pass self._app_uri = self.get_app_module(add_extra_api=True)
self.log_level = loglevel.value
self.host = host
self.port = port
if env == Env.PROD:
if self.workers == self.get_fields()["workers"].default:
self.workers = self.get_recommended_workers()
else:
if self.workers > (max_workers := self.get_max_workers()):
self.workers = max_workers
if env == Env.DEV:
from reflex.config import get_config # prevent circular import
self.reload = True
self.reload_dirs = [str(Path(get_config().app_name))]
def run_prod(self): def run_prod(self):
"""Run in production mode.""" """Run in production mode."""
pass self.check_import()
command = ["uvicorn"]
for key, field in self.get_fields().items():
if (
field.metadata["exclude"] is False
and field.metadata["cli"]
and not self.is_default_value(key, (value := getattr(self, key)))
):
command += field.metadata["cli"](value).split(" ")
return command + [self._app_uri]
def run_dev(self): def run_dev(self):
"""Run in development mode.""" """Run in development mode."""
pass self.check_import()
options_ = {
key: value
for key, value in self.get_values().items()
if not self.is_default_value(key, value)
}
Server(
config=Config(**options_, app=self._app_uri),
).run()

View File

@ -203,9 +203,9 @@ def run_backend(
notify_backend() notify_backend()
# Run the backend in development mode. # Run the backend in development mode.
backend_server_prod = config.backend_server_prod backend_server_dev = config.backend_server_dev
backend_server_prod.setup(host, port, loglevel, Env.DEV) backend_server_dev.setup(host, port, loglevel, Env.DEV)
backend_server_prod.run_dev() backend_server_dev.run_dev()
def run_backend_prod( def run_backend_prod(