eptm_dashboard/.venv/lib/python3.12/site-packages/reflex_base/config.py

734 lines
27 KiB
Python

"""The Reflex config."""
import dataclasses
import importlib
import os
import sys
import threading
import urllib.parse
from collections.abc import Sequence
from importlib.util import find_spec
from pathlib import Path
from types import ModuleType
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal
from reflex_base import constants
from reflex_base.constants.base import LogLevel
from reflex_base.environment import EnvironmentVariables as EnvironmentVariables
from reflex_base.environment import EnvVar as EnvVar
from reflex_base.environment import (
ExistingPath,
SequenceOptions,
_load_dotenv_from_files,
_paths_from_env_files,
interpret_env_var_value,
)
from reflex_base.environment import env_var as env_var
from reflex_base.environment import environment as environment
from reflex_base.plugins import Plugin
from reflex_base.plugins.sitemap import SitemapPlugin
from reflex_base.utils import console
from reflex_base.utils.exceptions import ConfigError
@dataclasses.dataclass(kw_only=True)
class DBConfig:
"""Database config."""
engine: str
username: str | None = ""
password: str | None = ""
host: str | None = ""
port: int | None = None
database: str
@classmethod
def postgresql(
cls,
database: str,
username: str,
password: str | None = None,
host: str | None = None,
port: int | None = 5432,
) -> "DBConfig":
"""Create an instance with postgresql engine.
Args:
database: Database name.
username: Database username.
password: Database password.
host: Database host.
port: Database port.
Returns:
DBConfig instance.
"""
return cls(
engine="postgresql",
username=username,
password=password,
host=host,
port=port,
database=database,
)
@classmethod
def postgresql_psycopg(
cls,
database: str,
username: str,
password: str | None = None,
host: str | None = None,
port: int | None = 5432,
) -> "DBConfig":
"""Create an instance with postgresql+psycopg engine.
Args:
database: Database name.
username: Database username.
password: Database password.
host: Database host.
port: Database port.
Returns:
DBConfig instance.
"""
return cls(
engine="postgresql+psycopg",
username=username,
password=password,
host=host,
port=port,
database=database,
)
@classmethod
def sqlite(
cls,
database: str,
) -> "DBConfig":
"""Create an instance with sqlite engine.
Args:
database: Database name.
Returns:
DBConfig instance.
"""
return cls(
engine="sqlite",
database=database,
)
def get_url(self) -> str:
"""Get database URL.
Returns:
The database URL.
"""
host = (
f"{self.host}:{self.port}" if self.host and self.port else self.host or ""
)
username = urllib.parse.quote_plus(self.username) if self.username else ""
password = urllib.parse.quote_plus(self.password) if self.password else ""
if username:
path = f"{username}:{password}@{host}" if password else f"{username}@{host}"
else:
path = f"{host}"
return f"{self.engine}://{path}/{self.database}"
# These vars are not logged because they may contain sensitive information.
_sensitive_env_vars = {"DB_URL", "ASYNC_DB_URL", "REDIS_URL"}
@dataclasses.dataclass(kw_only=True)
class BaseConfig:
"""Base config for the Reflex app.
Attributes:
app_name: The name of the app (should match the name of the app directory).
app_module_import: The path to the app module.
loglevel: The log level to use.
frontend_port: The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
frontend_path: The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
backend_port: The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
backend_path: The path prefix for backend routes. For example, "/api" mounts the event websocket, /ping, /_upload, /_health, and /_all_routes under /api, and is automatically included in URLs baked into the frontend. Changing this requires a full `reflex run` restart — routes are registered at startup.
api_url: The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production.
deploy_url: The url the frontend will be hosted on.
backend_host: The url the backend will be hosted on.
db_url: The database url used by rx.Model.
async_db_url: The async database url used by rx.Model.
redis_url: The redis url.
telemetry_enabled: Telemetry opt-in.
bun_path: The bun path.
static_page_generation_timeout: Timeout to do a production build of a frontend page.
cors_allowed_origins: Comma separated list of origins that are allowed to connect to the backend API.
vite_allowed_hosts: Allowed hosts for the Vite dev server. Set to True to allow all hosts, or provide a list of hostnames (e.g. ["myservice.local"]) to allow specific ones. Prevents 403 errors in Docker, Codespaces, reverse proxies, etc.
react_strict_mode: Whether to use React strict mode.
frontend_packages: Additional frontend packages to install.
state_manager_mode: Indicate which type of state manager to use.
redis_lock_expiration: Maximum expiration lock time for redis state manager.
redis_lock_warning_threshold: Maximum lock time before warning for redis state manager.
redis_token_expiration: Token expiration time for redis state manager.
env_file: Path to file containing key-values pairs to override in the environment; Dotenv format.
state_auto_setters: Whether to automatically create setters for state base vars.
show_built_with_reflex: Whether to display the sticky "Built with Reflex" badge on all pages.
is_reflex_cloud: Whether the app is running in the reflex cloud environment.
extra_overlay_function: Extra overlay function to run after the app is built. Formatted such that `from path_0.path_1... import path[-1]`, and calling it with no arguments would work. For example, "reflex_components_moment.moment".
plugins: List of plugins to use in the app.
disable_plugins: List of plugin types to disable in the app.
transport: The transport method for client-server communication.
"""
app_name: str
app_module_import: str | None = None
loglevel: constants.LogLevel = constants.LogLevel.DEFAULT
frontend_port: int | None = None
frontend_path: str = ""
backend_port: int | None = None
backend_path: str = ""
api_url: str = f"http://localhost:{constants.DefaultPorts.BACKEND_PORT}"
deploy_url: str | None = f"http://localhost:{constants.DefaultPorts.FRONTEND_PORT}"
backend_host: str = "0.0.0.0"
db_url: str | None = None
async_db_url: str | None = None
redis_url: str | None = None
telemetry_enabled: bool = True
bun_path: ExistingPath = constants.Bun.DEFAULT_PATH
static_page_generation_timeout: int = 60
cors_allowed_origins: Annotated[
Sequence[str],
SequenceOptions(delimiter=","),
] = dataclasses.field(default=("*",))
vite_allowed_hosts: bool | list[str] = False
react_strict_mode: bool = True
frontend_packages: list[str] = dataclasses.field(default_factory=list)
state_manager_mode: constants.StateManagerMode = constants.StateManagerMode.DISK
redis_lock_expiration: int = constants.Expiration.LOCK
redis_lock_warning_threshold: int = constants.Expiration.LOCK_WARNING_THRESHOLD
redis_token_expiration: int = constants.Expiration.TOKEN
# Attributes that were explicitly set by the user.
_non_default_attributes: set[str] = dataclasses.field(
default_factory=set, init=False
)
env_file: str | None = None
state_auto_setters: bool = False
show_built_with_reflex: bool | None = None
is_reflex_cloud: bool = False
extra_overlay_function: str | None = None
plugins: list[Plugin] = dataclasses.field(default_factory=list)
disable_plugins: list[type[Plugin]] = dataclasses.field(default_factory=list)
transport: Literal["websocket", "polling"] = "websocket"
# Whether to skip plugin checks.
_skip_plugins_checks: bool = dataclasses.field(default=False, repr=False)
_prefixes: ClassVar[list[str]] = ["REFLEX_"]
_PLUGINS_ENABLED_BY_DEFAULT = [
SitemapPlugin,
]
@dataclasses.dataclass(kw_only=True, init=False)
class Config(BaseConfig):
"""Configuration class for Reflex applications.
The config defines runtime settings for your app including server ports, database connections,
frontend packages, and deployment settings.
By default, the config is defined in an `rxconfig.py` file in the root of your app:
```python
# rxconfig.py
import reflex as rx
config = rx.Config(
app_name="myapp",
# Server configuration
frontend_port=3000,
backend_port=8000,
# Database
db_url="postgresql://user:pass@localhost:5432/mydb",
# Additional frontend packages
frontend_packages=["react-icons"],
# CORS settings for production
cors_allowed_origins=["https://mydomain.com"],
)
```
## Environment Variable Overrides
Any config value can be overridden by setting an environment variable with the `REFLEX_`
prefix and the parameter name in uppercase:
```bash
REFLEX_DB_URL="postgresql://user:pass@localhost/db" reflex run
REFLEX_FRONTEND_PORT=3001 reflex run
```
## Key Configuration Areas
- **App Settings**: `app_name`, `loglevel`, `telemetry_enabled`
- **Server**: `frontend_port`, `backend_port`, `api_url`, `cors_allowed_origins`
- **Database**: `db_url`, `async_db_url`, `redis_url`
- **Frontend**: `frontend_packages`, `react_strict_mode`
- **State Management**: `state_manager_mode`, `state_auto_setters`
- **Plugins**: `plugins`, `disable_plugins`
See the [configuration docs](https://reflex.dev/docs/advanced-onboarding/configuration) for complete details on all available options.
"""
# Track whether the app name has already been validated for this Config instance.
_app_name_is_valid: bool = dataclasses.field(default=False, repr=False)
def _post_init(self, **kwargs):
"""Post-initialization method to set up the config.
This method is called after the config is initialized. It sets up the
environment variables, updates the config from the environment, and
replaces default URLs if ports were set.
Args:
**kwargs: The kwargs passed to the Pydantic init method.
Raises:
ConfigError: If some values in the config are invalid.
"""
class_fields = self.class_fields()
for key, value in kwargs.items():
if key not in class_fields:
setattr(self, key, value)
# Clean up this code when we remove plain envvar in 0.8.0
env_loglevel = os.environ.get("REFLEX_LOGLEVEL")
if env_loglevel is not None:
env_loglevel = LogLevel(env_loglevel.lower())
if env_loglevel or self.loglevel != LogLevel.DEFAULT:
console.set_log_level(env_loglevel or self.loglevel)
# Update the config from environment variables.
env_kwargs = self.update_from_env()
for key, env_value in env_kwargs.items():
setattr(self, key, env_value)
# Normalize plugins: auto-instantiate Plugin subclasses, reject bad values.
self._normalize_plugins()
# Normalize disable_plugins: convert strings and Plugin subclasses to instances.
self._normalize_disable_plugins()
# Add builtin plugins if not disabled.
if not self._skip_plugins_checks:
self._add_builtin_plugins()
# Warn if state_auto_setters is explicitly set.
if "state_auto_setters" in kwargs:
if kwargs["state_auto_setters"]:
reason = (
"auto setters will be removed; use explicit event handlers instead"
)
else:
reason = "state_auto_setters=False is already the default and the option will be removed"
console.deprecate(
feature_name="state_auto_setters",
reason=reason,
deprecation_version="0.9.0",
removal_version="1.0",
)
# Update default URLs if ports were set
kwargs.update(env_kwargs)
self._non_default_attributes = set(kwargs.keys())
self._replace_defaults(**kwargs)
if (
self.state_manager_mode == constants.StateManagerMode.REDIS
and not self.redis_url
):
msg = f"{self._prefixes[0]}REDIS_URL is required when using the redis state manager."
raise ConfigError(msg)
def _normalize_plugins(self):
"""Normalize ``plugins`` entries to Plugin instances.
Auto-instantiates Plugin subclasses passed without parentheses (e.g.
``plugins=[SitemapPlugin]``) so they behave the same as
``plugins=[SitemapPlugin()]``. Any entry that is neither a Plugin
subclass nor a Plugin instance raises ``ConfigError`` with a message
that names the offending value, instead of failing later in the
compiler with a confusing ``TypeError`` about a missing ``self``.
"""
normalized: list[Plugin] = []
for entry in self.plugins:
if isinstance(entry, Plugin):
normalized.append(entry)
elif isinstance(entry, type) and issubclass(entry, Plugin):
try:
normalized.append(entry())
except TypeError as exc:
msg = (
f"reflex.Config.plugins entry {entry.__name__!r} could not be "
f"instantiated and may require arguments; pass an instance "
f"instead, e.g. plugins=[{entry.__name__}(...)]."
)
raise ConfigError(msg) from exc
else:
msg = (
f"reflex.Config.plugins must contain Plugin instances, but got "
f"{entry!r} of type {type(entry).__name__}. "
f"Pass an instance, e.g. plugins=[SitemapPlugin()]."
)
raise ConfigError(msg)
self.plugins = normalized
def _normalize_disable_plugins(self):
"""Normalize disable_plugins list entries to Plugin subclasses.
Handles backward compatibility by converting strings (fully qualified
import paths) and Plugin instances to their associated classes.
"""
normalized: list[type[Plugin]] = []
for entry in self.disable_plugins:
if isinstance(entry, type) and issubclass(entry, Plugin):
normalized.append(entry)
elif isinstance(entry, Plugin):
normalized.append(type(entry))
elif isinstance(entry, str):
console.deprecate(
feature_name="Passing strings to disable_plugins",
reason="pass Plugin classes directly instead, e.g. disable_plugins=[SitemapPlugin]",
deprecation_version="0.8.28",
removal_version="1.0",
)
try:
from reflex_base.environment import interpret_plugin_class_env
normalized.append(
interpret_plugin_class_env(entry, "disable_plugins")
)
except Exception:
console.warn(
f"Failed to import plugin from string {entry!r} in disable_plugins. "
"Please pass Plugin subclasses directly.",
)
else:
console.warn(
f"reflex.Config.disable_plugins should contain Plugin subclasses, but got {entry!r}.",
)
self.disable_plugins = normalized
def _add_builtin_plugins(self):
"""Add the builtin plugins to the config."""
for plugin in _PLUGINS_ENABLED_BY_DEFAULT:
plugin_name = plugin.__module__ + "." + plugin.__qualname__
if plugin not in self.disable_plugins:
if not any(isinstance(p, plugin) for p in self.plugins):
console.warn(
f"`{plugin_name}` plugin is enabled by default, but not explicitly added to the config. "
"If you want to use it, please add it to the `plugins` list in your config inside of `rxconfig.py`. "
f"To disable this plugin, add `{plugin.__name__}` to the `disable_plugins` list.",
)
self.plugins.append(plugin())
else:
if any(isinstance(p, plugin) for p in self.plugins):
console.warn(
f"`{plugin_name}` is disabled in the config, but it is still present in the `plugins` list. "
"Please remove it from the `plugins` list in your config inside of `rxconfig.py`.",
)
for disabled_plugin in self.disable_plugins:
if disabled_plugin not in _PLUGINS_ENABLED_BY_DEFAULT:
console.warn(
f"`{disabled_plugin!r}` is disabled in the config, but it is not a built-in plugin. "
"Please remove it from the `disable_plugins` list in your config inside of `rxconfig.py`.",
)
@classmethod
def class_fields(cls) -> set[str]:
"""Get the fields of the config class.
Returns:
The fields of the config class.
"""
return {field.name for field in dataclasses.fields(cls)}
if not TYPE_CHECKING:
def __init__(self, **kwargs):
"""Initialize the config values.
Args:
**kwargs: The kwargs to pass to the Pydantic init method.
# noqa: DAR101 self
"""
class_fields = self.class_fields()
super().__init__(**{k: v for k, v in kwargs.items() if k in class_fields})
self._post_init(**kwargs)
def json(self) -> str:
"""Get the config as a JSON string.
Returns:
The config as a JSON string.
"""
import json
from reflex_base.utils.serializers import serialize
return json.dumps(self, default=serialize)
@staticmethod
def _prepend_path(path: str, prefix: str) -> str:
"""Prepend ``prefix`` (normalized to ``/prefix``) to ``path`` when both are non-empty.
Args:
path: The path to prepend the prefix to.
prefix: The configured prefix (e.g. ``frontend_path`` or ``backend_path``).
Returns:
The path with the prefix prepended if it begins with a slash, otherwise the original path.
"""
if prefix and path.startswith("/"):
return f"/{prefix.strip('/')}{path}"
return path
def prepend_frontend_path(self, path: str) -> str:
"""Prepend the frontend path to a given path.
Args:
path: The path to prepend the frontend path to.
Returns:
The path with the frontend path prepended if it begins with a slash, otherwise the original path.
"""
return self._prepend_path(path, self.frontend_path)
def prepend_backend_path(self, path: str) -> str:
"""Prepend the backend path to a given path.
Args:
path: The path to prepend the backend path to.
Returns:
The path with the backend path prepended if it begins with a slash, otherwise the original path.
"""
return self._prepend_path(path, self.backend_path)
@property
def app_module(self) -> ModuleType | None:
"""Return the app module if `app_module_import` is set.
Returns:
The app module.
"""
return (
importlib.import_module(self.app_module_import)
if self.app_module_import
else None
)
@property
def module(self) -> str:
"""Get the module name of the app.
Returns:
The module name.
"""
if self.app_module_import is not None:
return self.app_module_import
return self.app_name + "." + self.app_name
def update_from_env(self) -> dict[str, Any]:
"""Update the config values based on set environment variables.
If there is a set env_file, it is loaded first.
Returns:
The updated config values.
"""
if self.env_file:
_load_dotenv_from_files(_paths_from_env_files(self.env_file))
updated_values = {}
# Iterate over the fields.
for field in dataclasses.fields(self):
# The env var name is the key in uppercase.
environment_variable = None
for prefix in self._prefixes:
if environment_variable := os.environ.get(
f"{prefix}{field.name.upper()}"
):
break
# If the env var is set, override the config value.
if environment_variable and environment_variable.strip():
# Interpret the value.
value = interpret_env_var_value(
environment_variable,
field.type,
field.name,
)
# Set the value.
updated_values[field.name] = value
if field.name.upper() in _sensitive_env_vars:
environment_variable = "***"
if value != getattr(self, field.name):
console.debug(
f"Overriding config value {field.name} with env var {field.name.upper()}={environment_variable}",
dedupe=True,
)
return updated_values
def get_event_namespace(self) -> str:
"""Get the path that the backend Websocket server lists on.
Returns:
The namespace for websocket.
"""
event_url = constants.Endpoint.EVENT.get_url()
return urllib.parse.urlsplit(event_url).path
def _replace_defaults(self, **kwargs):
"""Replace formatted defaults when the caller provides updates.
Args:
**kwargs: The kwargs passed to the config or from the env.
"""
if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
self.api_url = f"http://localhost:{kwargs['backend_port']}"
if (
"deploy_url" not in self._non_default_attributes
and "frontend_port" in kwargs
):
self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
if "api_url" not in self._non_default_attributes:
# If running in Github Codespaces, override API_URL
codespace_name = os.getenv("CODESPACE_NAME")
github_codespaces_port_forwarding_domain = os.getenv(
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
)
# If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port
replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN")
backend_port = kwargs.get("backend_port", self.backend_port)
if codespace_name and github_codespaces_port_forwarding_domain:
self.api_url = (
f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
f".{github_codespaces_port_forwarding_domain}"
)
elif replit_dev_domain and backend_port:
self.api_url = f"https://{replit_dev_domain}:{backend_port}"
def _set_persistent(self, **kwargs):
"""Set values in this config and in the environment so they persist into subprocess.
Args:
**kwargs: The kwargs passed to the config.
"""
for key, value in kwargs.items():
if value is not None:
os.environ[self._prefixes[0] + key.upper()] = str(value)
setattr(self, key, value)
self._non_default_attributes.update(kwargs)
self._replace_defaults(**kwargs)
def _get_config() -> Config:
"""Get the app config.
Returns:
The app config.
"""
# only import the module if it exists. If a module spec exists then
# the module exists.
spec = find_spec(constants.Config.MODULE)
if not spec:
# we need this condition to ensure that a ModuleNotFound error is not thrown when
# running unit/integration tests or during `reflex init`.
return Config(app_name="", _skip_plugins_checks=True)
rxconfig = importlib.import_module(constants.Config.MODULE)
return rxconfig.config
# Protect sys.path from concurrent modification
_config_lock = threading.RLock()
def get_config(reload: bool = False) -> Config:
"""Get the app config.
Args:
reload: Re-import the rxconfig module from disk
Returns:
The app config.
"""
cached_rxconfig = sys.modules.get(constants.Config.MODULE, None)
if cached_rxconfig is not None:
if reload:
# Remove any cached module when `reload` is requested.
del sys.modules[constants.Config.MODULE]
else:
return cached_rxconfig.config
with _config_lock:
orig_sys_path = sys.path.copy()
sys.path.clear()
sys.path.append(str(Path.cwd()))
try:
# Try to import the module with only the current directory in the path.
return _get_config()
except Exception:
# If the module import fails, try to import with the original sys.path.
sys.path.extend(orig_sys_path)
return _get_config()
finally:
# Find any entries added to sys.path by rxconfig.py itself.
extra_paths = [
p for p in sys.path if p not in orig_sys_path and p != str(Path.cwd())
]
# Restore the original sys.path.
sys.path.clear()
sys.path.extend(extra_paths)
sys.path.extend(orig_sys_path)