config.py 11 KB

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