config.py 12 KB

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