config.py 12 KB

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