config.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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, get_args
  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, types
  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. # Pydantic config
  108. model_config = pydantic.ConfigDict(
  109. validate_assignment=True,
  110. )
  111. # The name of the app.
  112. app_name: str
  113. # The log level to use.
  114. loglevel: constants.LogLevel = constants.LogLevel.INFO
  115. # The port to run the frontend on.
  116. frontend_port: int = 3000
  117. # The path to run the frontend on.
  118. frontend_path: str = ""
  119. # The port to run the backend on.
  120. backend_port: int = 8000
  121. # The backend url the frontend will connect to.
  122. api_url: str = f"http://localhost:{backend_port}"
  123. # The url the frontend will be hosted on.
  124. deploy_url: Optional[str] = f"http://localhost:{frontend_port}"
  125. # The url the backend will be hosted on.
  126. backend_host: str = "0.0.0.0"
  127. # The database url.
  128. db_url: Optional[str] = "sqlite:///reflex.db"
  129. # The redis url.
  130. redis_url: Optional[str] = None
  131. # Telemetry opt-in.
  132. telemetry_enabled: bool = True
  133. # The bun path
  134. bun_path: str = constants.Bun.DEFAULT_PATH
  135. # List of origins that are allowed to connect to the backend API.
  136. cors_allowed_origins: List[str] = ["*"]
  137. # Tailwind config.
  138. tailwind: Optional[Dict[str, Any]] = {}
  139. # Timeout when launching the gunicorn server. TODO(rename this to backend_timeout?)
  140. timeout: int = 120
  141. # Whether to enable or disable nextJS gzip compression.
  142. next_compression: bool = True
  143. # The event namespace for ws connection
  144. event_namespace: Optional[str] = None
  145. # Additional frontend packages to install.
  146. frontend_packages: List[str] = []
  147. # The hosting service backend URL.
  148. cp_backend_url: str = Hosting.CP_BACKEND_URL
  149. # The hosting service frontend URL.
  150. cp_web_url: str = Hosting.CP_WEB_URL
  151. # The worker class used in production mode
  152. gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
  153. # Attributes that were explicitly set by the user.
  154. _non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
  155. def __init__(self, *args, **kwargs):
  156. """Initialize the config values.
  157. Args:
  158. *args: The args to pass to the Pydantic init method.
  159. **kwargs: The kwargs to pass to the Pydantic init method.
  160. """
  161. super().__init__(*args, **kwargs)
  162. # Check for deprecated values.
  163. self.check_deprecated_values(**kwargs)
  164. # Update the config from environment variables.
  165. env_kwargs = self.update_from_env()
  166. for key, env_value in env_kwargs.items():
  167. setattr(self, key, env_value)
  168. # Update default URLs if ports were set
  169. kwargs.update(env_kwargs)
  170. self._non_default_attributes.update(kwargs)
  171. self._replace_defaults(**kwargs)
  172. @staticmethod
  173. def check_deprecated_values(**kwargs):
  174. """Check for deprecated config values.
  175. Args:
  176. **kwargs: The kwargs passed to the config.
  177. Raises:
  178. ValueError: If a deprecated config value is found.
  179. """
  180. if "db_config" in kwargs:
  181. raise ValueError("db_config is deprecated - use db_url instead")
  182. if "admin_dash" in kwargs:
  183. raise ValueError(
  184. "admin_dash is deprecated in the config - pass it as a param to rx.App instead"
  185. )
  186. if "env_path" in kwargs:
  187. raise ValueError(
  188. "env_path is deprecated - use environment variables instead"
  189. )
  190. def update_from_env(self) -> dict[str, Any]:
  191. """Update the config from environment variables.
  192. Returns:
  193. The updated config values.
  194. Raises:
  195. ValueError: If an environment variable is set to an invalid type.
  196. """
  197. updated_values = {}
  198. # Iterate over the fields.
  199. for key, field in self.model_fields.items():
  200. # The env var name is the key in uppercase.
  201. env_var = os.environ.get(key.upper())
  202. # If the env var is set, override the config value.
  203. if env_var is not None:
  204. if key.upper() != "DB_URL":
  205. console.info(
  206. f"Overriding config value {key} with env var {key.upper()}={env_var}"
  207. )
  208. # Convert the env var to the expected type.
  209. try:
  210. if types._issubclass(field.annotation, bool):
  211. # special handling for bool values
  212. env_var = env_var.lower() in ["true", "1", "yes"]
  213. elif types.is_generic_alias(field.annotation):
  214. env_var = get_args(field.annotation)[0](env_var)
  215. elif field.annotation:
  216. env_var = field.annotation(env_var)
  217. else:
  218. raise ValueError(f"Invalid type {field.annotation}")
  219. except ValueError:
  220. console.error(
  221. f"Could not convert {key.upper()}={env_var} to type {field.annotation}"
  222. )
  223. raise
  224. # Set the value.
  225. updated_values[key] = env_var
  226. return updated_values
  227. def get_event_namespace(self) -> str | None:
  228. """Get the websocket event namespace.
  229. Returns:
  230. The namespace for websocket.
  231. """
  232. if self.event_namespace:
  233. console.deprecate(
  234. feature_name="Passing event_namespace in the config",
  235. reason="",
  236. deprecation_version="0.3.5",
  237. removal_version="0.5.0",
  238. )
  239. return f'/{self.event_namespace.strip("/")}'
  240. event_url = constants.Endpoint.EVENT.get_url()
  241. return urllib.parse.urlsplit(event_url).path
  242. def _replace_defaults(self, **kwargs):
  243. """Replace formatted defaults when the caller provides updates.
  244. Args:
  245. **kwargs: The kwargs passed to the config or from the env.
  246. """
  247. if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
  248. self.api_url = f"http://localhost:{kwargs['backend_port']}"
  249. if (
  250. "deploy_url" not in self._non_default_attributes
  251. and "frontend_port" in kwargs
  252. ):
  253. self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
  254. # If running in Github Codespaces, override API_URL
  255. codespace_name = os.getenv("CODESPACE_NAME")
  256. if "api_url" not in self._non_default_attributes and codespace_name:
  257. GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
  258. "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
  259. )
  260. if codespace_name:
  261. self.api_url = (
  262. f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
  263. f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
  264. )
  265. def _set_persistent(self, **kwargs):
  266. """Set values in this config and in the environment so they persist into subprocess.
  267. Args:
  268. **kwargs: The kwargs passed to the config.
  269. """
  270. for key, value in kwargs.items():
  271. if value is not None:
  272. os.environ[key.upper()] = str(value)
  273. setattr(self, key, value)
  274. self._non_default_attributes.update(kwargs)
  275. self._replace_defaults(**kwargs)
  276. def get_config(reload: bool = False) -> Config:
  277. """Get the app config.
  278. Args:
  279. reload: Re-import the rxconfig module from disk
  280. Returns:
  281. The app config.
  282. """
  283. sys.path.insert(0, os.getcwd())
  284. try:
  285. rxconfig = __import__(constants.Config.MODULE)
  286. if reload:
  287. importlib.reload(rxconfig)
  288. return rxconfig.config
  289. except ImportError:
  290. return Config(app_name="") # type: ignore