config.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. """The Reflex config."""
  2. from __future__ import annotations
  3. import importlib
  4. import os
  5. import sys
  6. import threading
  7. import urllib.parse
  8. from importlib.util import find_spec
  9. from pathlib import Path
  10. from types import ModuleType
  11. from typing import Any, ClassVar
  12. import pydantic.v1 as pydantic
  13. from reflex import constants
  14. from reflex.base import Base
  15. from reflex.constants.base import LogLevel
  16. from reflex.environment import EnvironmentVariables as EnvironmentVariables
  17. from reflex.environment import EnvVar as EnvVar
  18. from reflex.environment import ExistingPath, interpret_env_var_value
  19. from reflex.environment import env_var as env_var
  20. from reflex.environment import environment as environment
  21. from reflex.plugins import Plugin, TailwindV3Plugin, TailwindV4Plugin
  22. from reflex.utils import console
  23. from reflex.utils.exceptions import ConfigError
  24. from reflex.utils.types import true_type_for_pydantic_field
  25. try:
  26. from dotenv import load_dotenv
  27. except ImportError:
  28. load_dotenv = None
  29. def _load_dotenv_from_str(env_files: str) -> None:
  30. if not env_files:
  31. return
  32. if load_dotenv is None:
  33. console.error(
  34. """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`."""
  35. )
  36. return
  37. # load env files in reverse order if they exist
  38. for env_file_path in [
  39. Path(p) for s in reversed(env_files.split(os.pathsep)) if (p := s.strip())
  40. ]:
  41. if env_file_path.exists():
  42. load_dotenv(env_file_path, override=True)
  43. def _load_dotenv_from_env():
  44. """Load environment variables from paths specified in REFLEX_ENV_FILE."""
  45. show_deprecation = False
  46. env_env_file = os.environ.get("REFLEX_ENV_FILE")
  47. if not env_env_file:
  48. env_env_file = os.environ.get("ENV_FILE")
  49. if env_env_file:
  50. show_deprecation = True
  51. if show_deprecation:
  52. console.deprecate(
  53. "Usage of deprecated ENV_FILE env var detected.",
  54. reason="Prefer `REFLEX_` prefix when setting env vars.",
  55. deprecation_version="0.7.13",
  56. removal_version="0.8.0",
  57. )
  58. if env_env_file:
  59. _load_dotenv_from_str(env_env_file)
  60. # Load the env files at import time if they are set in the ENV_FILE environment variable.
  61. _load_dotenv_from_env()
  62. class DBConfig(Base):
  63. """Database config."""
  64. engine: str
  65. username: str | None = ""
  66. password: str | None = ""
  67. host: str | None = ""
  68. port: int | None = None
  69. database: str
  70. @classmethod
  71. def postgresql(
  72. cls,
  73. database: str,
  74. username: str,
  75. password: str | None = None,
  76. host: str | None = None,
  77. port: int | None = 5432,
  78. ) -> DBConfig:
  79. """Create an instance with postgresql engine.
  80. Args:
  81. database: Database name.
  82. username: Database username.
  83. password: Database password.
  84. host: Database host.
  85. port: Database port.
  86. Returns:
  87. DBConfig instance.
  88. """
  89. return cls(
  90. engine="postgresql",
  91. username=username,
  92. password=password,
  93. host=host,
  94. port=port,
  95. database=database,
  96. )
  97. @classmethod
  98. def postgresql_psycopg(
  99. cls,
  100. database: str,
  101. username: str,
  102. password: str | None = None,
  103. host: str | None = None,
  104. port: int | None = 5432,
  105. ) -> DBConfig:
  106. """Create an instance with postgresql+psycopg engine.
  107. Args:
  108. database: Database name.
  109. username: Database username.
  110. password: Database password.
  111. host: Database host.
  112. port: Database port.
  113. Returns:
  114. DBConfig instance.
  115. """
  116. return cls(
  117. engine="postgresql+psycopg",
  118. username=username,
  119. password=password,
  120. host=host,
  121. port=port,
  122. database=database,
  123. )
  124. @classmethod
  125. def sqlite(
  126. cls,
  127. database: str,
  128. ) -> DBConfig:
  129. """Create an instance with sqlite engine.
  130. Args:
  131. database: Database name.
  132. Returns:
  133. DBConfig instance.
  134. """
  135. return cls(
  136. engine="sqlite",
  137. database=database,
  138. )
  139. def get_url(self) -> str:
  140. """Get database URL.
  141. Returns:
  142. The database URL.
  143. """
  144. host = (
  145. f"{self.host}:{self.port}" if self.host and self.port else self.host or ""
  146. )
  147. username = urllib.parse.quote_plus(self.username) if self.username else ""
  148. password = urllib.parse.quote_plus(self.password) if self.password else ""
  149. if username:
  150. path = f"{username}:{password}@{host}" if password else f"{username}@{host}"
  151. else:
  152. path = f"{host}"
  153. return f"{self.engine}://{path}/{self.database}"
  154. # These vars are not logged because they may contain sensitive information.
  155. _sensitive_env_vars = {"DB_URL", "ASYNC_DB_URL", "REDIS_URL"}
  156. class Config(Base):
  157. """The config defines runtime settings for the app.
  158. By default, the config is defined in an `rxconfig.py` file in the root of the app.
  159. ```python
  160. # rxconfig.py
  161. import reflex as rx
  162. config = rx.Config(
  163. app_name="myapp",
  164. api_url="http://localhost:8000",
  165. )
  166. ```
  167. Every config value can be overridden by an environment variable with the same name in uppercase.
  168. For example, `db_url` can be overridden by setting the `DB_URL` environment variable.
  169. See the [configuration](https://reflex.dev/docs/getting-started/configuration/) docs for more info.
  170. """
  171. class Config: # pyright: ignore [reportIncompatibleVariableOverride]
  172. """Pydantic config for the config."""
  173. validate_assignment = True
  174. use_enum_values = False
  175. # The name of the app (should match the name of the app directory).
  176. app_name: str
  177. # The path to the app module.
  178. app_module_import: str | None = None
  179. # The log level to use.
  180. loglevel: constants.LogLevel = constants.LogLevel.DEFAULT
  181. # The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
  182. frontend_port: int | None = None
  183. # The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
  184. frontend_path: str = ""
  185. # The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
  186. backend_port: int | None = None
  187. # The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production.
  188. api_url: str = f"http://localhost:{constants.DefaultPorts.BACKEND_PORT}"
  189. # The url the frontend will be hosted on.
  190. deploy_url: str | None = f"http://localhost:{constants.DefaultPorts.FRONTEND_PORT}"
  191. # The url the backend will be hosted on.
  192. backend_host: str = "0.0.0.0"
  193. # The database url used by rx.Model.
  194. db_url: str | None = "sqlite:///reflex.db"
  195. # The async database url used by rx.Model.
  196. async_db_url: str | None = None
  197. # The redis url
  198. redis_url: str | None = None
  199. # Telemetry opt-in.
  200. telemetry_enabled: bool = True
  201. # The bun path
  202. bun_path: ExistingPath = constants.Bun.DEFAULT_PATH
  203. # Timeout to do a production build of a frontend page.
  204. static_page_generation_timeout: int = 60
  205. # List of origins that are allowed to connect to the backend API.
  206. cors_allowed_origins: list[str] = ["*"]
  207. # Tailwind config.
  208. tailwind: dict[str, Any] | None = {"plugins": ["@tailwindcss/typography"]}
  209. # DEPRECATED. Timeout when launching the gunicorn server.
  210. timeout: int | None = None
  211. # Whether to enable or disable nextJS gzip compression.
  212. next_compression: bool = True
  213. # Whether to enable or disable NextJS dev indicator.
  214. next_dev_indicators: bool = False
  215. # Whether to use React strict mode in nextJS
  216. react_strict_mode: bool = True
  217. # Additional frontend packages to install.
  218. frontend_packages: list[str] = []
  219. # DEPRECATED. The worker class used in production mode
  220. gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
  221. # DEPRECATED. Number of gunicorn workers from user
  222. gunicorn_workers: int | None = None
  223. # DEPRECATED. Number of requests before a worker is restarted; set to 0 to disable
  224. gunicorn_max_requests: int | None = None
  225. # DEPRECATED. Variance limit for max requests; gunicorn only
  226. gunicorn_max_requests_jitter: int | None = None
  227. # Indicate which type of state manager to use
  228. state_manager_mode: constants.StateManagerMode = constants.StateManagerMode.DISK
  229. # Maximum expiration lock time for redis state manager
  230. redis_lock_expiration: int = constants.Expiration.LOCK
  231. # Maximum lock time before warning for redis state manager.
  232. redis_lock_warning_threshold: int = constants.Expiration.LOCK_WARNING_THRESHOLD
  233. # Token expiration time for redis state manager
  234. redis_token_expiration: int = constants.Expiration.TOKEN
  235. # Attributes that were explicitly set by the user.
  236. _non_default_attributes: set[str] = pydantic.PrivateAttr(set())
  237. # Path to file containing key-values pairs to override in the environment; Dotenv format.
  238. env_file: str | None = None
  239. # Whether to automatically create setters for state base vars
  240. state_auto_setters: bool = True
  241. # Whether to display the sticky "Built with Reflex" badge on all pages.
  242. show_built_with_reflex: bool | None = None
  243. # Whether the app is running in the reflex cloud environment.
  244. is_reflex_cloud: bool = False
  245. # 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".
  246. extra_overlay_function: str | None = None
  247. # List of plugins to use in the app.
  248. plugins: list[Plugin] = []
  249. _prefixes: ClassVar[list[str]] = ["REFLEX_"]
  250. def __init__(self, *args, **kwargs):
  251. """Initialize the config values.
  252. Args:
  253. *args: The args to pass to the Pydantic init method.
  254. **kwargs: The kwargs to pass to the Pydantic init method.
  255. Raises:
  256. ConfigError: If some values in the config are invalid.
  257. """
  258. super().__init__(*args, **kwargs)
  259. # Clean up this code when we remove plain envvar in 0.8.0
  260. show_deprecation = False
  261. env_loglevel = os.environ.get("REFLEX_LOGLEVEL")
  262. if not env_loglevel:
  263. env_loglevel = os.environ.get("LOGLEVEL")
  264. if env_loglevel:
  265. show_deprecation = True
  266. if env_loglevel is not None:
  267. env_loglevel = LogLevel(env_loglevel.lower())
  268. if env_loglevel or self.loglevel != LogLevel.DEFAULT:
  269. console.set_log_level(env_loglevel or self.loglevel)
  270. if show_deprecation:
  271. console.deprecate(
  272. "Usage of deprecated LOGLEVEL env var detected.",
  273. reason="Prefer `REFLEX_` prefix when setting env vars.",
  274. deprecation_version="0.7.13",
  275. removal_version="0.8.0",
  276. )
  277. # Update the config from environment variables.
  278. env_kwargs = self.update_from_env()
  279. for key, env_value in env_kwargs.items():
  280. setattr(self, key, env_value)
  281. # Update default URLs if ports were set
  282. kwargs.update(env_kwargs)
  283. self._non_default_attributes.update(kwargs)
  284. self._replace_defaults(**kwargs)
  285. if self.tailwind is not None and not any(
  286. isinstance(plugin, (TailwindV3Plugin, TailwindV4Plugin))
  287. for plugin in self.plugins
  288. ):
  289. console.deprecate(
  290. "Inferring tailwind usage",
  291. reason="""
  292. If you are using tailwind, add `rx.plugins.TailwindV3Plugin()` to the `plugins=[]` in rxconfig.py.
  293. If you are not using tailwind, set `tailwind` to `None` in rxconfig.py.""",
  294. deprecation_version="0.7.13",
  295. removal_version="0.8.0",
  296. dedupe=True,
  297. )
  298. self.plugins.append(TailwindV3Plugin())
  299. if (
  300. self.state_manager_mode == constants.StateManagerMode.REDIS
  301. and not self.redis_url
  302. ):
  303. msg = f"{self._prefixes[0]}REDIS_URL is required when using the redis state manager."
  304. raise ConfigError(msg)
  305. @property
  306. def app_module(self) -> ModuleType | None:
  307. """Return the app module if `app_module_import` is set.
  308. Returns:
  309. The app module.
  310. """
  311. return (
  312. importlib.import_module(self.app_module_import)
  313. if self.app_module_import
  314. else None
  315. )
  316. @property
  317. def module(self) -> str:
  318. """Get the module name of the app.
  319. Returns:
  320. The module name.
  321. """
  322. if self.app_module_import is not None:
  323. return self.app_module_import
  324. return self.app_name + "." + self.app_name
  325. def update_from_env(self) -> dict[str, Any]:
  326. """Update the config values based on set environment variables.
  327. If there is a set env_file, it is loaded first.
  328. Returns:
  329. The updated config values.
  330. """
  331. if self.env_file:
  332. _load_dotenv_from_str(self.env_file)
  333. updated_values = {}
  334. # Iterate over the fields.
  335. for key, field in self.__fields__.items():
  336. # The env var name is the key in uppercase.
  337. for prefix in self._prefixes:
  338. if environment_variable := os.environ.get(f"{prefix}{key.upper()}"):
  339. break
  340. else:
  341. # Default to non-prefixed env var if other are not found.
  342. if environment_variable := os.environ.get(key.upper()):
  343. console.deprecate(
  344. f"Usage of deprecated {key.upper()} env var detected.",
  345. reason=f"Prefer `{self._prefixes[0]}` prefix when setting env vars.",
  346. deprecation_version="0.7.13",
  347. removal_version="0.8.0",
  348. )
  349. # If the env var is set, override the config value.
  350. if environment_variable and environment_variable.strip():
  351. # Interpret the value.
  352. value = interpret_env_var_value(
  353. environment_variable,
  354. true_type_for_pydantic_field(field),
  355. field.name,
  356. )
  357. # Set the value.
  358. updated_values[key] = value
  359. if key.upper() in _sensitive_env_vars:
  360. environment_variable = "***"
  361. if value != getattr(self, key):
  362. console.debug(
  363. f"Overriding config value {key} with env var {key.upper()}={environment_variable}",
  364. dedupe=True,
  365. )
  366. return updated_values
  367. def get_event_namespace(self) -> str:
  368. """Get the path that the backend Websocket server lists on.
  369. Returns:
  370. The namespace for websocket.
  371. """
  372. event_url = constants.Endpoint.EVENT.get_url()
  373. return urllib.parse.urlsplit(event_url).path
  374. def _replace_defaults(self, **kwargs):
  375. """Replace formatted defaults when the caller provides updates.
  376. Args:
  377. **kwargs: The kwargs passed to the config or from the env.
  378. """
  379. if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
  380. self.api_url = f"http://localhost:{kwargs['backend_port']}"
  381. if (
  382. "deploy_url" not in self._non_default_attributes
  383. and "frontend_port" in kwargs
  384. ):
  385. self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
  386. if "api_url" not in self._non_default_attributes:
  387. # If running in Github Codespaces, override API_URL
  388. codespace_name = os.getenv("CODESPACE_NAME")
  389. github_codespaces_port_forwarding_domain = os.getenv(
  390. "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
  391. )
  392. # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port
  393. replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN")
  394. backend_port = kwargs.get("backend_port", self.backend_port)
  395. if codespace_name and github_codespaces_port_forwarding_domain:
  396. self.api_url = (
  397. f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
  398. f".{github_codespaces_port_forwarding_domain}"
  399. )
  400. elif replit_dev_domain and backend_port:
  401. self.api_url = f"https://{replit_dev_domain}:{backend_port}"
  402. def _set_persistent(self, **kwargs):
  403. """Set values in this config and in the environment so they persist into subprocess.
  404. Args:
  405. **kwargs: The kwargs passed to the config.
  406. """
  407. for key, value in kwargs.items():
  408. if value is not None:
  409. os.environ[self._prefixes[0] + key.upper()] = str(value)
  410. setattr(self, key, value)
  411. self._non_default_attributes.update(kwargs)
  412. self._replace_defaults(**kwargs)
  413. def _get_config() -> Config:
  414. """Get the app config.
  415. Returns:
  416. The app config.
  417. """
  418. # only import the module if it exists. If a module spec exists then
  419. # the module exists.
  420. spec = find_spec(constants.Config.MODULE)
  421. if not spec:
  422. # we need this condition to ensure that a ModuleNotFound error is not thrown when
  423. # running unit/integration tests or during `reflex init`.
  424. return Config(app_name="")
  425. rxconfig = importlib.import_module(constants.Config.MODULE)
  426. return rxconfig.config
  427. # Protect sys.path from concurrent modification
  428. _config_lock = threading.RLock()
  429. def get_config(reload: bool = False) -> Config:
  430. """Get the app config.
  431. Args:
  432. reload: Re-import the rxconfig module from disk
  433. Returns:
  434. The app config.
  435. """
  436. cached_rxconfig = sys.modules.get(constants.Config.MODULE, None)
  437. if cached_rxconfig is not None:
  438. if reload:
  439. # Remove any cached module when `reload` is requested.
  440. del sys.modules[constants.Config.MODULE]
  441. else:
  442. return cached_rxconfig.config
  443. with _config_lock:
  444. orig_sys_path = sys.path.copy()
  445. sys.path.clear()
  446. sys.path.append(str(Path.cwd()))
  447. try:
  448. # Try to import the module with only the current directory in the path.
  449. return _get_config()
  450. except Exception:
  451. # If the module import fails, try to import with the original sys.path.
  452. sys.path.extend(orig_sys_path)
  453. return _get_config()
  454. finally:
  455. # Find any entries added to sys.path by rxconfig.py itself.
  456. extra_paths = [
  457. p for p in sys.path if p not in orig_sys_path and p != str(Path.cwd())
  458. ]
  459. # Restore the original sys.path.
  460. sys.path.clear()
  461. sys.path.extend(extra_paths)
  462. sys.path.extend(orig_sys_path)