config.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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 = 3000
  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 = 8000
  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. # Additional frontend packages to install.
  159. frontend_packages: List[str] = []
  160. # The hosting service backend URL.
  161. cp_backend_url: str = Hosting.CP_BACKEND_URL
  162. # The hosting service frontend URL.
  163. cp_web_url: str = Hosting.CP_WEB_URL
  164. # The worker class used in production mode
  165. gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
  166. # Number of gunicorn workers from user
  167. gunicorn_workers: Optional[int] = None
  168. # Attributes that were explicitly set by the user.
  169. _non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
  170. def __init__(self, *args, **kwargs):
  171. """Initialize the config values.
  172. Args:
  173. *args: The args to pass to the Pydantic init method.
  174. **kwargs: The kwargs to pass to the Pydantic init method.
  175. """
  176. super().__init__(*args, **kwargs)
  177. # Update the config from environment variables.
  178. env_kwargs = self.update_from_env()
  179. for key, env_value in env_kwargs.items():
  180. setattr(self, key, env_value)
  181. # Update default URLs if ports were set
  182. kwargs.update(env_kwargs)
  183. self._non_default_attributes.update(kwargs)
  184. self._replace_defaults(**kwargs)
  185. @property
  186. def module(self) -> str:
  187. """Get the module name of the app.
  188. Returns:
  189. The module name.
  190. """
  191. return ".".join([self.app_name, self.app_name])
  192. def update_from_env(self) -> dict[str, Any]:
  193. """Update the config values based on set environment variables.
  194. Returns:
  195. The updated config values.
  196. Raises:
  197. EnvVarValueError: If an environment variable is set to an invalid type.
  198. """
  199. from reflex.utils.exceptions import EnvVarValueError
  200. updated_values = {}
  201. # Iterate over the fields.
  202. for key, field in self.__fields__.items():
  203. # The env var name is the key in uppercase.
  204. env_var = os.environ.get(key.upper())
  205. # If the env var is set, override the config value.
  206. if env_var is not None:
  207. if key.upper() != "DB_URL":
  208. console.info(
  209. f"Overriding config value {key} with env var {key.upper()}={env_var}"
  210. )
  211. # Convert the env var to the expected type.
  212. try:
  213. if issubclass(field.type_, bool):
  214. # special handling for bool values
  215. env_var = env_var.lower() in ["true", "1", "yes"]
  216. else:
  217. env_var = field.type_(env_var)
  218. except ValueError as ve:
  219. console.error(
  220. f"Could not convert {key.upper()}={env_var} to type {field.type_}"
  221. )
  222. raise EnvVarValueError from ve
  223. # Set the value.
  224. updated_values[key] = env_var
  225. return updated_values
  226. def get_event_namespace(self) -> str:
  227. """Get the path that the backend Websocket server lists on.
  228. Returns:
  229. The namespace for websocket.
  230. """
  231. event_url = constants.Endpoint.EVENT.get_url()
  232. return urllib.parse.urlsplit(event_url).path
  233. def _replace_defaults(self, **kwargs):
  234. """Replace formatted defaults when the caller provides updates.
  235. Args:
  236. **kwargs: The kwargs passed to the config or from the env.
  237. """
  238. if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
  239. self.api_url = f"http://localhost:{kwargs['backend_port']}"
  240. if (
  241. "deploy_url" not in self._non_default_attributes
  242. and "frontend_port" in kwargs
  243. ):
  244. self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
  245. # If running in Github Codespaces, override API_URL
  246. codespace_name = os.getenv("CODESPACE_NAME")
  247. if "api_url" not in self._non_default_attributes and codespace_name:
  248. GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
  249. "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
  250. )
  251. if codespace_name:
  252. self.api_url = (
  253. f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
  254. f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
  255. )
  256. def _set_persistent(self, **kwargs):
  257. """Set values in this config and in the environment so they persist into subprocess.
  258. Args:
  259. **kwargs: The kwargs passed to the config.
  260. """
  261. for key, value in kwargs.items():
  262. if value is not None:
  263. os.environ[key.upper()] = str(value)
  264. setattr(self, key, value)
  265. self._non_default_attributes.update(kwargs)
  266. self._replace_defaults(**kwargs)
  267. def get_config(reload: bool = False) -> Config:
  268. """Get the app config.
  269. Args:
  270. reload: Re-import the rxconfig module from disk
  271. Returns:
  272. The app config.
  273. """
  274. sys.path.insert(0, os.getcwd())
  275. try:
  276. rxconfig = __import__(constants.Config.MODULE)
  277. if reload:
  278. importlib.reload(rxconfig)
  279. return rxconfig.config
  280. except ImportError:
  281. return Config(app_name="") # type: ignore