config.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. """The Reflex config."""
  2. from __future__ import annotations
  3. import importlib
  4. import os
  5. import sys
  6. import urllib.parse
  7. from pathlib import Path
  8. from typing import Any, Dict, List, Optional, Set, Union
  9. from reflex.utils.exceptions import ConfigError
  10. try:
  11. import pydantic.v1 as pydantic
  12. except ModuleNotFoundError:
  13. import pydantic
  14. from reflex_cli.constants.hosting import Hosting
  15. from reflex import constants
  16. from reflex.base import Base
  17. from reflex.utils import console
  18. class DBConfig(Base):
  19. """Database config."""
  20. engine: str
  21. username: Optional[str] = ""
  22. password: Optional[str] = ""
  23. host: Optional[str] = ""
  24. port: Optional[int] = None
  25. database: str
  26. @classmethod
  27. def postgresql(
  28. cls,
  29. database: str,
  30. username: str,
  31. password: str | None = None,
  32. host: str | None = None,
  33. port: int | None = 5432,
  34. ) -> DBConfig:
  35. """Create an instance with postgresql engine.
  36. Args:
  37. database: Database name.
  38. username: Database username.
  39. password: Database password.
  40. host: Database host.
  41. port: Database port.
  42. Returns:
  43. DBConfig instance.
  44. """
  45. return cls(
  46. engine="postgresql",
  47. username=username,
  48. password=password,
  49. host=host,
  50. port=port,
  51. database=database,
  52. )
  53. @classmethod
  54. def postgresql_psycopg2(
  55. cls,
  56. database: str,
  57. username: str,
  58. password: str | None = None,
  59. host: str | None = None,
  60. port: int | None = 5432,
  61. ) -> DBConfig:
  62. """Create an instance with postgresql+psycopg2 engine.
  63. Args:
  64. database: Database name.
  65. username: Database username.
  66. password: Database password.
  67. host: Database host.
  68. port: Database port.
  69. Returns:
  70. DBConfig instance.
  71. """
  72. return cls(
  73. engine="postgresql+psycopg2",
  74. username=username,
  75. password=password,
  76. host=host,
  77. port=port,
  78. database=database,
  79. )
  80. @classmethod
  81. def sqlite(
  82. cls,
  83. database: str,
  84. ) -> DBConfig:
  85. """Create an instance with sqlite engine.
  86. Args:
  87. database: Database name.
  88. Returns:
  89. DBConfig instance.
  90. """
  91. return cls(
  92. engine="sqlite",
  93. database=database,
  94. )
  95. def get_url(self) -> str:
  96. """Get database URL.
  97. Returns:
  98. The database URL.
  99. """
  100. host = (
  101. f"{self.host}:{self.port}" if self.host and self.port else self.host or ""
  102. )
  103. username = urllib.parse.quote_plus(self.username) if self.username else ""
  104. password = urllib.parse.quote_plus(self.password) if self.password else ""
  105. if username:
  106. path = f"{username}:{password}@{host}" if password else f"{username}@{host}"
  107. else:
  108. path = f"{host}"
  109. return f"{self.engine}://{path}/{self.database}"
  110. class Config(Base):
  111. """The config defines runtime settings for the app.
  112. By default, the config is defined in an `rxconfig.py` file in the root of the app.
  113. ```python
  114. # rxconfig.py
  115. import reflex as rx
  116. config = rx.Config(
  117. app_name="myapp",
  118. api_url="http://localhost:8000",
  119. )
  120. ```
  121. Every config value can be overridden by an environment variable with the same name in uppercase.
  122. For example, `db_url` can be overridden by setting the `DB_URL` environment variable.
  123. See the [configuration](https://reflex.dev/docs/getting-started/configuration/) docs for more info.
  124. """
  125. class Config:
  126. """Pydantic config for the config."""
  127. validate_assignment = True
  128. # The name of the app (should match the name of the app directory).
  129. app_name: str
  130. # The log level to use.
  131. loglevel: constants.LogLevel = constants.LogLevel.DEFAULT
  132. # The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
  133. frontend_port: int = constants.DefaultPorts.FRONTEND_PORT
  134. # The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
  135. frontend_path: str = ""
  136. # The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
  137. backend_port: int = constants.DefaultPorts.BACKEND_PORT
  138. # The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production.
  139. api_url: str = f"http://localhost:{backend_port}"
  140. # The url the frontend will be hosted on.
  141. deploy_url: Optional[str] = f"http://localhost:{frontend_port}"
  142. # The url the backend will be hosted on.
  143. backend_host: str = "0.0.0.0"
  144. # The database url used by rx.Model.
  145. db_url: Optional[str] = "sqlite:///reflex.db"
  146. # The redis url
  147. redis_url: Optional[str] = None
  148. # Telemetry opt-in.
  149. telemetry_enabled: bool = True
  150. # The bun path
  151. bun_path: Union[str, Path] = constants.Bun.DEFAULT_PATH
  152. # List of origins that are allowed to connect to the backend API.
  153. cors_allowed_origins: List[str] = ["*"]
  154. # Tailwind config.
  155. tailwind: Optional[Dict[str, Any]] = {"plugins": ["@tailwindcss/typography"]}
  156. # Timeout when launching the gunicorn server. TODO(rename this to backend_timeout?)
  157. timeout: int = 120
  158. # Whether to enable or disable nextJS gzip compression.
  159. next_compression: bool = True
  160. # Whether to use React strict mode in nextJS
  161. react_strict_mode: bool = True
  162. # Additional frontend packages to install.
  163. frontend_packages: List[str] = []
  164. # The hosting service backend URL.
  165. cp_backend_url: str = Hosting.CP_BACKEND_URL
  166. # The hosting service frontend URL.
  167. cp_web_url: str = Hosting.CP_WEB_URL
  168. # The worker class used in production mode
  169. gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
  170. # Number of gunicorn workers from user
  171. gunicorn_workers: Optional[int] = None
  172. # Indicate which type of state manager to use
  173. state_manager_mode: constants.StateManagerMode = constants.StateManagerMode.DISK
  174. # Maximum expiration lock time for redis state manager
  175. redis_lock_expiration: int = constants.Expiration.LOCK
  176. # Token expiration time for redis state manager
  177. redis_token_expiration: int = constants.Expiration.TOKEN
  178. # Attributes that were explicitly set by the user.
  179. _non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
  180. def __init__(self, *args, **kwargs):
  181. """Initialize the config values.
  182. Args:
  183. *args: The args to pass to the Pydantic init method.
  184. **kwargs: The kwargs to pass to the Pydantic init method.
  185. Raises:
  186. ConfigError: If some values in the config are invalid.
  187. """
  188. super().__init__(*args, **kwargs)
  189. # Update the config from environment variables.
  190. env_kwargs = self.update_from_env()
  191. for key, env_value in env_kwargs.items():
  192. setattr(self, key, env_value)
  193. # Update default URLs if ports were set
  194. kwargs.update(env_kwargs)
  195. self._non_default_attributes.update(kwargs)
  196. self._replace_defaults(**kwargs)
  197. if (
  198. self.state_manager_mode == constants.StateManagerMode.REDIS
  199. and not self.redis_url
  200. ):
  201. raise ConfigError(
  202. "REDIS_URL is required when using the redis state manager."
  203. )
  204. @property
  205. def module(self) -> str:
  206. """Get the module name of the app.
  207. Returns:
  208. The module name.
  209. """
  210. return ".".join([self.app_name, self.app_name])
  211. def update_from_env(self) -> dict[str, Any]:
  212. """Update the config values based on set environment variables.
  213. Returns:
  214. The updated config values.
  215. Raises:
  216. EnvVarValueError: If an environment variable is set to an invalid type.
  217. """
  218. from reflex.utils.exceptions import EnvVarValueError
  219. updated_values = {}
  220. # Iterate over the fields.
  221. for key, field in self.__fields__.items():
  222. # The env var name is the key in uppercase.
  223. env_var = os.environ.get(key.upper())
  224. # If the env var is set, override the config value.
  225. if env_var is not None:
  226. if key.upper() != "DB_URL":
  227. console.info(
  228. f"Overriding config value {key} with env var {key.upper()}={env_var}",
  229. dedupe=True,
  230. )
  231. # Convert the env var to the expected type.
  232. try:
  233. if issubclass(field.type_, bool):
  234. # special handling for bool values
  235. env_var = env_var.lower() in ["true", "1", "yes"]
  236. else:
  237. env_var = field.type_(env_var)
  238. except ValueError as ve:
  239. console.error(
  240. f"Could not convert {key.upper()}={env_var} to type {field.type_}"
  241. )
  242. raise EnvVarValueError from ve
  243. # Set the value.
  244. updated_values[key] = env_var
  245. return updated_values
  246. def get_event_namespace(self) -> str:
  247. """Get the path that the backend Websocket server lists on.
  248. Returns:
  249. The namespace for websocket.
  250. """
  251. event_url = constants.Endpoint.EVENT.get_url()
  252. return urllib.parse.urlsplit(event_url).path
  253. def _replace_defaults(self, **kwargs):
  254. """Replace formatted defaults when the caller provides updates.
  255. Args:
  256. **kwargs: The kwargs passed to the config or from the env.
  257. """
  258. if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
  259. self.api_url = f"http://localhost:{kwargs['backend_port']}"
  260. if (
  261. "deploy_url" not in self._non_default_attributes
  262. and "frontend_port" in kwargs
  263. ):
  264. self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
  265. if "api_url" not in self._non_default_attributes:
  266. # If running in Github Codespaces, override API_URL
  267. codespace_name = os.getenv("CODESPACE_NAME")
  268. GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
  269. "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
  270. )
  271. # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port
  272. replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN")
  273. backend_port = kwargs.get("backend_port", self.backend_port)
  274. if codespace_name and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN:
  275. self.api_url = (
  276. f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
  277. f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
  278. )
  279. elif replit_dev_domain and backend_port:
  280. self.api_url = f"https://{replit_dev_domain}:{backend_port}"
  281. def _set_persistent(self, **kwargs):
  282. """Set values in this config and in the environment so they persist into subprocess.
  283. Args:
  284. **kwargs: The kwargs passed to the config.
  285. """
  286. for key, value in kwargs.items():
  287. if value is not None:
  288. os.environ[key.upper()] = str(value)
  289. setattr(self, key, value)
  290. self._non_default_attributes.update(kwargs)
  291. self._replace_defaults(**kwargs)
  292. def get_config(reload: bool = False) -> Config:
  293. """Get the app config.
  294. Args:
  295. reload: Re-import the rxconfig module from disk
  296. Returns:
  297. The app config.
  298. """
  299. sys.path.insert(0, os.getcwd())
  300. # only import the module if it exists. If a module spec exists then
  301. # the module exists.
  302. spec = importlib.util.find_spec(constants.Config.MODULE) # type: ignore
  303. if not spec:
  304. # we need this condition to ensure that a ModuleNotFound error is not thrown when
  305. # running unit/integration tests.
  306. return Config(app_name="")
  307. rxconfig = importlib.import_module(constants.Config.MODULE)
  308. if reload:
  309. importlib.reload(rxconfig)
  310. return rxconfig.config