config.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. """The Reflex config."""
  2. from __future__ import annotations
  3. import dataclasses
  4. import enum
  5. import importlib
  6. import inspect
  7. import os
  8. import sys
  9. import urllib.parse
  10. from pathlib import Path
  11. from typing import (
  12. TYPE_CHECKING,
  13. Any,
  14. Callable,
  15. Dict,
  16. Generic,
  17. List,
  18. Optional,
  19. Set,
  20. TypeVar,
  21. get_args,
  22. )
  23. from typing_extensions import Annotated, get_type_hints
  24. from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError
  25. from reflex.utils.types import GenericType, is_union, value_inside_optional
  26. try:
  27. import pydantic.v1 as pydantic
  28. except ModuleNotFoundError:
  29. import pydantic
  30. from reflex_cli.constants.hosting import Hosting
  31. from reflex import constants
  32. from reflex.base import Base
  33. from reflex.utils import console
  34. class DBConfig(Base):
  35. """Database config."""
  36. engine: str
  37. username: Optional[str] = ""
  38. password: Optional[str] = ""
  39. host: Optional[str] = ""
  40. port: Optional[int] = None
  41. database: str
  42. @classmethod
  43. def postgresql(
  44. cls,
  45. database: str,
  46. username: str,
  47. password: str | None = None,
  48. host: str | None = None,
  49. port: int | None = 5432,
  50. ) -> DBConfig:
  51. """Create an instance with postgresql engine.
  52. Args:
  53. database: Database name.
  54. username: Database username.
  55. password: Database password.
  56. host: Database host.
  57. port: Database port.
  58. Returns:
  59. DBConfig instance.
  60. """
  61. return cls(
  62. engine="postgresql",
  63. username=username,
  64. password=password,
  65. host=host,
  66. port=port,
  67. database=database,
  68. )
  69. @classmethod
  70. def postgresql_psycopg2(
  71. cls,
  72. database: str,
  73. username: str,
  74. password: str | None = None,
  75. host: str | None = None,
  76. port: int | None = 5432,
  77. ) -> DBConfig:
  78. """Create an instance with postgresql+psycopg2 engine.
  79. Args:
  80. database: Database name.
  81. username: Database username.
  82. password: Database password.
  83. host: Database host.
  84. port: Database port.
  85. Returns:
  86. DBConfig instance.
  87. """
  88. return cls(
  89. engine="postgresql+psycopg2",
  90. username=username,
  91. password=password,
  92. host=host,
  93. port=port,
  94. database=database,
  95. )
  96. @classmethod
  97. def sqlite(
  98. cls,
  99. database: str,
  100. ) -> DBConfig:
  101. """Create an instance with sqlite engine.
  102. Args:
  103. database: Database name.
  104. Returns:
  105. DBConfig instance.
  106. """
  107. return cls(
  108. engine="sqlite",
  109. database=database,
  110. )
  111. def get_url(self) -> str:
  112. """Get database URL.
  113. Returns:
  114. The database URL.
  115. """
  116. host = (
  117. f"{self.host}:{self.port}" if self.host and self.port else self.host or ""
  118. )
  119. username = urllib.parse.quote_plus(self.username) if self.username else ""
  120. password = urllib.parse.quote_plus(self.password) if self.password else ""
  121. if username:
  122. path = f"{username}:{password}@{host}" if password else f"{username}@{host}"
  123. else:
  124. path = f"{host}"
  125. return f"{self.engine}://{path}/{self.database}"
  126. # TODO: Change all interpret_.* signatures to value: str, field: dataclasses.Field once we migrate rx.Config to dataclasses
  127. def interpret_boolean_env(value: str, field_name: str) -> bool:
  128. """Interpret a boolean environment variable value.
  129. Args:
  130. value: The environment variable value.
  131. field_name: The field name.
  132. Returns:
  133. The interpreted value.
  134. Raises:
  135. EnvironmentVarValueError: If the value is invalid.
  136. """
  137. true_values = ["true", "1", "yes", "y"]
  138. false_values = ["false", "0", "no", "n"]
  139. if value.lower() in true_values:
  140. return True
  141. elif value.lower() in false_values:
  142. return False
  143. raise EnvironmentVarValueError(f"Invalid boolean value: {value} for {field_name}")
  144. def interpret_int_env(value: str, field_name: str) -> int:
  145. """Interpret an integer environment variable value.
  146. Args:
  147. value: The environment variable value.
  148. field_name: The field name.
  149. Returns:
  150. The interpreted value.
  151. Raises:
  152. EnvironmentVarValueError: If the value is invalid.
  153. """
  154. try:
  155. return int(value)
  156. except ValueError as ve:
  157. raise EnvironmentVarValueError(
  158. f"Invalid integer value: {value} for {field_name}"
  159. ) from ve
  160. def interpret_existing_path_env(value: str, field_name: str) -> ExistingPath:
  161. """Interpret a path environment variable value as an existing path.
  162. Args:
  163. value: The environment variable value.
  164. field_name: The field name.
  165. Returns:
  166. The interpreted value.
  167. Raises:
  168. EnvironmentVarValueError: If the path does not exist.
  169. """
  170. path = Path(value)
  171. if not path.exists():
  172. raise EnvironmentVarValueError(f"Path does not exist: {path} for {field_name}")
  173. return path
  174. def interpret_path_env(value: str, field_name: str) -> Path:
  175. """Interpret a path environment variable value.
  176. Args:
  177. value: The environment variable value.
  178. field_name: The field name.
  179. Returns:
  180. The interpreted value.
  181. """
  182. return Path(value)
  183. def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any:
  184. """Interpret an enum environment variable value.
  185. Args:
  186. value: The environment variable value.
  187. field_type: The field type.
  188. field_name: The field name.
  189. Returns:
  190. The interpreted value.
  191. Raises:
  192. EnvironmentVarValueError: If the value is invalid.
  193. """
  194. try:
  195. return field_type(value)
  196. except ValueError as ve:
  197. raise EnvironmentVarValueError(
  198. f"Invalid enum value: {value} for {field_name}"
  199. ) from ve
  200. def interpret_env_var_value(
  201. value: str, field_type: GenericType, field_name: str
  202. ) -> Any:
  203. """Interpret an environment variable value based on the field type.
  204. Args:
  205. value: The environment variable value.
  206. field_type: The field type.
  207. field_name: The field name.
  208. Returns:
  209. The interpreted value.
  210. Raises:
  211. ValueError: If the value is invalid.
  212. """
  213. field_type = value_inside_optional(field_type)
  214. if is_union(field_type):
  215. raise ValueError(
  216. f"Union types are not supported for environment variables: {field_name}."
  217. )
  218. if field_type is bool:
  219. return interpret_boolean_env(value, field_name)
  220. elif field_type is str:
  221. return value
  222. elif field_type is int:
  223. return interpret_int_env(value, field_name)
  224. elif field_type is Path:
  225. return interpret_path_env(value, field_name)
  226. elif field_type is ExistingPath:
  227. return interpret_existing_path_env(value, field_name)
  228. elif inspect.isclass(field_type) and issubclass(field_type, enum.Enum):
  229. return interpret_enum_env(value, field_type, field_name)
  230. else:
  231. raise ValueError(
  232. f"Invalid type for environment variable {field_name}: {field_type}. This is probably an issue in Reflex."
  233. )
  234. T = TypeVar("T")
  235. ENV_VAR_DEFAULT_FACTORY = Callable[[], T]
  236. class EnvVar(Generic[T]):
  237. """Environment variable."""
  238. name: str
  239. default: Any
  240. default_factory: Optional[ENV_VAR_DEFAULT_FACTORY]
  241. type_: T
  242. def __init__(
  243. self,
  244. name: str,
  245. default: Any,
  246. default_factory: Optional[ENV_VAR_DEFAULT_FACTORY],
  247. type_: T,
  248. ) -> None:
  249. """Initialize the environment variable.
  250. Args:
  251. name: The environment variable name.
  252. default: The default value.
  253. default_factory: The default factory.
  254. type_: The type of the value.
  255. """
  256. self.name = name
  257. self.default = default
  258. self.default_factory = default_factory
  259. self.type_ = type_
  260. def get_default(self) -> T:
  261. """Get the default value.
  262. Returns:
  263. The default value.
  264. """
  265. if self.default_factory is not None:
  266. return self.default_factory()
  267. return self.default
  268. def interpret(self, value: str) -> T:
  269. """Interpret the environment variable value.
  270. Args:
  271. value: The environment variable value.
  272. Returns:
  273. The interpreted value.
  274. """
  275. return interpret_env_var_value(value, self.type_, self.name)
  276. def getenv(self) -> Optional[T]:
  277. """Get the interpreted environment variable value.
  278. Returns:
  279. The environment variable value.
  280. """
  281. env_value = os.getenv(self.name, None)
  282. if env_value is not None:
  283. return self.interpret(env_value)
  284. return None
  285. def is_set(self) -> bool:
  286. """Check if the environment variable is set.
  287. Returns:
  288. True if the environment variable is set.
  289. """
  290. return self.name in os.environ
  291. def get(self) -> T:
  292. """Get the interpreted environment variable value or the default value if not set.
  293. Returns:
  294. The interpreted value.
  295. """
  296. env_value = self.getenv()
  297. if env_value is not None:
  298. return env_value
  299. return self.get_default()
  300. def set(self, value: T | None) -> None:
  301. """Set the environment variable. None unsets the variable.
  302. Args:
  303. value: The value to set.
  304. """
  305. if value is None:
  306. _ = os.environ.pop(self.name, None)
  307. else:
  308. if isinstance(value, enum.Enum):
  309. value = value.value
  310. os.environ[self.name] = str(value)
  311. class env_var: # type: ignore
  312. """Descriptor for environment variables."""
  313. name: str
  314. default: Any
  315. default_factory: Optional[ENV_VAR_DEFAULT_FACTORY]
  316. internal: bool = False
  317. def __init__(
  318. self,
  319. default: Any = None,
  320. default_factory: Optional[ENV_VAR_DEFAULT_FACTORY] = None,
  321. internal: bool = False,
  322. ) -> None:
  323. """Initialize the descriptor.
  324. Args:
  325. default: The default value.
  326. default_factory: The default factory.
  327. internal: Whether the environment variable is reflex internal.
  328. """
  329. self.default = default
  330. self.default_factory = default_factory
  331. self.internal = internal
  332. def __set_name__(self, owner, name):
  333. """Set the name of the descriptor.
  334. Args:
  335. owner: The owner class.
  336. name: The name of the descriptor.
  337. """
  338. self.name = name
  339. def __get__(self, instance, owner):
  340. """Get the EnvVar instance.
  341. Args:
  342. instance: The instance.
  343. owner: The owner class.
  344. Returns:
  345. The EnvVar instance.
  346. """
  347. type_ = get_args(get_type_hints(owner)[self.name])[0]
  348. env_name = self.name
  349. if self.internal:
  350. env_name = f"__{env_name}"
  351. return EnvVar(
  352. name=env_name,
  353. default=self.default,
  354. type_=type_,
  355. default_factory=self.default_factory,
  356. )
  357. if TYPE_CHECKING:
  358. def __new__(
  359. cls,
  360. default: Optional[T] = None,
  361. default_factory: Optional[ENV_VAR_DEFAULT_FACTORY[T]] = None,
  362. internal: bool = False,
  363. ) -> EnvVar[T]:
  364. """Create a new EnvVar instance.
  365. Args:
  366. cls: The class.
  367. default: The default value.
  368. default_factory: The default factory.
  369. internal: Whether the environment variable is reflex internal.
  370. """
  371. ...
  372. class PathExistsFlag:
  373. """Flag to indicate that a path must exist."""
  374. ExistingPath = Annotated[Path, PathExistsFlag]
  375. class EnvironmentVariables:
  376. """Environment variables class to instantiate environment variables."""
  377. def __init__(self):
  378. """Initialize the environment variables.
  379. Raises:
  380. NotImplementedError: Always.
  381. """
  382. raise NotImplementedError(
  383. f"{type(self).__name__} is a class singleton and not meant to be instantiated."
  384. )
  385. # Whether to use npm over bun to install frontend packages.
  386. REFLEX_USE_NPM: EnvVar[bool] = env_var(False)
  387. # The npm registry to use.
  388. NPM_CONFIG_REGISTRY: EnvVar[Optional[str]] = env_var(None)
  389. # Whether to use Granian for the backend. Otherwise, use Uvicorn.
  390. REFLEX_USE_GRANIAN: EnvVar[bool] = env_var(False)
  391. # The username to use for authentication on python package repository. Username and password must both be provided.
  392. TWINE_USERNAME: EnvVar[Optional[str]] = env_var(None)
  393. # The password to use for authentication on python package repository. Username and password must both be provided.
  394. TWINE_PASSWORD: EnvVar[Optional[str]] = env_var(None)
  395. # Whether to use the system installed bun. If set to false, bun will be bundled with the app.
  396. REFLEX_USE_SYSTEM_BUN: EnvVar[bool] = env_var(False)
  397. # Whether to use the system installed node and npm. If set to false, node and npm will be bundled with the app.
  398. REFLEX_USE_SYSTEM_NODE: EnvVar[bool] = env_var(False)
  399. # The working directory for the next.js commands.
  400. REFLEX_WEB_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.WEB))
  401. # Path to the alembic config file
  402. ALEMBIC_CONFIG: EnvVar[ExistingPath] = env_var(Path(constants.ALEMBIC_CONFIG))
  403. # Disable SSL verification for HTTPX requests.
  404. SSL_NO_VERIFY: EnvVar[bool] = env_var(False)
  405. # The directory to store uploaded files.
  406. REFLEX_UPLOADED_FILES_DIR: EnvVar[Path] = env_var(
  407. Path(constants.Dirs.UPLOADED_FILES)
  408. )
  409. # Whether to use separate processes to compile the frontend and how many. If not set, defaults to thread executor.
  410. REFLEX_COMPILE_PROCESSES: EnvVar[Optional[int]] = env_var(None)
  411. # Whether to use separate threads to compile the frontend and how many. Defaults to `min(32, os.cpu_count() + 4)`.
  412. REFLEX_COMPILE_THREADS: EnvVar[Optional[int]] = env_var(None)
  413. # The directory to store reflex dependencies.
  414. REFLEX_DIR: EnvVar[Path] = env_var(Path(constants.Reflex.DIR))
  415. # Whether to print the SQL queries if the log level is INFO or lower.
  416. SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False)
  417. # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration.
  418. REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False)
  419. # Whether to skip purging the web directory in dev mode.
  420. REFLEX_PERSIST_WEB_DIR: EnvVar[bool] = env_var(False)
  421. # The reflex.build frontend host.
  422. REFLEX_BUILD_FRONTEND: EnvVar[str] = env_var(
  423. constants.Templates.REFLEX_BUILD_FRONTEND
  424. )
  425. # The reflex.build backend host.
  426. REFLEX_BUILD_BACKEND: EnvVar[str] = env_var(
  427. constants.Templates.REFLEX_BUILD_BACKEND
  428. )
  429. # This env var stores the execution mode of the app
  430. REFLEX_ENV_MODE: EnvVar[constants.Env] = env_var(constants.Env.DEV)
  431. # Whether to run the backend only. Exclusive with REFLEX_FRONTEND_ONLY.
  432. REFLEX_BACKEND_ONLY: EnvVar[bool] = env_var(False)
  433. # Whether to run the frontend only. Exclusive with REFLEX_BACKEND_ONLY.
  434. REFLEX_FRONTEND_ONLY: EnvVar[bool] = env_var(False)
  435. # Reflex internal env to reload the config.
  436. RELOAD_CONFIG: EnvVar[bool] = env_var(False, internal=True)
  437. # If this env var is set to "yes", App.compile will be a no-op
  438. REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True)
  439. # Whether to run app harness tests in headless mode.
  440. APP_HARNESS_HEADLESS: EnvVar[bool] = env_var(False)
  441. # Which app harness driver to use.
  442. APP_HARNESS_DRIVER: EnvVar[str] = env_var("Chrome")
  443. # Arguments to pass to the app harness driver.
  444. APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("")
  445. # Where to save screenshots when tests fail.
  446. SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None)
  447. # Whether to minify state names. Default to true in prod mode and false otherwise.
  448. REFLEX_MINIFY_STATES: EnvVar[Optional[bool]] = env_var(
  449. default_factory=lambda: EnvironmentVariables.REFLEX_ENV_MODE.get()
  450. == constants.Env.PROD
  451. )
  452. class Config(Base):
  453. """The config defines runtime settings for the app.
  454. By default, the config is defined in an `rxconfig.py` file in the root of the app.
  455. ```python
  456. # rxconfig.py
  457. import reflex as rx
  458. config = rx.Config(
  459. app_name="myapp",
  460. api_url="http://localhost:8000",
  461. )
  462. ```
  463. Every config value can be overridden by an environment variable with the same name in uppercase.
  464. For example, `db_url` can be overridden by setting the `DB_URL` environment variable.
  465. See the [configuration](https://reflex.dev/docs/getting-started/configuration/) docs for more info.
  466. """
  467. class Config:
  468. """Pydantic config for the config."""
  469. validate_assignment = True
  470. # The name of the app (should match the name of the app directory).
  471. app_name: str
  472. # The log level to use.
  473. loglevel: constants.LogLevel = constants.LogLevel.DEFAULT
  474. # The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
  475. frontend_port: int = constants.DefaultPorts.FRONTEND_PORT
  476. # The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
  477. frontend_path: str = ""
  478. # The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
  479. backend_port: int = constants.DefaultPorts.BACKEND_PORT
  480. # The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production.
  481. api_url: str = f"http://localhost:{backend_port}"
  482. # The url the frontend will be hosted on.
  483. deploy_url: Optional[str] = f"http://localhost:{frontend_port}"
  484. # The url the backend will be hosted on.
  485. backend_host: str = "0.0.0.0"
  486. # The database url used by rx.Model.
  487. db_url: Optional[str] = "sqlite:///reflex.db"
  488. # The redis url
  489. redis_url: Optional[str] = None
  490. # Telemetry opt-in.
  491. telemetry_enabled: bool = True
  492. # The bun path
  493. bun_path: ExistingPath = constants.Bun.DEFAULT_PATH
  494. # Timeout to do a production build of a frontend page.
  495. static_page_generation_timeout: int = 60
  496. # List of origins that are allowed to connect to the backend API.
  497. cors_allowed_origins: List[str] = ["*"]
  498. # Tailwind config.
  499. tailwind: Optional[Dict[str, Any]] = {"plugins": ["@tailwindcss/typography"]}
  500. # Timeout when launching the gunicorn server. TODO(rename this to backend_timeout?)
  501. timeout: int = 120
  502. # Whether to enable or disable nextJS gzip compression.
  503. next_compression: bool = True
  504. # Whether to use React strict mode in nextJS
  505. react_strict_mode: bool = True
  506. # Additional frontend packages to install.
  507. frontend_packages: List[str] = []
  508. # The hosting service backend URL.
  509. cp_backend_url: str = Hosting.CP_BACKEND_URL
  510. # The hosting service frontend URL.
  511. cp_web_url: str = Hosting.CP_WEB_URL
  512. # The worker class used in production mode
  513. gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
  514. # Number of gunicorn workers from user
  515. gunicorn_workers: Optional[int] = None
  516. # Number of requests before a worker is restarted
  517. gunicorn_max_requests: int = 100
  518. # Variance limit for max requests; gunicorn only
  519. gunicorn_max_requests_jitter: int = 25
  520. # Indicate which type of state manager to use
  521. state_manager_mode: constants.StateManagerMode = constants.StateManagerMode.DISK
  522. # Maximum expiration lock time for redis state manager
  523. redis_lock_expiration: int = constants.Expiration.LOCK
  524. # Token expiration time for redis state manager
  525. redis_token_expiration: int = constants.Expiration.TOKEN
  526. # Attributes that were explicitly set by the user.
  527. _non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
  528. # Path to file containing key-values pairs to override in the environment; Dotenv format.
  529. env_file: Optional[str] = None
  530. def __init__(self, *args, **kwargs):
  531. """Initialize the config values.
  532. Args:
  533. *args: The args to pass to the Pydantic init method.
  534. **kwargs: The kwargs to pass to the Pydantic init method.
  535. Raises:
  536. ConfigError: If some values in the config are invalid.
  537. """
  538. super().__init__(*args, **kwargs)
  539. # Update the config from environment variables.
  540. env_kwargs = self.update_from_env()
  541. for key, env_value in env_kwargs.items():
  542. setattr(self, key, env_value)
  543. # Update default URLs if ports were set
  544. kwargs.update(env_kwargs)
  545. self._non_default_attributes.update(kwargs)
  546. self._replace_defaults(**kwargs)
  547. if (
  548. self.state_manager_mode == constants.StateManagerMode.REDIS
  549. and not self.redis_url
  550. ):
  551. raise ConfigError(
  552. "REDIS_URL is required when using the redis state manager."
  553. )
  554. @property
  555. def module(self) -> str:
  556. """Get the module name of the app.
  557. Returns:
  558. The module name.
  559. """
  560. return ".".join([self.app_name, self.app_name])
  561. def update_from_env(self) -> dict[str, Any]:
  562. """Update the config values based on set environment variables.
  563. If there is a set env_file, it is loaded first.
  564. Returns:
  565. The updated config values.
  566. """
  567. if self.env_file:
  568. try:
  569. from dotenv import load_dotenv # type: ignore
  570. # load env file if exists
  571. load_dotenv(self.env_file, override=True)
  572. except ImportError:
  573. console.error(
  574. """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.0.1"`."""
  575. )
  576. updated_values = {}
  577. # Iterate over the fields.
  578. for key, field in self.__fields__.items():
  579. # The env var name is the key in uppercase.
  580. env_var = os.environ.get(key.upper())
  581. # If the env var is set, override the config value.
  582. if env_var is not None:
  583. if key.upper() != "DB_URL":
  584. console.info(
  585. f"Overriding config value {key} with env var {key.upper()}={env_var}",
  586. dedupe=True,
  587. )
  588. # Interpret the value.
  589. value = interpret_env_var_value(env_var, field.outer_type_, field.name)
  590. # Set the value.
  591. updated_values[key] = value
  592. return updated_values
  593. def get_event_namespace(self) -> str:
  594. """Get the path that the backend Websocket server lists on.
  595. Returns:
  596. The namespace for websocket.
  597. """
  598. event_url = constants.Endpoint.EVENT.get_url()
  599. return urllib.parse.urlsplit(event_url).path
  600. def _replace_defaults(self, **kwargs):
  601. """Replace formatted defaults when the caller provides updates.
  602. Args:
  603. **kwargs: The kwargs passed to the config or from the env.
  604. """
  605. if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
  606. self.api_url = f"http://localhost:{kwargs['backend_port']}"
  607. if (
  608. "deploy_url" not in self._non_default_attributes
  609. and "frontend_port" in kwargs
  610. ):
  611. self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
  612. if "api_url" not in self._non_default_attributes:
  613. # If running in Github Codespaces, override API_URL
  614. codespace_name = os.getenv("CODESPACE_NAME")
  615. GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
  616. "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
  617. )
  618. # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port
  619. replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN")
  620. backend_port = kwargs.get("backend_port", self.backend_port)
  621. if codespace_name and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN:
  622. self.api_url = (
  623. f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
  624. f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
  625. )
  626. elif replit_dev_domain and backend_port:
  627. self.api_url = f"https://{replit_dev_domain}:{backend_port}"
  628. def _set_persistent(self, **kwargs):
  629. """Set values in this config and in the environment so they persist into subprocess.
  630. Args:
  631. **kwargs: The kwargs passed to the config.
  632. """
  633. for key, value in kwargs.items():
  634. if value is not None:
  635. os.environ[key.upper()] = str(value)
  636. setattr(self, key, value)
  637. self._non_default_attributes.update(kwargs)
  638. self._replace_defaults(**kwargs)
  639. def get_config(reload: bool = False) -> Config:
  640. """Get the app config.
  641. Args:
  642. reload: Re-import the rxconfig module from disk
  643. Returns:
  644. The app config.
  645. """
  646. sys.path.insert(0, os.getcwd())
  647. # only import the module if it exists. If a module spec exists then
  648. # the module exists.
  649. spec = importlib.util.find_spec(constants.Config.MODULE) # type: ignore
  650. if not spec:
  651. # we need this condition to ensure that a ModuleNotFound error is not thrown when
  652. # running unit/integration tests.
  653. return Config(app_name="")
  654. rxconfig = importlib.import_module(constants.Config.MODULE)
  655. if reload:
  656. importlib.reload(rxconfig)
  657. return rxconfig.config