361 lines
11 KiB
Python
361 lines
11 KiB
Python
"""The Reflex config."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import os
|
|
import sys
|
|
import urllib.parse
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set
|
|
|
|
try:
|
|
# TODO The type checking guard can be removed once
|
|
# reflex-hosting-cli tools are compatible with pydantic v2
|
|
|
|
if not TYPE_CHECKING:
|
|
import pydantic.v1 as pydantic
|
|
else:
|
|
raise ModuleNotFoundError
|
|
except ModuleNotFoundError:
|
|
import pydantic
|
|
|
|
from reflex_cli.constants.hosting import Hosting
|
|
|
|
from reflex import constants
|
|
from reflex.base import Base
|
|
from reflex.utils import console
|
|
|
|
|
|
class DBConfig(Base):
|
|
"""Database config."""
|
|
|
|
engine: str
|
|
username: Optional[str] = ""
|
|
password: Optional[str] = ""
|
|
host: Optional[str] = ""
|
|
port: Optional[int] = 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_psycopg2(
|
|
cls,
|
|
database: str,
|
|
username: str,
|
|
password: str | None = None,
|
|
host: str | None = None,
|
|
port: int | None = 5432,
|
|
) -> DBConfig:
|
|
"""Create an instance with postgresql+psycopg2 engine.
|
|
|
|
Args:
|
|
database: Database name.
|
|
username: Database username.
|
|
password: Database password.
|
|
host: Database host.
|
|
port: Database port.
|
|
|
|
Returns:
|
|
DBConfig instance.
|
|
"""
|
|
return cls(
|
|
engine="postgresql+psycopg2",
|
|
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}"
|
|
|
|
|
|
class Config(Base):
|
|
"""The config defines runtime settings for the app.
|
|
|
|
By default, the config is defined in an `rxconfig.py` file in the root of the app.
|
|
|
|
```python
|
|
# rxconfig.py
|
|
import reflex as rx
|
|
|
|
config = rx.Config(
|
|
app_name="myapp",
|
|
api_url="http://localhost:8000",
|
|
)
|
|
```
|
|
|
|
Every config value can be overridden by an environment variable with the same name in uppercase.
|
|
For example, `db_url` can be overridden by setting the `DB_URL` environment variable.
|
|
|
|
See the [configuration](https://reflex.dev/docs/getting-started/configuration/) docs for more info.
|
|
"""
|
|
|
|
class Config:
|
|
"""Pydantic config for the config."""
|
|
|
|
validate_assignment = True
|
|
|
|
# The name of the app (should match the name of the app directory).
|
|
app_name: str
|
|
|
|
# The log level to use.
|
|
loglevel: constants.LogLevel = constants.LogLevel.INFO
|
|
|
|
# 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_port: int = 3000
|
|
|
|
# The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
|
|
frontend_path: str = ""
|
|
|
|
# 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_port: int = 8000
|
|
|
|
# The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production.
|
|
api_url: str = f"http://localhost:{backend_port}"
|
|
|
|
# The url the frontend will be hosted on.
|
|
deploy_url: Optional[str] = f"http://localhost:{frontend_port}"
|
|
|
|
# The url the backend will be hosted on.
|
|
backend_host: str = "0.0.0.0"
|
|
|
|
# The database url used by rx.Model.
|
|
db_url: Optional[str] = "sqlite:///reflex.db"
|
|
|
|
# The redis url
|
|
redis_url: Optional[str] = None
|
|
|
|
# Telemetry opt-in.
|
|
telemetry_enabled: bool = True
|
|
|
|
# The bun path
|
|
bun_path: str = constants.Bun.DEFAULT_PATH
|
|
|
|
# List of origins that are allowed to connect to the backend API.
|
|
cors_allowed_origins: List[str] = ["*"]
|
|
|
|
# Tailwind config.
|
|
tailwind: Optional[Dict[str, Any]] = {}
|
|
|
|
# Timeout when launching the gunicorn server. TODO(rename this to backend_timeout?)
|
|
timeout: int = 120
|
|
|
|
# Whether to enable or disable nextJS gzip compression.
|
|
next_compression: bool = True
|
|
|
|
# Additional frontend packages to install.
|
|
frontend_packages: List[str] = []
|
|
|
|
# The hosting service backend URL.
|
|
cp_backend_url: str = Hosting.CP_BACKEND_URL
|
|
# The hosting service frontend URL.
|
|
cp_web_url: str = Hosting.CP_WEB_URL
|
|
|
|
# The worker class used in production mode
|
|
gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
|
|
|
|
# Attributes that were explicitly set by the user.
|
|
_non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initialize the config values.
|
|
|
|
Args:
|
|
*args: The args to pass to the Pydantic init method.
|
|
**kwargs: The kwargs to pass to the Pydantic init method.
|
|
"""
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# 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)
|
|
|
|
# Update default URLs if ports were set
|
|
kwargs.update(env_kwargs)
|
|
self._non_default_attributes.update(kwargs)
|
|
self._replace_defaults(**kwargs)
|
|
|
|
@property
|
|
def module(self) -> str:
|
|
"""Get the module name of the app.
|
|
|
|
Returns:
|
|
The module name.
|
|
"""
|
|
return ".".join([self.app_name, self.app_name])
|
|
|
|
def update_from_env(self) -> dict[str, Any]:
|
|
"""Update the config values based on set environment variables.
|
|
|
|
Returns:
|
|
The updated config values.
|
|
|
|
Raises:
|
|
ValueError: If an environment variable is set to an invalid type.
|
|
"""
|
|
updated_values = {}
|
|
# Iterate over the fields.
|
|
for key, field in self.__fields__.items():
|
|
# The env var name is the key in uppercase.
|
|
env_var = os.environ.get(key.upper())
|
|
|
|
# If the env var is set, override the config value.
|
|
if env_var is not None:
|
|
if key.upper() != "DB_URL":
|
|
console.info(
|
|
f"Overriding config value {key} with env var {key.upper()}={env_var}"
|
|
)
|
|
|
|
# Convert the env var to the expected type.
|
|
try:
|
|
if issubclass(field.type_, bool):
|
|
# special handling for bool values
|
|
env_var = env_var.lower() in ["true", "1", "yes"]
|
|
else:
|
|
env_var = field.type_(env_var)
|
|
except ValueError:
|
|
console.error(
|
|
f"Could not convert {key.upper()}={env_var} to type {field.type_}"
|
|
)
|
|
raise
|
|
|
|
# Set the value.
|
|
updated_values[key] = env_var
|
|
|
|
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 running in Github Codespaces, override API_URL
|
|
codespace_name = os.getenv("CODESPACE_NAME")
|
|
if "api_url" not in self._non_default_attributes and codespace_name:
|
|
GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
|
|
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
|
|
)
|
|
if codespace_name:
|
|
self.api_url = (
|
|
f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
|
|
f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
|
|
)
|
|
|
|
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[key.upper()] = str(value)
|
|
setattr(self, key, value)
|
|
self._non_default_attributes.update(kwargs)
|
|
self._replace_defaults(**kwargs)
|
|
|
|
|
|
def get_config(reload: bool = False) -> Config:
|
|
"""Get the app config.
|
|
|
|
Args:
|
|
reload: Re-import the rxconfig module from disk
|
|
|
|
Returns:
|
|
The app config.
|
|
"""
|
|
sys.path.insert(0, os.getcwd())
|
|
try:
|
|
rxconfig = __import__(constants.Config.MODULE)
|
|
if reload:
|
|
importlib.reload(rxconfig)
|
|
return rxconfig.config
|
|
|
|
except ImportError:
|
|
return Config(app_name="") # type: ignore
|