main.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. from __future__ import annotations as _annotations
  2. from pathlib import Path
  3. from typing import Any, ClassVar
  4. from pydantic import ConfigDict
  5. from pydantic._internal._config import config_keys
  6. from pydantic._internal._utils import deep_update
  7. from pydantic.main import BaseModel
  8. from .sources import (
  9. ENV_FILE_SENTINEL,
  10. DotEnvSettingsSource,
  11. DotenvType,
  12. EnvSettingsSource,
  13. InitSettingsSource,
  14. PydanticBaseSettingsSource,
  15. SecretsSettingsSource,
  16. )
  17. class SettingsConfigDict(ConfigDict, total=False):
  18. case_sensitive: bool
  19. env_prefix: str
  20. env_file: DotenvType | None
  21. env_file_encoding: str | None
  22. env_nested_delimiter: str | None
  23. secrets_dir: str | Path | None
  24. # Extend `config_keys` by pydantic settings config keys to
  25. # support setting config through class kwargs.
  26. # Pydantic uses `config_keys` in `pydantic._internal._config.ConfigWrapper.for_model`
  27. # to extract config keys from model kwargs, So, by adding pydantic settings keys to
  28. # `config_keys`, they will be considered as valid config keys and will be collected
  29. # by Pydantic.
  30. config_keys |= set(SettingsConfigDict.__annotations__.keys())
  31. class BaseSettings(BaseModel):
  32. """
  33. Base class for settings, allowing values to be overridden by environment variables.
  34. This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose),
  35. Heroku and any 12 factor app design.
  36. All the below attributes can be set via `model_config`.
  37. Args:
  38. _case_sensitive: Whether environment variables names should be read with case-sensitivity. Defaults to `None`.
  39. _env_prefix: Prefix for all environment variables. Defaults to `None`.
  40. _env_file: The env file(s) to load settings values from. Defaults to `Path('')`, which
  41. means that the value from `model_config['env_file']` should be used. You can also pass
  42. `None` to indicate that environment variables should not be loaded from an env file.
  43. _env_file_encoding: The env file encoding, e.g. `'latin-1'`. Defaults to `None`.
  44. _env_nested_delimiter: The nested env values delimiter. Defaults to `None`.
  45. _secrets_dir: The secret files directory. Defaults to `None`.
  46. """
  47. def __init__(
  48. __pydantic_self__,
  49. _case_sensitive: bool | None = None,
  50. _env_prefix: str | None = None,
  51. _env_file: DotenvType | None = ENV_FILE_SENTINEL,
  52. _env_file_encoding: str | None = None,
  53. _env_nested_delimiter: str | None = None,
  54. _secrets_dir: str | Path | None = None,
  55. **values: Any,
  56. ) -> None:
  57. # Uses something other than `self` the first arg to allow "self" as a settable attribute
  58. super().__init__(
  59. **__pydantic_self__._settings_build_values(
  60. values,
  61. _case_sensitive=_case_sensitive,
  62. _env_prefix=_env_prefix,
  63. _env_file=_env_file,
  64. _env_file_encoding=_env_file_encoding,
  65. _env_nested_delimiter=_env_nested_delimiter,
  66. _secrets_dir=_secrets_dir,
  67. )
  68. )
  69. @classmethod
  70. def settings_customise_sources(
  71. cls,
  72. settings_cls: type[BaseSettings],
  73. init_settings: PydanticBaseSettingsSource,
  74. env_settings: PydanticBaseSettingsSource,
  75. dotenv_settings: PydanticBaseSettingsSource,
  76. file_secret_settings: PydanticBaseSettingsSource,
  77. ) -> tuple[PydanticBaseSettingsSource, ...]:
  78. """
  79. Define the sources and their order for loading the settings values.
  80. Args:
  81. settings_cls: The Settings class.
  82. init_settings: The `InitSettingsSource` instance.
  83. env_settings: The `EnvSettingsSource` instance.
  84. dotenv_settings: The `DotEnvSettingsSource` instance.
  85. file_secret_settings: The `SecretsSettingsSource` instance.
  86. Returns:
  87. A tuple containing the sources and their order for loading the settings values.
  88. """
  89. return init_settings, env_settings, dotenv_settings, file_secret_settings
  90. def _settings_build_values(
  91. self,
  92. init_kwargs: dict[str, Any],
  93. _case_sensitive: bool | None = None,
  94. _env_prefix: str | None = None,
  95. _env_file: DotenvType | None = None,
  96. _env_file_encoding: str | None = None,
  97. _env_nested_delimiter: str | None = None,
  98. _secrets_dir: str | Path | None = None,
  99. ) -> dict[str, Any]:
  100. # Determine settings config values
  101. case_sensitive = _case_sensitive if _case_sensitive is not None else self.model_config.get('case_sensitive')
  102. env_prefix = _env_prefix if _env_prefix is not None else self.model_config.get('env_prefix')
  103. env_file = _env_file if _env_file != ENV_FILE_SENTINEL else self.model_config.get('env_file')
  104. env_file_encoding = (
  105. _env_file_encoding if _env_file_encoding is not None else self.model_config.get('env_file_encoding')
  106. )
  107. env_nested_delimiter = (
  108. _env_nested_delimiter
  109. if _env_nested_delimiter is not None
  110. else self.model_config.get('env_nested_delimiter')
  111. )
  112. secrets_dir = _secrets_dir if _secrets_dir is not None else self.model_config.get('secrets_dir')
  113. # Configure built-in sources
  114. init_settings = InitSettingsSource(self.__class__, init_kwargs=init_kwargs)
  115. env_settings = EnvSettingsSource(
  116. self.__class__,
  117. case_sensitive=case_sensitive,
  118. env_prefix=env_prefix,
  119. env_nested_delimiter=env_nested_delimiter,
  120. )
  121. dotenv_settings = DotEnvSettingsSource(
  122. self.__class__,
  123. env_file=env_file,
  124. env_file_encoding=env_file_encoding,
  125. case_sensitive=case_sensitive,
  126. env_prefix=env_prefix,
  127. env_nested_delimiter=env_nested_delimiter,
  128. )
  129. file_secret_settings = SecretsSettingsSource(
  130. self.__class__, secrets_dir=secrets_dir, case_sensitive=case_sensitive, env_prefix=env_prefix
  131. )
  132. # Provide a hook to set built-in sources priority and add / remove sources
  133. sources = self.settings_customise_sources(
  134. self.__class__,
  135. init_settings=init_settings,
  136. env_settings=env_settings,
  137. dotenv_settings=dotenv_settings,
  138. file_secret_settings=file_secret_settings,
  139. )
  140. if sources:
  141. return deep_update(*reversed([source() for source in sources]))
  142. else:
  143. # no one should mean to do this, but I think returning an empty dict is marginally preferable
  144. # to an informative error and much better than a confusing error
  145. return {}
  146. model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
  147. extra='forbid',
  148. arbitrary_types_allowed=True,
  149. validate_default=True,
  150. case_sensitive=False,
  151. env_prefix='',
  152. env_file=None,
  153. env_file_encoding=None,
  154. env_nested_delimiter=None,
  155. secrets_dir=None,
  156. protected_namespaces=('model_', 'settings_'),
  157. )