From 7ecabafdd0ca1b2026946b8a35899093aa560bb5 Mon Sep 17 00:00:00 2001 From: KronosDev-Pro Date: Sat, 9 Nov 2024 21:02:39 +0000 Subject: [PATCH] drop pydantic for dataclass & add uvicorn prod/dev --- reflex/config.py | 20 +- reflex/server/__init__.py | 7 + reflex/server/base.py | 178 ++++- reflex/server/granian.py | 310 +++++---- reflex/server/gunicorn.py | 1372 ++++++++----------------------------- reflex/server/uvicorn.py | 228 +++++- reflex/utils/exec.py | 6 +- 7 files changed, 857 insertions(+), 1264 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index 852383122..20401e6ac 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -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. diff --git a/reflex/server/__init__.py b/reflex/server/__init__.py index 9633ddc7c..b39dc8049 100644 --- a/reflex/server/__init__.py +++ b/reflex/server/__init__.py @@ -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", +] diff --git a/reflex/server/base.py b/reflex/server/base.py index c821c2686..012511233 100644 --- a/reflex/server/base.py +++ b/reflex/server/base.py @@ -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() diff --git a/reflex/server/granian.py b/reflex/server/granian.py index fda03912c..2a76d8c53 100644 --- a/reflex/server/granian.py +++ b/reflex/server/granian.py @@ -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, diff --git a/reflex/server/gunicorn.py b/reflex/server/gunicorn.py index c0bfb3b50..53544da7b 100644 --- a/reflex/server/gunicorn.py +++ b/reflex/server/gunicorn.py @@ -2,13 +2,12 @@ from __future__ import annotations -import os -import ssl import sys +from dataclasses import dataclass from typing import Any, Callable, Literal -from reflex.constants.base import Env, LogLevel -from reflex.server.base import CustomBackendServer +from reflex.constants.base import IS_WINDOWS, Env, LogLevel +from reflex.server.base import CliType, CustomBackendServer, field_ from reflex.utils import console _mapping_attr_to_cli: dict[str, str] = { @@ -87,78 +86,25 @@ _mapping_attr_to_cli: dict[str, str] = { } +@dataclass class GunicornBackendServer(CustomBackendServer): - """Gunicorn backendServer.""" + """Gunicorn backendServer. - # https://github.com/benoitc/gunicorn/blob/bacbf8aa5152b94e44aa5d2a94aeaf0318a85248/gunicorn/config.py - - app_uri: str | None = None - - config: str = "./gunicorn.conf.py" - """\ - :ref:`The Gunicorn config file`. - - A string of the form ``PATH``, ``file:PATH``, or ``python:MODULE_NAME``. - - Only has an effect when specified on the command line or as part of an - application specific configuration. - - By default, a file named ``gunicorn.conf.py`` will be read from the same - directory where gunicorn is being run. - """ - - wsgi_app: str | None = None - """\ - A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. - """ - - bind: list[str] = ["127.0.0.1:8000"] - """\ - The socket to bind. - - A string of the form: ``HOST``, ``HOST:PORT``, ``unix:PATH``, - ``fd://FD``. An IP is a valid ``HOST``. - - .. versionchanged:: 20.0 - Support for ``fd://FD`` got added. - - Multiple addresses can be bound. ex.:: - - $ gunicorn -b 127.0.0.1:8000 -b [::1]:8000 test:app - - will bind the `test:app` application on localhost both on ipv6 - and ipv4 interfaces. - - If the ``PORT`` environment variable is defined, the default - is ``['0.0.0.0:$PORT']``. If it is not defined, the default - is ``['127.0.0.1:8000']``. - """ - - backlog: int = 2048 - """\ - The maximum number of pending connections. - - This refers to the number of clients that can be waiting to be served. - Exceeding this number results in the client getting an error when - attempting to connect. It should only affect servers under significant - load. - - Must be a positive integer. Generally set in the 64-2048 range. - """ - - workers: int = 0 - """\ - The number of worker processes for handling requests. - - A positive integer generally in the ``2-4 x $(NUM_CORES)`` range. - You'll want to vary this a bit to find the best for your particular - application's work load. - - By default, the value of the ``WEB_CONCURRENCY`` environment variable, - which is set by some Platform-as-a-Service providers such as Heroku. If - it is not defined, the default is ``1``. + https://docs.gunicorn.org/en/latest/settings.html """ + config: str = field_( + default="./gunicorn.conf.py", metadata_cli=CliType.default("--config {value}") + ) + bind: list[str] = field_( + default=None, + default_factory=lambda: ["127.0.0.1:8000"], + metadata_cli=CliType.multiple("--bind {value}"), + ) + backlog: int = field_( + default=2048, metadata_cli=CliType.default("--backlog {value}") + ) + workers: int = field_(default=0, metadata_cli=CliType.default("--workers {value}")) worker_class: Literal[ "sync", "eventlet", @@ -166,1016 +112,258 @@ class GunicornBackendServer(CustomBackendServer): "tornado", "gthread", "uvicorn.workers.UvicornH11Worker", - ] = "sync" - """\ - The type of workers to use. - - The default class (``sync``) should handle most "normal" types of - workloads. You'll want to read :doc:`design` for information on when - you might want to choose one of the other worker classes. Required - libraries may be installed using setuptools' ``extras_require`` feature. - - A string referring to one of the following bundled classes: - - * ``sync`` - * ``eventlet`` - Requires eventlet >= 0.24.1 (or install it via - ``pip install gunicorn[eventlet]``) - * ``gevent`` - Requires gevent >= 1.4 (or install it via - ``pip install gunicorn[gevent]``) - * ``tornado`` - Requires tornado >= 0.2 (or install it via - ``pip install gunicorn[tornado]``) - * ``gthread`` - Python 2 requires the futures package to be installed - (or install it via ``pip install gunicorn[gthread]``) - - Optionally, you can provide your own worker by giving Gunicorn a - Python path to a subclass of ``gunicorn.workers.base.Worker``. - This alternative syntax will load the gevent class: - ``gunicorn.workers.ggevent.GeventWorker``. - """ - - threads: int = 0 - """\ - The number of worker threads for handling requests. - - Run each worker with the specified number of threads. - - A positive integer generally in the ``2-4 x $(NUM_CORES)`` range. - You'll want to vary this a bit to find the best for your particular - application's work load. - - If it is not defined, the default is ``1``. - - This setting only affects the Gthread worker type. - - .. note:: - If you try to use the ``sync`` worker type and set the ``threads`` - setting to more than 1, the ``gthread`` worker type will be used - instead. - """ - - worker_connections: int = 1000 - """\ - The maximum number of simultaneous clients. - - This setting only affects the ``gthread``, ``eventlet`` and ``gevent`` worker types. - """ - - max_requests: int = 0 - """\ - The maximum number of requests a worker will process before restarting. - - Any value greater than zero will limit the number of requests a worker - will process before automatically restarting. This is a simple method - to help limit the damage of memory leaks. - - If this is set to zero (the default) then the automatic worker - restarts are disabled. - """ - - max_requests_jitter: int = 0 - """\ - The maximum jitter to add to the *max_requests* setting. - - The jitter causes the restart per worker to be randomized by - ``randint(0, max_requests_jitter)``. This is intended to stagger worker - restarts to avoid all workers restarting at the same time. - """ - - timeout: int = 30 - """\ - Workers silent for more than this many seconds are killed and restarted. - - Value is a positive number or 0. Setting it to 0 has the effect of - infinite timeouts by disabling timeouts for all workers entirely. - - Generally, the default of thirty seconds should suffice. Only set this - noticeably higher if you're sure of the repercussions for sync workers. - For the non sync workers it just means that the worker process is still - communicating and is not tied to the length of time required to handle a - single request. - """ - - graceful_timeout: int = 30 - """\ - Timeout for graceful workers restart. - - After receiving a restart signal, workers have this much time to finish - serving requests. Workers still alive after the timeout (starting from - the receipt of the restart signal) are force killed. - """ - - keepalive: int = 2 - """\ - The number of seconds to wait for requests on a Keep-Alive connection. - - Generally set in the 1-5 seconds range for servers with direct connection - to the client (e.g. when you don't have separate load balancer). When - Gunicorn is deployed behind a load balancer, it often makes sense to - set this to a higher value. - - .. note:: - ``sync`` worker does not support persistent connections and will - ignore this option. - """ - - limit_request_line: int = 4094 - """\ - The maximum size of HTTP request line in bytes. - - This parameter is used to limit the allowed size of a client's - HTTP request-line. Since the request-line consists of the HTTP - method, URI, and protocol version, this directive places a - restriction on the length of a request-URI allowed for a request - on the server. A server needs this value to be large enough to - hold any of its resource names, including any information that - might be passed in the query part of a GET request. Value is a number - from 0 (unlimited) to 8190. - - This parameter can be used to prevent any DDOS attack. - """ - - limit_request_fields: int = 100 - """\ - Limit the number of HTTP headers fields in a request. - - This parameter is used to limit the number of headers in a request to - prevent DDOS attack. Used with the *limit_request_field_size* it allows - more safety. By default this value is 100 and can't be larger than - 32768. - """ - - limit_request_field_size: int = 8190 - """\ - Limit the allowed size of an HTTP request header field. - - Value is a positive number or 0. Setting it to 0 will allow unlimited - header field sizes. - - .. warning:: - Setting this parameter to a very high or unlimited value can open - up for DDOS attacks. - """ - - reload: bool = False - """\ - Restart workers when code changes. - - This setting is intended for development. It will cause workers to be - restarted whenever application code changes. - - The reloader is incompatible with application preloading. When using a - paste configuration be sure that the server block does not import any - application code or the reload will not work as designed. - - The default behavior is to attempt inotify with a fallback to file - system polling. Generally, inotify should be preferred if available - because it consumes less system resources. - - .. note:: - In order to use the inotify reloader, you must have the ``inotify`` - package installed. - """ - - reload_engine: Literal["auto", "poll", "inotify"] = "auto" - """\ - The implementation that should be used to power :ref:`reload`. - - Valid engines are: - - * ``'auto'`` - * ``'poll'`` - * ``'inotify'`` (requires inotify) - """ - - reload_extra_files: list[str] = [] - """\ - Extends :ref:`reload` option to also watch and reload on additional files - (e.g., templates, configurations, specifications, etc.). - """ - - spew: bool = False - """\ - Install a trace function that spews every line executed by the server. - - This is the nuclear option. - """ - - check_config: bool = False - """\ - Check the configuration and exit. The exit status is 0 if the - configuration is correct, and 1 if the configuration is incorrect. - """ - - print_config: bool = False - """\ - Print the configuration settings as fully resolved. Implies :ref:`check-config`. - """ - - preload_app: bool = False - """\ - Load application code before the worker processes are forked. - - By preloading an application you can save some RAM resources as well as - speed up server boot times. Although, if you defer application loading - to each worker process, you can reload your application code easily by - restarting workers. - """ - - sendfile: bool | None = None - """\ - Disables the use of ``sendfile()``. - - If not set, the value of the ``SENDFILE`` environment variable is used - to enable or disable its usage. - """ - - reuse_port: bool = False - """\ - Set the ``SO_REUSEPORT`` flag on the listening socket. - """ - - chdir: str = "." - """\ - Change directory to specified directory before loading apps. - """ - - daemon: bool = False - """\ - Daemonize the Gunicorn process. - - Detaches the server from the controlling terminal and enters the - background. - """ - - raw_env: list[str] = [] - """\ - Set environment variables in the execution environment. - - Should be a list of strings in the ``key=value`` format. - """ - - pidfile: str | None = None - """\ - A filename to use for the PID file. - - If not set, no PID file will be written. - """ - - worker_tmp_dir: str | None = None - """\ - A directory to use for the worker heartbeat temporary file. - - If not set, the default temporary directory will be used. - - .. note:: - The current heartbeat system involves calling ``os.fchmod`` on - temporary file handlers and may block a worker for arbitrary time - if the directory is on a disk-backed filesystem. - - See :ref:`blocking-os-fchmod` for more detailed information - and a solution for avoiding this problem. - """ - - user: int = os.geteuid() - """\ - Switch worker processes to run as this user. - - A valid user id (as an integer) or the name of a user that can be - retrieved with a call to ``pwd.getpwnam(value)`` or ``None`` to not - change the worker process user. - """ - - group: int = os.getegid() - """\ - Switch worker process to run as this group. - - A valid group id (as an integer) or the name of a user that can be - retrieved with a call to ``pwd.getgrnam(value)`` or ``None`` to not - change the worker processes group. - """ - - umask: int = 0 - """\ - A bit mask for the file mode on files written by Gunicorn. - - Note that this affects unix socket permissions. - - A valid value for the ``os.umask(mode)`` call or a string compatible - with ``int(value, 0)`` (``0`` means Python guesses the base, so values - like ``0``, ``0xFF``, ``0022`` are valid for decimal, hex, and octal - representations) - """ - - initgroups: bool = False - """\ - If true, set the worker process's group access list with all of the - groups of which the specified username is a member, plus the specified - group id. - """ - - tmp_upload_dir: str | None = None - """\ - Directory to store temporary request data as they are read. - - This may disappear in the near future. - - This path should be writable by the process permissions set for Gunicorn - workers. If not specified, Gunicorn will choose a system generated - temporary directory. - """ - - secure_scheme_headers: dict[str, Any] = { - "X-FORWARDED-PROTOCOL": "ssl", - "X-FORWARDED-PROTO": "https", - "X-FORWARDED-SSL": "on", - } - """\ - A dictionary containing headers and values that the front-end proxy - uses to indicate HTTPS requests. If the source IP is permitted by - :ref:`forwarded-allow-ips` (below), *and* at least one request header matches - a key-value pair listed in this dictionary, then Gunicorn will set - ``wsgi.url_scheme`` to ``https``, so your application can tell that the - request is secure. - - If the other headers listed in this dictionary are not present in the request, they will be ignored, - but if the other headers are present and do not match the provided values, then - the request will fail to parse. See the note below for more detailed examples of this behaviour. - - The dictionary should map upper-case header names to exact string - values. The value comparisons are case-sensitive, unlike the header - names, so make sure they're exactly what your front-end proxy sends - when handling HTTPS requests. - - It is important that your front-end proxy configuration ensures that - the headers defined here can not be passed directly from the client. - """ - - forwarded_allow_ips: str = "127.0.0.1,::1" - """\ - Front-end's IPs from which allowed to handle set secure headers. - (comma separated). - - Set to ``*`` to disable checking of front-end IPs. This is useful for setups - where you don't know in advance the IP address of front-end, but - instead have ensured via other means that only your - authorized front-ends can access Gunicorn. - - By default, the value of the ``FORWARDED_ALLOW_IPS`` environment - variable. If it is not defined, the default is ``"127.0.0.1,::1"``. - - .. note:: - - This option does not affect UNIX socket connections. Connections not associated with - an IP address are treated as allowed, unconditionally. - - .. note:: - - The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of - ``secure_scheme_headers`` is complex. Various scenarios are documented below to further elaborate. - In each case, we have a request from the remote address 134.213.44.18, and the default value of - ``secure_scheme_headers``: - - .. code:: - - secure_scheme_headers = { - 'X-FORWARDED-PROTOCOL': 'ssl', - 'X-FORWARDED-PROTO': 'https', - 'X-FORWARDED-SSL': 'on' - } - - - .. list-table:: - :header-rows: 1 - :align: center - :widths: auto - - * - ``forwarded-allow-ips`` - - Secure Request Headers - - Result - - Explanation - * - ``["127.0.0.1"]`` - - ``X-Forwarded-Proto: https`` - - ``wsgi.url_scheme = "http"`` - - IP address was not allowed - * - ``"*"`` - - - - ``wsgi.url_scheme = "http"`` - - IP address allowed, but no secure headers provided - * - ``"*"`` - - ``X-Forwarded-Proto: https`` - - ``wsgi.url_scheme = "https"`` - - IP address allowed, one request header matched - * - ``["134.213.44.18"]`` - - ``X-Forwarded-Ssl: on`` ``X-Forwarded-Proto: http`` - - ``InvalidSchemeHeaders()`` raised - - IP address allowed, but the two secure headers disagreed on if HTTPS was used - """ - - accesslog: str | None = None - """\ - The Access log file to write to. - - ``'-'`` means log to stdout. - """ - - disable_redirect_access_to_syslog: bool = False - """\ - Disable redirect access logs to syslog. - """ - - access_log_format: str = ( - '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + ] = field_(default="sync", metadata_cli=CliType.default("--worker-class {value}")) + threads: int = field_(default=0, metadata_cli=CliType.default("--threads {value}")) + worker_connections: int = field_( + default=1000, metadata_cli=CliType.default("--worker-connections {value}") ) - """\ - The access log format. - - =========== =========== - Identifier Description - =========== =========== - h remote address - l ``'-'`` - u user name (if HTTP Basic auth used) - t date of the request - r status line (e.g. ``GET / HTTP/1.1``) - m request method - U URL path without query string - q query string - H protocol - s status - B response length - b response length or ``'-'`` (CLF format) - f referrer (note: header is ``referer``) - a user agent - T request time in seconds - M request time in milliseconds - D request time in microseconds - L request time in decimal seconds - p process ID - {header}i request header - {header}o response header - {variable}e environment variable - =========== =========== - - Use lowercase for header and environment variable names, and put - ``{...}x`` names inside ``%(...)s``. For example:: - - %({x-forwarded-for}i)s - """ - - errorlog: str = "-" - """\ - The Error log file to write to. - - Using ``'-'`` for FILE makes gunicorn log to stderr. - """ - - loglevel: Literal["debug", "info", "warning", "error", "critical"] = "info" - """\ - The granularity of Error log outputs. - """ - - capture_output: bool = False - """\ - Redirect stdout/stderr to specified file in :ref:`errorlog`. - """ - - logger_class: str = "gunicorn.glogging.Logger" - """\ - The logger you want to use to log events in Gunicorn. - - The default class (``gunicorn.glogging.Logger``) handles most - normal usages in logging. It provides error and access logging. - - You can provide your own logger by giving Gunicorn a Python path to a - class that quacks like ``gunicorn.glogging.Logger``. - """ - - logconfig: str | None = None - """\ - The log config file to use. - Gunicorn uses the standard Python logging module's Configuration - file format. - """ - - logconfig_dict: dict = {} - """\ - The log config dictionary to use, using the standard Python - logging module's dictionary configuration format. This option - takes precedence over the :ref:`logconfig` and :ref:`logconfig-json` options, - which uses the older file configuration format and JSON - respectively. - - Format: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig - - For more context you can look at the default configuration dictionary for logging, - which can be found at ``gunicorn.glogging.CONFIG_DEFAULTS``. - """ - - logconfig_json: str | None = None - """\ - The log config to read config from a JSON file - - Format: https://docs.python.org/3/library/logging.config.html#logging.config.jsonConfig - """ - - syslog_addr: str = ( - "unix:///var/run/syslog" - if sys.platform == "darwin" - else ( - "unix:///var/run/log" - if sys.platform - in ( - "freebsd", - "dragonfly", - ) - else ( - "unix:///dev/log" - if sys.platform == "openbsd" - else "udp://localhost:514" - ) - ) + max_requests: int = field_( + default=0, metadata_cli=CliType.default("--max-requests {value}") ) - """\ - Address to send syslog messages. - - Address is a string of the form: - * ``unix://PATH#TYPE`` : for unix domain socket, ``TYPE`` can be ``stream`` for the stream driver or ``dgram`` for the dgram driver, ``stream`` is the default - * ``udp://HOST:PORT`` : for UDP sockets - * ``tcp://HOST:PORT`` : for TCP sockets - """ - - syslog: bool = False - """\ - Send *Gunicorn* logs to syslog. - """ - - syslog_prefix: str | None = None - """\ - Makes Gunicorn use the parameter as program-name in the syslog entries. - - All entries will be prefixed by ``gunicorn.``. By default the - program name is the name of the process. - """ - - syslog_facility: str = "user" - """\ - Syslog facility name - """ - - enable_stdio_inheritance: bool = False - """\ - Enable stdio inheritance. - - Enable inheritance for stdio file descriptors in daemon mode. - - Note: To disable the Python stdout buffering, you can to set the user - environment variable ``PYTHONUNBUFFERED`` . - """ - - statsd_host: str | None = None - """\ - The address of the StatsD server to log to. - - Address is a string of the form: - - * ``unix://PATH`` : for a unix domain socket. - * ``HOST:PORT`` : for a network address - """ - - dogstatsd_tags: str = "" - """\ - A comma-delimited list of datadog statsd (dogstatsd) tags to append to - statsd metrics. - """ - - statsd_prefix: str = "" - """\ - Prefix to use when emitting statsd metrics (a trailing ``.`` is added, - if not provided). - """ - - proc_name: str | None = None - """\ - A base to use with setproctitle for process naming. - - This affects things like ``ps`` and ``top``. If you're going to be - running more than one instance of Gunicorn you'll probably want to set a - name to tell them apart. This requires that you install the setproctitle - module. - - If not set, the *default_proc_name* setting will be used. - """ - - default_proc_name: str = "gunicorn" - """\ - Internal setting that is adjusted for each type of application. - """ - - pythonpath: str | None = None - """\ - A comma-separated list of directories to add to the Python path. - - e.g. - ``'/home/djangoprojects/myproject,/home/python/mylibrary'``. - """ - - paste: str | None = None - """\ - Load a PasteDeploy config file. The argument may contain a ``#`` - symbol followed by the name of an app section from the config file, - e.g. ``production.ini#admin``. - - At this time, using alternate server blocks is not supported. Use the - command line arguments to control server configuration instead. - """ - - on_starting: Callable = lambda server: None - """\ - Called just before the master process is initialized. - - The callable needs to accept a single instance variable for the Arbiter. - """ - - on_reload: Callable = lambda server: None - """\ - Called to recycle workers during a reload via SIGHUP. - - The callable needs to accept a single instance variable for the Arbiter. - """ - - when_ready: Callable = lambda server: None - """\ - Called just after the server is started. - - The callable needs to accept a single instance variable for the Arbiter. - """ - - pre_fork: Callable = lambda server, worker: None - """\ - Called just before a worker is forked. - - The callable needs to accept two instance variables for the Arbiter and - new Worker. - """ - - post_fork: Callable = lambda server, worker: None - """\ - Called just after a worker has been forked. - - The callable needs to accept two instance variables for the Arbiter and - new Worker. - """ - - post_worker_init: Callable = lambda worker: None - """\ - Called just after a worker has initialized the application. - - The callable needs to accept one instance variable for the initialized - Worker. - """ - - worker_int: Callable = lambda worker: None - """\ - Called just after a worker exited on SIGINT or SIGQUIT. - - The callable needs to accept one instance variable for the initialized - Worker. - """ - - worker_abort: Callable = lambda worker: None - """\ - Called when a worker received the SIGABRT signal. - - This call generally happens on timeout. - - The callable needs to accept one instance variable for the initialized - Worker. - """ - - pre_exec: Callable = lambda server: None - """\ - Called just before a new master process is forked. - - The callable needs to accept a single instance variable for the Arbiter. - """ - - pre_request: Callable = lambda worker, req: worker.log.debug( - "%s %s", req.method, req.path + max_requests_jitter: int = field_( + default=0, metadata_cli=CliType.default("--max-requests-jitter {value}") ) - """\ - Called just before a worker processes the request. - - The callable needs to accept two instance variables for the Worker and - the Request. - """ - - post_request: Callable = lambda worker, req, environ, resp: None - """\ - Called after a worker processes the request. - - The callable needs to accept two instance variables for the Worker and - the Request. - """ - - child_exit: Callable = lambda server, worker: None - """\ - Called just after a worker has been exited, in the master process. - - The callable needs to accept two instance variables for the Arbiter and - the just-exited Worker. - """ - - worker_exit: Callable = lambda server, worker: None - """\ - Called just after a worker has been exited, in the worker process. - - The callable needs to accept two instance variables for the Arbiter and - the just-exited Worker. - """ - - nworkers_changed: Callable = lambda server, new_value, old_value: None - """\ - Called just after *num_workers* has been changed. - - The callable needs to accept an instance variable of the Arbiter and - two integers of number of workers after and before change. - - If the number of workers is set for the first time, *old_value* would - be ``None``. - """ - - on_exit: Callable = lambda server: None - """\ - Called just before exiting Gunicorn. - - The callable needs to accept a single instance variable for the Arbiter. - """ - - ssl_context: Callable = ( - lambda config, default_ssl_context_factory: default_ssl_context_factory() + timeout: int = field_(default=30, metadata_cli=CliType.default("--timeout {value}")) + graceful_timeout: int = field_( + default=30, metadata_cli=CliType.default("--graceful-timeout {value}") ) - """\ - Called when SSLContext is needed. - - Allows customizing SSL context. - - The callable needs to accept an instance variable for the Config and - a factory function that returns default SSLContext which is initialized - with certificates, private key, cert_reqs, and ciphers according to - config and can be further customized by the callable. - The callable needs to return SSLContext object. - - Following example shows a configuration file that sets the minimum TLS version to 1.3: - - .. code-block:: python - - def ssl_context(conf, default_ssl_context_factory): - import ssl - context = default_ssl_context_factory() - context.minimum_version = ssl.TLSVersion.TLSv1_3 - return context - """ - - proxy_protocol: bool = False - """\ - Enable detect PROXY protocol (PROXY mode). - - Allow using HTTP and Proxy together. It may be useful for work with - stunnel as HTTPS frontend and Gunicorn as HTTP server. - - PROXY protocol: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt - - Example for stunnel config:: - - [https] - protocol = proxy - accept = 443 - connect = 80 - cert = /etc/ssl/certs/stunnel.pem - key = /etc/ssl/certs/stunnel.key - """ - - proxy_allow_ips: str = "127.0.0.1,::1" - """\ - Front-end's IPs from which allowed accept proxy requests (comma separated). - - Set to ``*`` to disable checking of front-end IPs. This is useful for setups - where you don't know in advance the IP address of front-end, but - instead have ensured via other means that only your - authorized front-ends can access Gunicorn. - - .. note:: - - This option does not affect UNIX socket connections. Connections not associated with - an IP address are treated as allowed, unconditionally. - """ - - keyfile: str | None = None - """\ - SSL key file - """ - - certfile: str | None = None - """\ - SSL certificate file - """ - - ssl_version: int = ( - ssl.PROTOCOL_TLS if hasattr(ssl, "PROTOCOL_TLS") else ssl.PROTOCOL_SSLv23 + keepalive: int = field_( + default=2, metadata_cli=CliType.default("--keep-alive {value}") + ) + limit_request_line: int = field_( + default=4094, metadata_cli=CliType.default("--limit-request-line {value}") + ) + limit_request_fields: int = field_( + default=100, metadata_cli=CliType.default("--limit-request-fields {value}") + ) + limit_request_field_size: int = field_( + default=8190, metadata_cli=CliType.default("--limit-request-field_size {value}") + ) + reload: bool = field_(default=False, metadata_cli=CliType.boolean("--reload")) + reload_engine: Literal["auto", "poll", "inotify"] = field_( + default="auto", metadata_cli=CliType.default("--reload-engine {value}") + ) + reload_extra_files: list[str] = field_( + default=None, + default_factory=lambda: [], + metadata_cli=CliType.default("--reload-extra-file {value}"), + ) + spew: bool = field_(default=False, metadata_cli=CliType.boolean("--spew")) + check_config: bool = field_( + default=False, metadata_cli=CliType.boolean("--check-config") + ) + print_config: bool = field_( + default=False, metadata_cli=CliType.boolean("--print-config") + ) + preload_app: bool = field_(default=False, metadata_cli=CliType.boolean("--preload")) + sendfile: bool | None = field_( + default=None, metadata_cli=CliType.boolean("--no-sendfile", bool_value=False) + ) + reuse_port: bool = field_( + default=False, metadata_cli=CliType.boolean("--reuse-port") + ) + chdir: str = field_(default=".", metadata_cli=CliType.default("--chdir {value}")) + daemon: bool = field_(default=False, metadata_cli=CliType.boolean("--daemon")) + raw_env: list[str] = field_( + default=None, + default_factory=lambda: [], + metadata_cli=CliType.multiple("--env {value}"), + ) + pidfile: str | None = field_( + default=None, metadata_cli=CliType.default("--pid {value}") + ) + worker_tmp_dir: str | None = field_( + default=None, metadata_cli=CliType.default("--worker-tmp-dir {value}") + ) + user: int = field_(default=1000, metadata_cli=CliType.default("--user {value}")) + group: int = field_(default=1000, metadata_cli=CliType.default("--group {value}")) + umask: int = field_(default=0, metadata_cli=CliType.default("--umask {value}")) + initgroups: bool = field_( + default=False, metadata_cli=CliType.boolean("--initgroups") + ) + tmp_upload_dir: str | None = field_(default=None, metadata_cli=None) + secure_scheme_headers: dict[str, Any] = field_( + default=None, + default_factory=lambda: { + "X-FORWARDED-PROTOCOL": "ssl", + "X-FORWARDED-PROTO": "https", + "X-FORWARDED-SSL": "on", + }, + metadata_cli=None, + ) + forwarded_allow_ips: str = field_( + default="127.0.0.1,::1", + metadata_cli=CliType.default("--forwarded-allow-ips {value}"), + ) + accesslog: str | None = field_( + default=None, metadata_cli=CliType.default("--access-logfile {value}") + ) + disable_redirect_access_to_syslog: bool = field_( + default=False, + metadata_cli=CliType.boolean("--disable-redirect-access-to-syslog"), + ) + access_log_format: str = field_( + default='%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"', + metadata_cli=CliType.default("--access-logformat {value}"), + ) + errorlog: str = field_( + default="-", metadata_cli=CliType.default("--error-logfile {value}") + ) + loglevel: Literal["debug", "info", "warning", "error", "critical"] = field_( + default="info", metadata_cli=CliType.default("--log-level {value}") + ) + capture_output: bool = field_( + default=False, metadata_cli=CliType.boolean("--capture-output") + ) + logger_class: str = field_( + default="gunicorn.glogging.Logger", + metadata_cli=CliType.default("--logger-class {value}"), + ) + logconfig: str | None = field_( + default=None, metadata_cli=CliType.default("--log-config {value}") + ) + logconfig_dict: dict = field_( + default=None, default_factory=lambda: {}, metadata_cli=None + ) + logconfig_json: str | None = field_( + default=None, metadata_cli=CliType.default("--log-config-json {value}") + ) + syslog_addr: str = field_( + default="udp://localhost:514", + metadata_cli=CliType.default("--log-syslog-to {value}"), + ) + syslog: bool = field_(default=False, metadata_cli=CliType.boolean("--log-syslog")) + syslog_prefix: str | None = field_( + default=None, metadata_cli=CliType.default("--log-syslog-prefix {value}") + ) + syslog_facility: str = field_( + default="user", metadata_cli=CliType.default("--log-syslog-facility {value}") + ) + enable_stdio_inheritance: bool = field_( + default=False, metadata_cli=CliType.boolean("--enable-stdio-inheritance") + ) + statsd_host: str | None = field_( + default=None, metadata_cli=CliType.default("--statsd-host {value}") + ) + dogstatsd_tags: str = field_( + default="", metadata_cli=CliType.default("--dogstatsd-tags {value}") + ) + statsd_prefix: str = field_( + default="", metadata_cli=CliType.default("--statsd-prefix {value}") + ) + proc_name: str | None = field_( + default=None, metadata_cli=CliType.default("--name {value}") + ) + default_proc_name: str = field_(default="gunicorn", metadata_cli=None) + pythonpath: str | None = field_( + default=None, metadata_cli=CliType.default("--pythonpath {value}") + ) + paste: str | None = field_( + default=None, metadata_cli=CliType.default("--paster {value}") + ) + on_starting: Callable = field_(default=lambda server: None, metadata_cli=None) + on_reload: Callable = field_(default=lambda server: None, metadata_cli=None) + when_ready: Callable = field_(default=lambda server: None, metadata_cli=None) + pre_fork: Callable = field_(default=lambda server, worker: None, metadata_cli=None) + post_fork: Callable = field_(default=lambda server, worker: None, metadata_cli=None) + post_worker_init: Callable = field_(default=lambda worker: None, metadata_cli=None) + worker_int: Callable = field_(default=lambda worker: None, metadata_cli=None) + worker_abort: Callable = field_(default=lambda worker: None, metadata_cli=None) + pre_exec: Callable = field_(default=lambda server: None, metadata_cli=None) + pre_request: Callable = field_( + default=lambda worker, req: worker.log.debug("%s %s", req.method, req.path), + metadata_cli=None, + ) + post_request: Callable = field_( + default=lambda worker, req, environ, resp: None, metadata_cli=None + ) + child_exit: Callable = field_( + default=lambda server, worker: None, metadata_cli=None + ) + worker_exit: Callable = field_( + default=lambda server, worker: None, metadata_cli=None + ) + nworkers_changed: Callable = field_( + default=lambda server, new_value, old_value: None, metadata_cli=None + ) + on_exit: Callable = field_(default=lambda server: None, metadata_cli=None) + ssl_context: Callable[[Any, Any], Any] = field_( + default=lambda config, + default_ssl_context_factory: default_ssl_context_factory(), + metadata_cli=None, + ) + proxy_protocol: bool = field_( + default=False, metadata_cli=CliType.boolean("--proxy-protocol") + ) + proxy_allow_ips: str = field_( + default="127.0.0.1,::1", + metadata_cli=CliType.default("--proxy-allow-from {value}"), + ) + keyfile: str | None = field_( + default=None, metadata_cli=CliType.default("--keyfile {value}") + ) + certfile: str | None = field_( + default=None, metadata_cli=CliType.default("--certfile {value}") + ) + ssl_version: int = field_( + default=2, metadata_cli=CliType.default("--ssl-version {value}") + ) + cert_reqs: int = field_( + default=0, metadata_cli=CliType.default("--cert-reqs {value}") + ) + ca_certs: str | None = field_( + default=None, metadata_cli=CliType.default("--ca-certs {value}") + ) + suppress_ragged_eofs: bool = field_( + default=True, metadata_cli=CliType.boolean("--suppress-ragged-eofs") + ) + do_handshake_on_connect: bool = field_( + default=False, metadata_cli=CliType.boolean("--do-handshake-on-connect") + ) + ciphers: str | None = field_( + default=None, metadata_cli=CliType.default("--ciphers {value}") + ) + raw_paste_global_conf: list[str] = field_( + default=None, + default_factory=lambda: [], + metadata_cli=CliType.multiple("--paste-global {value}"), + ) + permit_obsolete_folding: bool = field_( + default=False, metadata_cli=CliType.boolean("--permit-obsolete-folding") + ) + strip_header_spaces: bool = field_( + default=False, metadata_cli=CliType.boolean("--strip-header-spaces") + ) + permit_unconventional_http_method: bool = field_( + default=False, + metadata_cli=CliType.boolean("--permit-unconventional-http-method"), + ) + permit_unconventional_http_version: bool = field_( + default=False, + metadata_cli=CliType.boolean("--permit-unconventional-http-version"), + ) + casefold_http_method: bool = field_( + default=False, metadata_cli=CliType.boolean("--casefold-http-method") + ) + forwarder_headers: str = field_( + default="SCRIPT_NAME,PATH_INFO", + metadata_cli=CliType.default("--forwarder-headers {value}"), + ) + header_map: Literal["drop", "refuse", "dangerous"] = field_( + default="drop", metadata_cli=CliType.default("--header-map {value}") ) - """\ - SSL version to use (see stdlib ssl module's). - .. deprecated:: 21.0 - The option is deprecated and it is currently ignored. Use :ref:`ssl-context` instead. - - ============= ============ - --ssl-version Description - ============= ============ - SSLv3 SSLv3 is not-secure and is strongly discouraged. - SSLv23 Alias for TLS. Deprecated in Python 3.6, use TLS. - TLS Negotiate highest possible version between client/server. - Can yield SSL. (Python 3.6+) - TLSv1 TLS 1.0 - TLSv1_1 TLS 1.1 (Python 3.4+) - TLSv1_2 TLS 1.2 (Python 3.4+) - TLS_SERVER Auto-negotiate the highest protocol version like TLS, - but only support server-side SSLSocket connections. - (Python 3.6+) - ============= ============ - - .. versionchanged:: 19.7 - The default value has been changed from ``ssl.PROTOCOL_TLSv1`` to - ``ssl.PROTOCOL_SSLv23``. - .. versionchanged:: 20.0 - This setting now accepts string names based on ``ssl.PROTOCOL_`` - constants. - .. versionchanged:: 20.0.1 - The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to - ``ssl.PROTOCOL_TLS`` when Python >= 3.6 . - """ - - cert_reqs: int = ssl.CERT_NONE - """\ - Whether client certificate is required (see stdlib ssl module's) - - =========== =========================== - --cert-reqs Description - =========== =========================== - `0` ``ssl.CERT_NONE`` - `1` ``ssl.CERT_OPTIONAL`` - `2` ``ssl.CERT_REQUIRED`` - =========== =========================== - """ - - ca_certs: str | None = None - """\ - CA certificates file - """ - - suppress_ragged_eofs: bool = True - """\ - Suppress ragged EOFs (see stdlib ssl module's) - """ - - do_handshake_on_connect: bool = False - """\ - Whether to perform SSL handshake on socket connect (see stdlib ssl module's) - """ - - ciphers: str | None = None - """\ - SSL Cipher suite to use, in the format of an OpenSSL cipher list. - - By default we use the default cipher list from Python's ``ssl`` module, - which contains ciphers considered strong at the time of each Python - release. - - As a recommended alternative, the Open Web App Security Project (OWASP) - offers `a vetted set of strong cipher strings rated A+ to C- - `_. - OWASP provides details on user-agent compatibility at each security level. - - See the `OpenSSL Cipher List Format Documentation - `_ - for details on the format of an OpenSSL cipher list. - """ - - raw_paste_global_conf: list[str] = [] - """\ - Set a PasteDeploy global config variable in ``key=value`` form. - - The option can be specified multiple times. - - The variables are passed to the PasteDeploy entrypoint. Example:: - - $ gunicorn -b 127.0.0.1:8000 --paste development.ini --paste-global FOO=1 --paste-global BAR=2 - """ - - permit_obsolete_folding: bool = False - """\ - Permit requests employing obsolete HTTP line folding mechanism - - The folding mechanism was deprecated by rfc7230 Section 3.2.4 and will not be - employed in HTTP request headers from standards-compliant HTTP clients. - - This option is provided to diagnose backwards-incompatible changes. - Use with care and only if necessary. Temporary; the precise effect of this option may - change in a future version, or it may be removed altogether. - """ - - strip_header_spaces: bool = False - """\ - Strip spaces present between the header name and the the ``:``. - - This is known to induce vulnerabilities and is not compliant with the HTTP/1.1 standard. - See https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn. - - Use with care and only if necessary. Deprecated; scheduled for removal in 25.0.0 - """ - - permit_unconventional_http_method: bool = False - """\ - Permit HTTP methods not matching conventions, such as IANA registration guidelines - - This permits request methods of length less than 3 or more than 20, - methods with lowercase characters or methods containing the # character. - HTTP methods are case sensitive by definition, and merely uppercase by convention. - - If unset, Gunicorn will apply nonstandard restrictions and cause 400 response status - in cases where otherwise 501 status is expected. While this option does modify that - behaviour, it should not be depended upon to guarantee standards-compliant behaviour. - Rather, it is provided temporarily, to assist in diagnosing backwards-incompatible - changes around the incomplete application of those restrictions. - - Use with care and only if necessary. Temporary; scheduled for removal in 24.0.0 - """ - - permit_unconventional_http_version: bool = False - """\ - Permit HTTP version not matching conventions of 2023 - - This disables the refusal of likely malformed request lines. - It is unusual to specify HTTP 1 versions other than 1.0 and 1.1. - - This option is provided to diagnose backwards-incompatible changes. - Use with care and only if necessary. Temporary; the precise effect of this option may - change in a future version, or it may be removed altogether. - """ - - casefold_http_method: bool = False - """\ - Transform received HTTP methods to uppercase - - HTTP methods are case sensitive by definition, and merely uppercase by convention. - - This option is provided because previous versions of gunicorn defaulted to this behaviour. - - Use with care and only if necessary. Deprecated; scheduled for removal in 24.0.0 - """ - - forwarder_headers: str = "SCRIPT_NAME,PATH_INFO" - """\ - A list containing upper-case header field names that the front-end proxy - (see :ref:`forwarded-allow-ips`) sets, to be used in WSGI environment. - - This option has no effect for headers not present in the request. - - This option can be used to transfer ``SCRIPT_NAME``, ``PATH_INFO`` - and ``REMOTE_USER``. - - It is important that your front-end proxy configuration ensures that - the headers defined here can not be passed directly from the client. - """ - - header_map: Literal["drop", "refuse", "dangerous"] = "drop" - """\ - Configure how header field names are mapped into environ - - Headers containing underscores are permitted by RFC9110, - but gunicorn joining headers of different names into - the same environment variable will dangerously confuse applications as to which is which. - - The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped. - The value ``refuse`` will return an error if a request contains *any* such header. - The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different - header field names into the same environ name. - - If the source is permitted as explained in :ref:`forwarded-allow-ips`, *and* the header name is - present in :ref:`forwarder-headers`, the header is mapped into environment regardless of - the state of this setting. - - Use with care and only if necessary and after considering if your problem could - instead be solved by specifically renaming or rewriting only the intended headers - on a proxy in front of Gunicorn. - """ - - def check_import(self, extra: bool = False): + def check_import(self): """Check package importation.""" from importlib.util import find_spec errors: list[str] = [] + if IS_WINDOWS: + errors.append( + "The `GunicornBackendServer` only works on UNIX machines. We recommend using the `UvicornBackendServer` for Windows machines." + ) + if find_spec("gunicorn") is None: errors.append( 'The `gunicorn` package is required to run `GunicornBackendServer`. Run `pip install "gunicorn>=20.1.0"`.' @@ -1187,7 +375,7 @@ class GunicornBackendServer(CustomBackendServer): def setup(self, host: str, port: int, loglevel: LogLevel, env: Env): """Setup.""" - self.app_uri = f"{self.get_app_module()}()" + self._app_uri = f"{self.get_app_module()}()" self.loglevel = loglevel.value # type: ignore self.bind = [f"{host}:{port}"] @@ -1211,25 +399,17 @@ class GunicornBackendServer(CustomBackendServer): def run_prod(self) -> list[str]: """Run in production mode.""" self.check_import() - command = ["gunicorn"] for key, field in self.get_fields().items(): - if key != "app": - 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): - if (key == "sendfile" and value is False) or ( - key != "sendfile" and value - ): - command.append(_mapping_attr_to_cli[key]) - 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 + [f"{self.get_app_module()}()"] + return command + [self._app_uri] def run_dev(self): """Run in development mode.""" @@ -1241,25 +421,29 @@ class GunicornBackendServer(CustomBackendServer): from gunicorn.app.base import BaseApplication from gunicorn.util import import_app as gunicorn_import_app - options_ = self.dict() - options_.pop("app", None) + model = self.get_fields() + options_ = { + key: value + for key, value in self.get_values().items() + if value != model[key].default + } class StandaloneApplication(BaseApplication): def __init__(self, app_uri, options=None): self.options = options or {} - self.app_uri = app_uri + self._app_uri = app_uri super().__init__() def load_config(self): config = { key: value for key, value in self.options.items() - if key in self.cfg.settings and value is not None - } # type: ignore + if key in self.cfg.settings and value is not None # type: ignore + } for key, value in config.items(): self.cfg.set(key.lower(), value) # type: ignore def load(self): - return gunicorn_import_app(self.app_uri) + return gunicorn_import_app(self._app_uri) - StandaloneApplication(app_uri=self.app_uri, options=options_).run() + StandaloneApplication(app_uri=self._app_uri, options=options_).run() diff --git a/reflex/server/uvicorn.py b/reflex/server/uvicorn.py index cb5131c4a..8d167eacb 100644 --- a/reflex/server/uvicorn.py +++ b/reflex/server/uvicorn.py @@ -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() diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 06a61a293..4daaa69b8 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -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(