config.py 11 KB

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