config.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. """The Reflex config."""
  2. from __future__ import annotations
  3. import dataclasses
  4. import importlib
  5. import os
  6. import sys
  7. import urllib.parse
  8. from pathlib import Path
  9. from typing import Any, Dict, List, Optional, Set, Union
  10. from typing_extensions import get_type_hints
  11. from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError
  12. from reflex.utils.types import value_inside_optional
  13. try:
  14. import pydantic.v1 as pydantic
  15. except ModuleNotFoundError:
  16. import pydantic
  17. from reflex_cli.constants.hosting import Hosting
  18. from reflex import constants
  19. from reflex.base import Base
  20. from reflex.utils import console
  21. class DBConfig(Base):
  22. """Database config."""
  23. engine: str
  24. username: Optional[str] = ""
  25. password: Optional[str] = ""
  26. host: Optional[str] = ""
  27. port: Optional[int] = None
  28. database: str
  29. @classmethod
  30. def postgresql(
  31. cls,
  32. database: str,
  33. username: str,
  34. password: str | None = None,
  35. host: str | None = None,
  36. port: int | None = 5432,
  37. ) -> DBConfig:
  38. """Create an instance with postgresql engine.
  39. Args:
  40. database: Database name.
  41. username: Database username.
  42. password: Database password.
  43. host: Database host.
  44. port: Database port.
  45. Returns:
  46. DBConfig instance.
  47. """
  48. return cls(
  49. engine="postgresql",
  50. username=username,
  51. password=password,
  52. host=host,
  53. port=port,
  54. database=database,
  55. )
  56. @classmethod
  57. def postgresql_psycopg2(
  58. cls,
  59. database: str,
  60. username: str,
  61. password: str | None = None,
  62. host: str | None = None,
  63. port: int | None = 5432,
  64. ) -> DBConfig:
  65. """Create an instance with postgresql+psycopg2 engine.
  66. Args:
  67. database: Database name.
  68. username: Database username.
  69. password: Database password.
  70. host: Database host.
  71. port: Database port.
  72. Returns:
  73. DBConfig instance.
  74. """
  75. return cls(
  76. engine="postgresql+psycopg2",
  77. username=username,
  78. password=password,
  79. host=host,
  80. port=port,
  81. database=database,
  82. )
  83. @classmethod
  84. def sqlite(
  85. cls,
  86. database: str,
  87. ) -> DBConfig:
  88. """Create an instance with sqlite engine.
  89. Args:
  90. database: Database name.
  91. Returns:
  92. DBConfig instance.
  93. """
  94. return cls(
  95. engine="sqlite",
  96. database=database,
  97. )
  98. def get_url(self) -> str:
  99. """Get database URL.
  100. Returns:
  101. The database URL.
  102. """
  103. host = (
  104. f"{self.host}:{self.port}" if self.host and self.port else self.host or ""
  105. )
  106. username = urllib.parse.quote_plus(self.username) if self.username else ""
  107. password = urllib.parse.quote_plus(self.password) if self.password else ""
  108. if username:
  109. path = f"{username}:{password}@{host}" if password else f"{username}@{host}"
  110. else:
  111. path = f"{host}"
  112. return f"{self.engine}://{path}/{self.database}"
  113. def get_default_value_for_field(field: dataclasses.Field) -> Any:
  114. """Get the default value for a field.
  115. Args:
  116. field: The field.
  117. Returns:
  118. The default value.
  119. Raises:
  120. ValueError: If no default value is found.
  121. """
  122. if field.default != dataclasses.MISSING:
  123. return field.default
  124. elif field.default_factory != dataclasses.MISSING:
  125. return field.default_factory()
  126. else:
  127. raise ValueError(
  128. f"Missing value for environment variable {field.name} and no default value found"
  129. )
  130. def interpret_boolean_env(value: str) -> bool:
  131. """Interpret a boolean environment variable value.
  132. Args:
  133. value: The environment variable value.
  134. Returns:
  135. The interpreted value.
  136. Raises:
  137. EnvironmentVarValueError: If the value is invalid.
  138. """
  139. true_values = ["true", "1", "yes", "y"]
  140. false_values = ["false", "0", "no", "n"]
  141. if value.lower() in true_values:
  142. return True
  143. elif value.lower() in false_values:
  144. return False
  145. raise EnvironmentVarValueError(f"Invalid boolean value: {value}")
  146. def interpret_int_env(value: str) -> int:
  147. """Interpret an integer environment variable value.
  148. Args:
  149. value: The environment variable value.
  150. Returns:
  151. The interpreted value.
  152. Raises:
  153. EnvironmentVarValueError: If the value is invalid.
  154. """
  155. try:
  156. return int(value)
  157. except ValueError as ve:
  158. raise EnvironmentVarValueError(f"Invalid integer value: {value}") from ve
  159. def interpret_path_env(value: str) -> Path:
  160. """Interpret a path environment variable value.
  161. Args:
  162. value: The environment variable value.
  163. Returns:
  164. The interpreted value.
  165. Raises:
  166. EnvironmentVarValueError: If the path does not exist.
  167. """
  168. path = Path(value)
  169. if not path.exists():
  170. raise EnvironmentVarValueError(f"Path does not exist: {path}")
  171. return path
  172. def interpret_env_var_value(value: str, field: dataclasses.Field) -> Any:
  173. """Interpret an environment variable value based on the field type.
  174. Args:
  175. value: The environment variable value.
  176. field: The field.
  177. Returns:
  178. The interpreted value.
  179. Raises:
  180. ValueError: If the value is invalid.
  181. """
  182. field_type = value_inside_optional(field.type)
  183. if field_type is bool:
  184. return interpret_boolean_env(value)
  185. elif field_type is str:
  186. return value
  187. elif field_type is int:
  188. return interpret_int_env(value)
  189. elif field_type is Path:
  190. return interpret_path_env(value)
  191. else:
  192. raise ValueError(
  193. f"Invalid type for environment variable {field.name}: {field_type}. This is probably an issue in Reflex."
  194. )
  195. @dataclasses.dataclass(init=False)
  196. class EnvironmentVariables:
  197. """Environment variables class to instantiate environment variables."""
  198. # Whether to use npm over bun to install frontend packages.
  199. REFLEX_USE_NPM: bool = False
  200. # The npm registry to use.
  201. NPM_CONFIG_REGISTRY: Optional[str] = None
  202. # Whether to use Granian for the backend. Otherwise, use Uvicorn.
  203. REFLEX_USE_GRANIAN: bool = False
  204. # The username to use for authentication on python package repository. Username and password must both be provided.
  205. TWINE_USERNAME: Optional[str] = None
  206. # The password to use for authentication on python package repository. Username and password must both be provided.
  207. TWINE_PASSWORD: Optional[str] = None
  208. # Whether to use the system installed bun. If set to false, bun will be bundled with the app.
  209. REFLEX_USE_SYSTEM_BUN: bool = False
  210. # Whether to use the system installed node and npm. If set to false, node and npm will be bundled with the app.
  211. REFLEX_USE_SYSTEM_NODE: bool = False
  212. # The working directory for the next.js commands.
  213. REFLEX_WEB_WORKDIR: Path = Path(constants.Dirs.WEB)
  214. # Path to the alembic config file
  215. ALEMBIC_CONFIG: Path = Path(constants.ALEMBIC_CONFIG)
  216. # Disable SSL verification for HTTPX requests.
  217. SSL_NO_VERIFY: bool = False
  218. # The directory to store uploaded files.
  219. REFLEX_UPLOADED_FILES_DIR: Path = Path(constants.Dirs.UPLOADED_FILES)
  220. # Whether to use seperate processes to compile the frontend and how many. If not set, defaults to thread executor.
  221. REFLEX_COMPILE_PROCESSES: Optional[int] = None
  222. # Whether to use seperate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`.
  223. REFLEX_COMPILE_THREADS: Optional[int] = None
  224. # The directory to store reflex dependencies.
  225. REFLEX_DIR: Path = Path(constants.Reflex.DIR)
  226. # Whether to print the SQL queries if the log level is INFO or lower.
  227. SQLALCHEMY_ECHO: bool = False
  228. # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
  229. REFLEX_IGNORE_REDIS_CONFIG_ERROR: bool = False
  230. # Whether to skip purging the web directory in dev mode.
  231. REFLEX_PERSIST_WEB_DIR: bool = False
  232. # The reflex.build frontend host.
  233. REFLEX_BUILD_FRONTEND: str = constants.Templates.REFLEX_BUILD_FRONTEND
  234. # The reflex.build backend host.
  235. REFLEX_BUILD_BACKEND: str = constants.Templates.REFLEX_BUILD_BACKEND
  236. def __init__(self):
  237. """Initialize the environment variables."""
  238. type_hints = get_type_hints(type(self))
  239. for field in dataclasses.fields(self):
  240. raw_value = os.getenv(field.name, None)
  241. field.type = type_hints.get(field.name) or field.type
  242. value = (
  243. interpret_env_var_value(raw_value, field)
  244. if raw_value is not None
  245. else get_default_value_for_field(field)
  246. )
  247. setattr(self, field.name, value)
  248. environment = EnvironmentVariables()
  249. class Config(Base):
  250. """The config defines runtime settings for the app.
  251. By default, the config is defined in an `rxconfig.py` file in the root of the app.
  252. ```python
  253. # rxconfig.py
  254. import reflex as rx
  255. config = rx.Config(
  256. app_name="myapp",
  257. api_url="http://localhost:8000",
  258. )
  259. ```
  260. Every config value can be overridden by an environment variable with the same name in uppercase.
  261. For example, `db_url` can be overridden by setting the `DB_URL` environment variable.
  262. See the [configuration](https://reflex.dev/docs/getting-started/configuration/) docs for more info.
  263. """
  264. class Config:
  265. """Pydantic config for the config."""
  266. validate_assignment = True
  267. # The name of the app (should match the name of the app directory).
  268. app_name: str
  269. # The log level to use.
  270. loglevel: constants.LogLevel = constants.LogLevel.DEFAULT
  271. # The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
  272. frontend_port: int = constants.DefaultPorts.FRONTEND_PORT
  273. # The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
  274. frontend_path: str = ""
  275. # The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
  276. backend_port: int = constants.DefaultPorts.BACKEND_PORT
  277. # The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production.
  278. api_url: str = f"http://localhost:{backend_port}"
  279. # The url the frontend will be hosted on.
  280. deploy_url: Optional[str] = f"http://localhost:{frontend_port}"
  281. # The url the backend will be hosted on.
  282. backend_host: str = "0.0.0.0"
  283. # The database url used by rx.Model.
  284. db_url: Optional[str] = "sqlite:///reflex.db"
  285. # The redis url
  286. redis_url: Optional[str] = None
  287. # Telemetry opt-in.
  288. telemetry_enabled: bool = True
  289. # The bun path
  290. bun_path: Union[str, Path] = constants.Bun.DEFAULT_PATH
  291. # List of origins that are allowed to connect to the backend API.
  292. cors_allowed_origins: List[str] = ["*"]
  293. # Tailwind config.
  294. tailwind: Optional[Dict[str, Any]] = {"plugins": ["@tailwindcss/typography"]}
  295. # Timeout when launching the gunicorn server. TODO(rename this to backend_timeout?)
  296. timeout: int = 120
  297. # Whether to enable or disable nextJS gzip compression.
  298. next_compression: bool = True
  299. # Whether to use React strict mode in nextJS
  300. react_strict_mode: bool = True
  301. # Additional frontend packages to install.
  302. frontend_packages: List[str] = []
  303. # The hosting service backend URL.
  304. cp_backend_url: str = Hosting.CP_BACKEND_URL
  305. # The hosting service frontend URL.
  306. cp_web_url: str = Hosting.CP_WEB_URL
  307. # The worker class used in production mode
  308. gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
  309. # Number of gunicorn workers from user
  310. gunicorn_workers: Optional[int] = None
  311. # Number of requests before a worker is restarted
  312. gunicorn_max_requests: int = 100
  313. # Variance limit for max requests; gunicorn only
  314. gunicorn_max_requests_jitter: int = 25
  315. # Indicate which type of state manager to use
  316. state_manager_mode: constants.StateManagerMode = constants.StateManagerMode.DISK
  317. # Maximum expiration lock time for redis state manager
  318. redis_lock_expiration: int = constants.Expiration.LOCK
  319. # Token expiration time for redis state manager
  320. redis_token_expiration: int = constants.Expiration.TOKEN
  321. # Attributes that were explicitly set by the user.
  322. _non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
  323. # Path to file containing key-values pairs to override in the environment; Dotenv format.
  324. env_file: Optional[str] = None
  325. def __init__(self, *args, **kwargs):
  326. """Initialize the config values.
  327. Args:
  328. *args: The args to pass to the Pydantic init method.
  329. **kwargs: The kwargs to pass to the Pydantic init method.
  330. Raises:
  331. ConfigError: If some values in the config are invalid.
  332. """
  333. super().__init__(*args, **kwargs)
  334. # Update the config from environment variables.
  335. env_kwargs = self.update_from_env()
  336. for key, env_value in env_kwargs.items():
  337. setattr(self, key, env_value)
  338. # Update default URLs if ports were set
  339. kwargs.update(env_kwargs)
  340. self._non_default_attributes.update(kwargs)
  341. self._replace_defaults(**kwargs)
  342. if (
  343. self.state_manager_mode == constants.StateManagerMode.REDIS
  344. and not self.redis_url
  345. ):
  346. raise ConfigError(
  347. "REDIS_URL is required when using the redis state manager."
  348. )
  349. @property
  350. def module(self) -> str:
  351. """Get the module name of the app.
  352. Returns:
  353. The module name.
  354. """
  355. return ".".join([self.app_name, self.app_name])
  356. def update_from_env(self) -> dict[str, Any]:
  357. """Update the config values based on set environment variables.
  358. If there is a set env_file, it is loaded first.
  359. Returns:
  360. The updated config values.
  361. Raises:
  362. EnvVarValueError: If an environment variable is set to an invalid type.
  363. """
  364. from reflex.utils.exceptions import EnvVarValueError
  365. if self.env_file:
  366. from dotenv import load_dotenv
  367. # load env file if exists
  368. load_dotenv(self.env_file, override=True)
  369. updated_values = {}
  370. # Iterate over the fields.
  371. for key, field in self.__fields__.items():
  372. # The env var name is the key in uppercase.
  373. env_var = os.environ.get(key.upper())
  374. # If the env var is set, override the config value.
  375. if env_var is not None:
  376. if key.upper() != "DB_URL":
  377. console.info(
  378. f"Overriding config value {key} with env var {key.upper()}={env_var}",
  379. dedupe=True,
  380. )
  381. # Convert the env var to the expected type.
  382. try:
  383. if issubclass(field.type_, bool):
  384. # special handling for bool values
  385. env_var = env_var.lower() in ["true", "1", "yes"]
  386. else:
  387. env_var = field.type_(env_var)
  388. except ValueError as ve:
  389. console.error(
  390. f"Could not convert {key.upper()}={env_var} to type {field.type_}"
  391. )
  392. raise EnvVarValueError from ve
  393. # Set the value.
  394. updated_values[key] = env_var
  395. return updated_values
  396. def get_event_namespace(self) -> str:
  397. """Get the path that the backend Websocket server lists on.
  398. Returns:
  399. The namespace for websocket.
  400. """
  401. event_url = constants.Endpoint.EVENT.get_url()
  402. return urllib.parse.urlsplit(event_url).path
  403. def _replace_defaults(self, **kwargs):
  404. """Replace formatted defaults when the caller provides updates.
  405. Args:
  406. **kwargs: The kwargs passed to the config or from the env.
  407. """
  408. if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
  409. self.api_url = f"http://localhost:{kwargs['backend_port']}"
  410. if (
  411. "deploy_url" not in self._non_default_attributes
  412. and "frontend_port" in kwargs
  413. ):
  414. self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
  415. if "api_url" not in self._non_default_attributes:
  416. # If running in Github Codespaces, override API_URL
  417. codespace_name = os.getenv("CODESPACE_NAME")
  418. GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
  419. "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
  420. )
  421. # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port
  422. replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN")
  423. backend_port = kwargs.get("backend_port", self.backend_port)
  424. if codespace_name and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN:
  425. self.api_url = (
  426. f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
  427. f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
  428. )
  429. elif replit_dev_domain and backend_port:
  430. self.api_url = f"https://{replit_dev_domain}:{backend_port}"
  431. def _set_persistent(self, **kwargs):
  432. """Set values in this config and in the environment so they persist into subprocess.
  433. Args:
  434. **kwargs: The kwargs passed to the config.
  435. """
  436. for key, value in kwargs.items():
  437. if value is not None:
  438. os.environ[key.upper()] = str(value)
  439. setattr(self, key, value)
  440. self._non_default_attributes.update(kwargs)
  441. self._replace_defaults(**kwargs)
  442. def get_config(reload: bool = False) -> Config:
  443. """Get the app config.
  444. Args:
  445. reload: Re-import the rxconfig module from disk
  446. Returns:
  447. The app config.
  448. """
  449. sys.path.insert(0, os.getcwd())
  450. # only import the module if it exists. If a module spec exists then
  451. # the module exists.
  452. spec = importlib.util.find_spec(constants.Config.MODULE) # type: ignore
  453. if not spec:
  454. # we need this condition to ensure that a ModuleNotFound error is not thrown when
  455. # running unit/integration tests.
  456. return Config(app_name="")
  457. rxconfig = importlib.import_module(constants.Config.MODULE)
  458. if reload:
  459. importlib.reload(rxconfig)
  460. return rxconfig.config