config.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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. 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. @staticmethod
  172. def check_deprecated_values(**kwargs):
  173. """Check for deprecated config values.
  174. Args:
  175. **kwargs: The kwargs passed to the config.
  176. Raises:
  177. ValueError: If a deprecated config value is found.
  178. """
  179. if "db_config" in kwargs:
  180. raise ValueError("db_config is deprecated - use db_url instead")
  181. if "admin_dash" in kwargs:
  182. raise ValueError(
  183. "admin_dash is deprecated in the config - pass it as a param to rx.App instead"
  184. )
  185. if "env_path" in kwargs:
  186. raise ValueError(
  187. "env_path is deprecated - use environment variables instead"
  188. )
  189. def update_from_env(self) -> dict[str, Any]:
  190. """Update the config from environment variables.
  191. Returns:
  192. The updated config values.
  193. Raises:
  194. ValueError: If an environment variable is set to an invalid type.
  195. """
  196. updated_values = {}
  197. # Iterate over the fields.
  198. for key, field in self.__fields__.items():
  199. # The env var name is the key in uppercase.
  200. env_var = os.environ.get(key.upper())
  201. # If the env var is set, override the config value.
  202. if env_var is not None:
  203. if key.upper() != "DB_URL":
  204. console.info(
  205. f"Overriding config value {key} with env var {key.upper()}={env_var}"
  206. )
  207. # Convert the env var to the expected type.
  208. try:
  209. if types._issubclass(field.annotation, bool):
  210. # special handling for bool values
  211. env_var = env_var.lower() in ["true", "1", "yes"]
  212. elif types.is_generic_alias(field.annotation):
  213. env_var = get_args(field.annotation)[0](env_var)
  214. else:
  215. env_var = field.annotation(env_var)
  216. except ValueError:
  217. console.error(
  218. f"Could not convert {key.upper()}={env_var} to type {field.annotation}"
  219. )
  220. raise
  221. # Set the value.
  222. updated_values[key] = env_var
  223. return updated_values
  224. def get_event_namespace(self) -> str | None:
  225. """Get the websocket event namespace.
  226. Returns:
  227. The namespace for websocket.
  228. """
  229. if self.event_namespace:
  230. console.deprecate(
  231. feature_name="Passing event_namespace in the config",
  232. reason="",
  233. deprecation_version="0.3.5",
  234. removal_version="0.4.0",
  235. )
  236. return f'/{self.event_namespace.strip("/")}'
  237. event_url = constants.Endpoint.EVENT.get_url()
  238. return urllib.parse.urlsplit(event_url).path
  239. def _replace_defaults(self, **kwargs):
  240. """Replace formatted defaults when the caller provides updates.
  241. Args:
  242. **kwargs: The kwargs passed to the config or from the env.
  243. """
  244. if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
  245. self.api_url = f"http://localhost:{kwargs['backend_port']}"
  246. if (
  247. "deploy_url" not in self._non_default_attributes
  248. and "frontend_port" in kwargs
  249. ):
  250. self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
  251. # If running in Github Codespaces, override API_URL
  252. codespace_name = os.getenv("CODESPACE_NAME")
  253. if "api_url" not in self._non_default_attributes and codespace_name:
  254. GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
  255. "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
  256. )
  257. if codespace_name:
  258. self.api_url = (
  259. f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
  260. f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
  261. )
  262. def _set_persistent(self, **kwargs):
  263. """Set values in this config and in the environment so they persist into subprocess.
  264. Args:
  265. **kwargs: The kwargs passed to the config.
  266. """
  267. for key, value in kwargs.items():
  268. if value is not None:
  269. os.environ[key.upper()] = str(value)
  270. setattr(self, key, value)
  271. self._non_default_attributes.update(kwargs)
  272. self._replace_defaults(**kwargs)
  273. def get_config(reload: bool = False) -> Config:
  274. """Get the app config.
  275. Args:
  276. reload: Re-import the rxconfig module from disk
  277. Returns:
  278. The app config.
  279. """
  280. sys.path.insert(0, os.getcwd())
  281. try:
  282. rxconfig = __import__(constants.Config.MODULE)
  283. if reload:
  284. importlib.reload(rxconfig)
  285. return rxconfig.config
  286. except ImportError:
  287. return Config(app_name="") # type: ignore