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

775 lines
25 KiB
Python

"""Environment variable management."""
from __future__ import annotations
import dataclasses
import enum
import importlib
import os
from collections.abc import Sequence
from functools import lru_cache
from pathlib import Path
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Generic,
Literal,
TypeVar,
get_args,
get_origin,
get_type_hints,
)
from reflex_base import constants
from reflex_base.constants.base import LogLevel
from reflex_base.plugins import Plugin
from reflex_base.utils.exceptions import EnvironmentVarValueError
from reflex_base.utils.types import GenericType, is_union, value_inside_optional
def get_default_value_for_field(field: dataclasses.Field) -> Any:
"""Get the default value for a field.
Args:
field: The field.
Returns:
The default value.
Raises:
ValueError: If no default value is found.
"""
if field.default != dataclasses.MISSING:
return field.default
if field.default_factory != dataclasses.MISSING:
return field.default_factory()
msg = f"Missing value for environment variable {field.name} and no default value found"
raise ValueError(msg)
# TODO: Change all interpret_.* signatures to value: str, field: dataclasses.Field once we migrate rx.Config to dataclasses
def interpret_boolean_env(value: str, field_name: str) -> bool:
"""Interpret a boolean environment variable value.
Args:
value: The environment variable value.
field_name: The field name.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
true_values = ["true", "1", "yes", "y"]
false_values = ["false", "0", "no", "n"]
if value.lower() in true_values:
return True
if value.lower() in false_values:
return False
msg = f"Invalid boolean value: {value!r} for {field_name}"
raise EnvironmentVarValueError(msg)
def interpret_int_env(value: str, field_name: str) -> int:
"""Interpret an integer environment variable value.
Args:
value: The environment variable value.
field_name: The field name.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
try:
return int(value)
except ValueError as ve:
msg = f"Invalid integer value: {value!r} for {field_name}"
raise EnvironmentVarValueError(msg) from ve
def interpret_float_env(value: str, field_name: str) -> float:
"""Interpret a float environment variable value.
Args:
value: The environment variable value.
field_name: The field name.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
try:
return float(value)
except ValueError as ve:
msg = f"Invalid float value: {value!r} for {field_name}"
raise EnvironmentVarValueError(msg) from ve
def interpret_existing_path_env(value: str, field_name: str) -> ExistingPath:
"""Interpret a path environment variable value as an existing path.
Args:
value: The environment variable value.
field_name: The field name.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the path does not exist.
"""
path = Path(value)
if not path.exists():
msg = f"Path does not exist: {path!r} for {field_name}"
raise EnvironmentVarValueError(msg)
return path
def interpret_path_env(value: str, field_name: str) -> Path:
"""Interpret a path environment variable value.
Args:
value: The environment variable value.
field_name: The field name.
Returns:
The interpreted value.
"""
return Path(value)
def interpret_plugin_class_env(value: str, field_name: str) -> type[Plugin]:
"""Interpret an environment variable value as a Plugin subclass.
Resolves a fully qualified import path to the Plugin subclass it refers to.
Args:
value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin").
field_name: The field name.
Returns:
The Plugin subclass.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
if "." not in value:
msg = f"Invalid plugin value: {value!r} for {field_name}. Plugin name must be in the format 'package.module.PluginName'."
raise EnvironmentVarValueError(msg)
import_path, plugin_name = value.rsplit(".", 1)
try:
module = importlib.import_module(import_path)
except ImportError as e:
msg = f"Failed to import module {import_path!r} for {field_name}: {e}"
raise EnvironmentVarValueError(msg) from e
try:
plugin_class = getattr(module, plugin_name, None)
except Exception as e:
msg = f"Failed to get plugin class {plugin_name!r} from module {import_path!r} for {field_name}: {e}"
raise EnvironmentVarValueError(msg) from e
if not isinstance(plugin_class, type) or not issubclass(plugin_class, Plugin):
msg = f"Invalid plugin class: {plugin_name!r} for {field_name}. Must be a subclass of Plugin."
raise EnvironmentVarValueError(msg)
return plugin_class
def interpret_plugin_env(value: str, field_name: str) -> Plugin:
"""Interpret a plugin environment variable value.
Resolves a fully qualified import path and returns an instance of the Plugin.
Args:
value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin").
field_name: The field name.
Returns:
An instance of the Plugin subclass.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
plugin_class = interpret_plugin_class_env(value, field_name)
try:
return plugin_class()
except Exception as e:
msg = f"Failed to instantiate plugin {plugin_class.__name__!r} for {field_name}: {e}"
raise EnvironmentVarValueError(msg) from e
def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any:
"""Interpret an enum environment variable value.
Args:
value: The environment variable value.
field_type: The field type.
field_name: The field name.
Returns:
The interpreted value.
Raises:
EnvironmentVarValueError: If the value is invalid.
"""
try:
return field_type(value)
except ValueError as ve:
msg = f"Invalid enum value: {value!r} for {field_name}"
raise EnvironmentVarValueError(msg) from ve
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
class SequenceOptions:
"""Options for interpreting Sequence environment variables."""
delimiter: str = ":"
strip: bool = False
DEFAULT_SEQUENCE_OPTIONS = SequenceOptions()
def interpret_env_var_value(
value: str, field_type: GenericType, field_name: str
) -> Any:
"""Interpret an environment variable value based on the field type.
Args:
value: The environment variable value.
field_type: The field type.
field_name: The field name.
Returns:
The interpreted value.
Raises:
ValueError: If the value is invalid.
EnvironmentVarValueError: If the value is invalid for the specific type.
"""
field_type = value_inside_optional(field_type)
# Unwrap Annotated to get the base type for env var interpretation.
# Preserve SequenceOptions and PathExistsFlag markers.
annotated_metadata: tuple[Any, ...] = ()
if get_origin(field_type) is Annotated:
annotated_args = get_args(field_type)
annotated_metadata = annotated_args[1:]
field_type = annotated_args[0]
if is_union(field_type):
errors = []
for arg in (union_types := get_args(field_type)):
try:
return interpret_env_var_value(value, arg, field_name)
except (ValueError, EnvironmentVarValueError) as e: # noqa: PERF203
errors.append(e)
msg = f"Could not interpret {value!r} for {field_name} as any of {union_types}: {errors}"
raise EnvironmentVarValueError(msg)
value = value.strip()
if field_type is bool:
return interpret_boolean_env(value, field_name)
if field_type is str:
return value
if field_type is LogLevel:
loglevel = LogLevel.from_string(value)
if loglevel is None:
msg = f"Invalid log level value: {value} for {field_name}"
raise EnvironmentVarValueError(msg)
return loglevel
if field_type is int:
return interpret_int_env(value, field_name)
if field_type is float:
return interpret_float_env(value, field_name)
if field_type is Path:
if PathExistsFlag in annotated_metadata:
return interpret_existing_path_env(value, field_name)
return interpret_path_env(value, field_name)
if field_type is ExistingPath:
return interpret_existing_path_env(value, field_name)
if field_type is Plugin:
return interpret_plugin_env(value, field_name)
if get_origin(field_type) is type:
type_args = get_args(field_type)
if (
type_args
and isinstance(type_args[0], type)
and issubclass(type_args[0], Plugin)
):
return interpret_plugin_class_env(value, field_name)
if get_origin(field_type) is Literal:
literal_values = get_args(field_type)
for literal_value in literal_values:
if isinstance(literal_value, str) and literal_value == value:
return literal_value
if isinstance(literal_value, bool):
try:
interpreted_bool = interpret_boolean_env(value, field_name)
if interpreted_bool == literal_value:
return interpreted_bool
except EnvironmentVarValueError:
continue
if isinstance(literal_value, int):
try:
interpreted_int = interpret_int_env(value, field_name)
if interpreted_int == literal_value:
return interpreted_int
except EnvironmentVarValueError:
continue
msg = f"Invalid literal value: {value!r} for {field_name}, expected one of {literal_values}"
raise EnvironmentVarValueError(msg)
# If the field was Annotated with SequenceOptions, extract the options
sequence_options = DEFAULT_SEQUENCE_OPTIONS
for arg in annotated_metadata:
if isinstance(arg, SequenceOptions):
sequence_options = arg
break
if get_origin(field_type) in (list, Sequence):
items = value.split(sequence_options.delimiter)
if sequence_options.strip:
items = [item.strip() for item in items]
return [
interpret_env_var_value(
v,
get_args(field_type)[0],
f"{field_name}[{i}]",
)
for i, v in enumerate(items)
]
if isinstance(field_type, type) and issubclass(field_type, enum.Enum):
return interpret_enum_env(value, field_type, field_name)
msg = f"Invalid type for environment variable {field_name}: {field_type}. This is probably an issue in Reflex."
raise ValueError(msg)
T = TypeVar("T")
class EnvVar(Generic[T]):
"""Environment variable."""
name: str
default: Any
type_: T
def __init__(self, name: str, default: Any, type_: T) -> None:
"""Initialize the environment variable.
Args:
name: The environment variable name.
default: The default value.
type_: The type of the value.
"""
self.name = name
self.default = default
self.type_ = type_
def interpret(self, value: str) -> T:
"""Interpret the environment variable value.
Args:
value: The environment variable value.
Returns:
The interpreted value.
"""
return interpret_env_var_value(value, self.type_, self.name)
def getenv(self) -> T | None:
"""Get the interpreted environment variable value.
Returns:
The environment variable value.
"""
env_value = os.getenv(self.name, None)
if env_value and env_value.strip():
return self.interpret(env_value)
return None
def is_set(self) -> bool:
"""Check if the environment variable is set.
Returns:
True if the environment variable is set.
"""
return bool(os.getenv(self.name, "").strip())
def get(self) -> T:
"""Get the interpreted environment variable value or the default value if not set.
Returns:
The interpreted value.
"""
env_value = self.getenv()
if env_value is not None:
return env_value
return self.default
def set(self, value: T | None) -> None:
"""Set the environment variable. None unsets the variable.
Args:
value: The value to set.
"""
if value is None:
_ = os.environ.pop(self.name, None)
else:
if isinstance(value, enum.Enum):
value = value.value
if isinstance(value, list):
str_value = ":".join(str(v) for v in value)
else:
str_value = str(value)
os.environ[self.name] = str_value
@lru_cache
def get_type_hints_environment(cls: type) -> dict[str, Any]:
"""Get the type hints for the environment variables.
Args:
cls: The class.
Returns:
The type hints.
"""
return get_type_hints(cls)
class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration]
"""Descriptor for environment variables."""
name: str
default: Any
internal: bool = False
def __init__(self, default: Any, internal: bool = False) -> None:
"""Initialize the descriptor.
Args:
default: The default value.
internal: Whether the environment variable is reflex internal.
"""
self.default = default
self.internal = internal
def __set_name__(self, owner: Any, name: str):
"""Set the name of the descriptor.
Args:
owner: The owner class.
name: The name of the descriptor.
"""
self.name = name
def __get__(
self, instance: EnvironmentVariables, owner: type[EnvironmentVariables]
):
"""Get the EnvVar instance.
Args:
instance: The instance.
owner: The owner class.
Returns:
The EnvVar instance.
"""
type_ = get_args(get_type_hints_environment(owner)[self.name])[0]
env_name = self.name
if self.internal:
env_name = f"__{env_name}"
return EnvVar(name=env_name, default=self.default, type_=type_)
if TYPE_CHECKING:
def env_var(default: Any, internal: bool = False) -> EnvVar:
"""Typing helper for the env_var descriptor.
Args:
default: The default value.
internal: Whether the environment variable is reflex internal.
Returns:
The EnvVar instance.
"""
return default
class PathExistsFlag:
"""Flag to indicate that a path must exist."""
ExistingPath = Annotated[Path, PathExistsFlag]
class PerformanceMode(enum.Enum):
"""Performance mode for the app."""
WARN = "warn"
RAISE = "raise"
OFF = "off"
class EnvironmentVariables:
"""Environment variables class to instantiate environment variables."""
# Indicate the current command that was invoked in the reflex CLI.
REFLEX_COMPILE_CONTEXT: EnvVar[constants.CompileContext] = env_var(
constants.CompileContext.UNDEFINED, internal=True
)
# Whether to use npm over bun to install and run the frontend.
REFLEX_USE_NPM: EnvVar[bool] = env_var(False)
# The npm registry to use.
NPM_CONFIG_REGISTRY: EnvVar[str | None] = env_var(None)
# Whether to use Granian for the backend. By default, the backend uses Uvicorn if available.
REFLEX_USE_GRANIAN: EnvVar[bool] = env_var(False)
# Whether to use the system installed bun. If set to false, bun will be bundled with the app.
REFLEX_USE_SYSTEM_BUN: EnvVar[bool] = env_var(False)
# The working directory for the frontend directory.
REFLEX_WEB_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.WEB))
# The working directory for the states directory.
REFLEX_STATES_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.STATES))
# Path to the alembic config file
ALEMBIC_CONFIG: EnvVar[ExistingPath] = env_var(Path(constants.ALEMBIC_CONFIG))
# Include schemas in alembic migrations.
ALEMBIC_INCLUDE_SCHEMAS: EnvVar[bool] = env_var(False)
# Disable SSL verification for HTTPX requests.
SSL_NO_VERIFY: EnvVar[bool] = env_var(False)
# The directory to store uploaded files.
REFLEX_UPLOADED_FILES_DIR: EnvVar[Path] = env_var(
Path(constants.Dirs.UPLOADED_FILES)
)
# The directory to store reflex dependencies.
REFLEX_DIR: EnvVar[Path] = env_var(constants.Reflex.DIR)
# Whether to print the SQL queries if the log level is INFO or lower.
SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False)
# Whether to check db connections before using them.
SQLALCHEMY_POOL_PRE_PING: EnvVar[bool] = env_var(True)
# The size of the database connection pool.
SQLALCHEMY_POOL_SIZE: EnvVar[int] = env_var(5)
# The maximum overflow size of the database connection pool.
SQLALCHEMY_MAX_OVERFLOW: EnvVar[int] = env_var(10)
# Recycle connections after this many seconds.
SQLALCHEMY_POOL_RECYCLE: EnvVar[int] = env_var(-1)
# The timeout for acquiring a connection from the pool.
SQLALCHEMY_POOL_TIMEOUT: EnvVar[int] = env_var(30)
# Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False)
# Whether to skip purging the web directory in dev mode.
REFLEX_PERSIST_WEB_DIR: EnvVar[bool] = env_var(False)
# This env var stores the execution mode of the app
REFLEX_ENV_MODE: EnvVar[constants.Env] = env_var(constants.Env.DEV)
# Whether to run the backend only. Exclusive with REFLEX_FRONTEND_ONLY.
REFLEX_BACKEND_ONLY: EnvVar[bool] = env_var(False)
# Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY.
REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False)
# The port to run the frontend on.
REFLEX_FRONTEND_PORT: EnvVar[int | None] = env_var(None)
# The port to run the backend on.
REFLEX_BACKEND_PORT: EnvVar[int | None] = env_var(None)
# If this env var is set to "yes", App.compile will be a no-op
REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True)
# Whether to run app harness tests in headless mode.
APP_HARNESS_HEADLESS: EnvVar[bool] = env_var(False)
# Which app harness driver to use.
APP_HARNESS_DRIVER: EnvVar[str] = env_var("Chrome")
# Arguments to pass to the app harness driver.
APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("")
# Whether to check for outdated package versions.
REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True)
# In which performance mode to run the app.
REFLEX_PERF_MODE: EnvVar[PerformanceMode] = env_var(PerformanceMode.WARN)
# The maximum size of the reflex state in kilobytes.
REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000)
# Whether to use the turbopack bundler.
REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(False)
# Additional paths to include in the hot reload. Separated by a colon.
REFLEX_HOT_RELOAD_INCLUDE_PATHS: EnvVar[list[Path]] = env_var([])
# Paths to exclude from the hot reload. Takes precedence over include paths. Separated by a colon.
REFLEX_HOT_RELOAD_EXCLUDE_PATHS: EnvVar[list[Path]] = env_var([])
# Enables different behavior for when the backend would do a cold start if it was inactive.
REFLEX_DOES_BACKEND_COLD_START: EnvVar[bool] = env_var(False)
# The timeout for the backend to do a cold start in seconds.
REFLEX_BACKEND_COLD_START_TIMEOUT: EnvVar[int] = env_var(10)
# Used by flexgen to enumerate the pages.
REFLEX_ADD_ALL_ROUTES_ENDPOINT: EnvVar[bool] = env_var(False)
# The address to bind the HTTP client to. You can set this to "::" to enable IPv6.
REFLEX_HTTP_CLIENT_BIND_ADDRESS: EnvVar[str | None] = env_var(None)
# Maximum size of the message in the websocket server in bytes.
REFLEX_SOCKET_MAX_HTTP_BUFFER_SIZE: EnvVar[int] = env_var(
constants.POLLING_MAX_HTTP_BUFFER_SIZE
)
# The interval to send a ping to the websocket server in seconds.
REFLEX_SOCKET_INTERVAL: EnvVar[int] = env_var(constants.Ping.INTERVAL)
# The timeout to wait for a pong from the websocket server in seconds.
REFLEX_SOCKET_TIMEOUT: EnvVar[int] = env_var(constants.Ping.TIMEOUT)
# Whether to run Granian in a spawn process. This enables Reflex to pick up on environment variable changes between hot reloads.
REFLEX_STRICT_HOT_RELOAD: EnvVar[bool] = env_var(False)
# The path to the reflex log file. If not set, the log file will be stored in the reflex user directory.
REFLEX_LOG_FILE: EnvVar[Path | None] = env_var(None)
# Enable full logging of debug messages to reflex user directory.
REFLEX_ENABLE_FULL_LOGGING: EnvVar[bool] = env_var(False)
# Whether to enable hot module replacement
VITE_HMR: EnvVar[bool] = env_var(True)
# Whether to force a full reload on changes.
VITE_FORCE_FULL_RELOAD: EnvVar[bool] = env_var(False)
# Whether to enable Rolldown's experimental HMR.
VITE_EXPERIMENTAL_HMR: EnvVar[bool] = env_var(False)
# Whether to generate sourcemaps for the frontend.
VITE_SOURCEMAP: EnvVar[Literal[False, True, "inline", "hidden"]] = env_var(False) # noqa: RUF038
# Whether to enable SSR for the frontend.
REFLEX_SSR: EnvVar[bool] = env_var(True)
# Whether to mount the compiled frontend app in the backend server in production.
REFLEX_MOUNT_FRONTEND_COMPILED_APP: EnvVar[bool] = env_var(False, internal=True)
# How long to delay writing updated states to disk. (Higher values mean less writes, but more chance of lost data.)
REFLEX_STATE_MANAGER_DISK_DEBOUNCE_SECONDS: EnvVar[float] = env_var(2.0)
# How long to wait between automatic reload on frontend error to avoid reload loops.
REFLEX_AUTO_RELOAD_COOLDOWN_TIME_MS: EnvVar[int] = env_var(10_000)
# Whether to enable debug logging for the redis state manager.
REFLEX_STATE_MANAGER_REDIS_DEBUG: EnvVar[bool] = env_var(False)
# Whether to opportunistically hold the redis lock to allow fast in-memory access while uncontended.
REFLEX_OPLOCK_ENABLED: EnvVar[bool] = env_var(False)
# How long to opportunistically hold the redis lock in milliseconds (must be less than the token expiration).
REFLEX_OPLOCK_HOLD_TIME_MS: EnvVar[int] = env_var(0)
environment = EnvironmentVariables()
try:
from dotenv import load_dotenv
except ImportError:
load_dotenv = None
def _paths_from_env_files(env_files: str) -> list[Path]:
"""Convert a string of paths separated by os.pathsep into a list of Path objects.
Args:
env_files: The string of paths.
Returns:
A list of Path objects.
"""
# load env files in reverse order
return list(
reversed([
Path(path)
for path_element in env_files.split(os.pathsep)
if (path := path_element.strip())
])
)
def _load_dotenv_from_files(files: list[Path]):
"""Load environment variables from a list of files.
Args:
files: A list of Path objects representing the environment variable files.
"""
from reflex_base.utils import console
if not files:
return
if load_dotenv is None:
console.error(
"""The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`."""
)
return
for env_file in files:
if env_file.exists():
load_dotenv(env_file, override=True)
def _paths_from_environment() -> list[Path]:
"""Get the paths from the REFLEX_ENV_FILE environment variable.
Returns:
A list of Path objects.
"""
env_files = os.environ.get("REFLEX_ENV_FILE")
if not env_files:
return []
return _paths_from_env_files(env_files)
def _load_dotenv_from_env():
"""Load environment variables from paths specified in REFLEX_ENV_FILE."""
_load_dotenv_from_files(_paths_from_environment())
# Load the env files at import time if they are set in the ENV_FILE environment variable.
_load_dotenv_from_env()