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
# Custom Backend Server
# 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(
backend_server_prod: server.CustomBackendServer = server.GunicornBackendServer(
threads=2,
workers=4,
)
backend_server_dev: server.CustomBackendServer = server.GranianBackendServer(
threads=1,
backend_server_dev: server.CustomBackendServer = server.UvicornBackendServer(
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):
"""Initialize the config values.
@ -730,7 +715,6 @@ class Config(Base):
Raises:
ConfigError: If some values in the config are invalid.
"""
print("[reflex.config::Config] start")
super().__init__(*args, **kwargs)
# Update the config from environment variables.

View File

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

View File

@ -4,16 +4,160 @@ from __future__ import annotations
import os
from abc import abstractmethod
from dataclasses import Field, dataclass
from dataclasses import field as dc_field
from pathlib import Path
from typing import Any, Callable, Sequence
from reflex import constants
from reflex.base import Base
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."""
_app_uri: str = field_(default="", metadata_cli=None, exclude=True)
@staticmethod
def get_app_module(for_granian_target: bool = False, add_extra_api: bool = False):
"""Get the app module for the backend.
@ -74,8 +218,36 @@ class CustomBackendServer(Base):
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
def check_import(self, extra: bool = False):
def check_import(self):
"""Check package importation."""
raise NotImplementedError()

View File

@ -6,10 +6,10 @@ import sys
from dataclasses import dataclass
from dataclasses import field as dc_field
from pathlib import Path
from typing import Any, Literal, Type
from typing import Any, Literal
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
@ -38,122 +38,160 @@ class HTTP2Settings: # https://github.com/emmett-framework/granian/blob/261ceba
max_send_buffer_size: int = dc_field(default=1024 * 400)
try:
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",
}
@dataclass
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
# NOTE: child of http1_settings, needed only for cli mode
http1_keep_alive: bool = HTTP1Settings.keep_alive
http1_max_buffer_size: int = HTTP1Settings.max_buffer_size
http1_pipeline_flush: bool = HTTP1Settings.pipeline_flush
# http2_settings: HTTP2Settings | None = None
# NOTE: child of http2_settings, needed only for cli mode
http2_adaptive_window: bool = HTTP2Settings.adaptive_window
http2_initial_connection_window_size: int = (
HTTP2Settings.initial_connection_window_size
address: str = field_(
default="127.0.0.1", metadata_cli=CliType.default("--host {value}")
)
port: int = field_(default=8000, metadata_cli=CliType.default("--port {value}"))
interface: Literal["asgi", "asginl", "rsgi", "wsgi"] = field_(
default="rsgi", metadata_cli=CliType.default("--interface {value}")
)
workers: int = field_(default=0, metadata_cli=CliType.default("--workers {value}"))
threads: int = field_(default=0, metadata_cli=CliType.default("--threads {value}"))
blocking_threads: int | None = field_(
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
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):
def check_import(self):
"""Check package importation."""
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"`.'
)
if find_spec("watchfiles") is None and extra:
# NOTE: the `\[` is for force `rich.Console` to not consider it like a color or anything else which he not printing `[.*]`
if find_spec("watchfiles") is None and self.reload:
errors.append(
r'Using --reload in `GranianBackendServer` requires the granian\[reload] extra. Run `pip install "granian\[reload]>=1.6.0"`.'
) # type: ignore
'Using `--reload` in `GranianBackendServer` requires the `watchfiles` extra. Run `pip install "watchfiles~=0.21"`.'
)
if errors:
console.error("\n".join(errors))
@ -176,18 +213,18 @@ class GranianBackendServer(CustomBackendServer):
def setup(self, host: str, port: int, loglevel: LogLevel, env: Env):
"""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.address = host
self.port = port
self.interface = "asgi" # prevent obvious error
self.interface = "asgi" # NOTE: prevent obvious error
if env == Env.PROD:
if self.workers == self.get_fields()["workers"].default:
self.workers = self.get_recommended_workers()
else:
if self.workers > (max_threads := self.get_max_workers()):
self.workers = max_threads
if self.workers > (max_workers := self.get_max_workers()):
self.workers = max_workers
if self.threads == self.get_fields()["threads"].default:
self.threads = self.get_recommended_threads()
@ -208,26 +245,18 @@ class GranianBackendServer(CustomBackendServer):
command = ["granian"]
for key, field in self.get_fields().items():
if key != "target":
value = getattr(self, key)
if _mapping_attr_to_cli.get(key) and value != field.default:
if isinstance(value, list):
for v in value:
command += [_mapping_attr_to_cli[key], str(v)]
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)]
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.get_app_module(for_granian_target=True, add_extra_api=True)
]
return command + [self._app_uri]
def run_dev(self):
"""Run in development mode."""
self.check_import(extra=self.reload)
self.check_import()
from granian import Granian
exclude_keys = (
@ -244,14 +273,17 @@ class GranianBackendServer(CustomBackendServer):
"http2_max_headers_size",
"http2_max_send_buffer_size",
)
model = self.get_fields()
Granian(
**{
**{
key: value
for key, value in self.dict().items()
if key not in exclude_keys and value != model[key].default
for key, value in self.get_values().items()
if (
key not in exclude_keys
and not self.is_default_value(key, value)
)
},
"target": self._app_uri,
"http1_settings": HTTP1Settings(
self.http1_keep_alive,
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
# `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
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.server.base import CustomBackendServer
from reflex.server.base import CliType, CustomBackendServer, field_
from reflex.utils import console
# TODO
@dataclass
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."""
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"`.'
)
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:
console.error("\n".join(errors))
sys.exit()
def setup(self, host: str, port: int, loglevel: LogLevel, env: Env):
"""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):
"""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):
"""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()
# Run the backend in development mode.
backend_server_prod = config.backend_server_prod
backend_server_prod.setup(host, port, loglevel, Env.DEV)
backend_server_prod.run_dev()
backend_server_dev = config.backend_server_dev
backend_server_dev.setup(host, port, loglevel, Env.DEV)
backend_server_dev.run_dev()
def run_backend_prod(