secrets.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """Secrets file settings source."""
  2. from __future__ import annotations as _annotations
  3. import os
  4. import warnings
  5. from pathlib import Path
  6. from typing import (
  7. TYPE_CHECKING,
  8. Any,
  9. )
  10. from pydantic.fields import FieldInfo
  11. from pydantic_settings.utils import path_type_label
  12. from ...exceptions import SettingsError
  13. from ..base import PydanticBaseEnvSettingsSource
  14. from ..types import PathType
  15. if TYPE_CHECKING:
  16. from pydantic_settings.main import BaseSettings
  17. class SecretsSettingsSource(PydanticBaseEnvSettingsSource):
  18. """
  19. Source class for loading settings values from secret files.
  20. """
  21. def __init__(
  22. self,
  23. settings_cls: type[BaseSettings],
  24. secrets_dir: PathType | None = None,
  25. case_sensitive: bool | None = None,
  26. env_prefix: str | None = None,
  27. env_ignore_empty: bool | None = None,
  28. env_parse_none_str: str | None = None,
  29. env_parse_enums: bool | None = None,
  30. ) -> None:
  31. super().__init__(
  32. settings_cls, case_sensitive, env_prefix, env_ignore_empty, env_parse_none_str, env_parse_enums
  33. )
  34. self.secrets_dir = secrets_dir if secrets_dir is not None else self.config.get('secrets_dir')
  35. def __call__(self) -> dict[str, Any]:
  36. """
  37. Build fields from "secrets" files.
  38. """
  39. secrets: dict[str, str | None] = {}
  40. if self.secrets_dir is None:
  41. return secrets
  42. secrets_dirs = [self.secrets_dir] if isinstance(self.secrets_dir, (str, os.PathLike)) else self.secrets_dir
  43. secrets_paths = [Path(p).expanduser() for p in secrets_dirs]
  44. self.secrets_paths = []
  45. for path in secrets_paths:
  46. if not path.exists():
  47. warnings.warn(f'directory "{path}" does not exist')
  48. else:
  49. self.secrets_paths.append(path)
  50. if not len(self.secrets_paths):
  51. return secrets
  52. for path in self.secrets_paths:
  53. if not path.is_dir():
  54. raise SettingsError(f'secrets_dir must reference a directory, not a {path_type_label(path)}')
  55. return super().__call__()
  56. @classmethod
  57. def find_case_path(cls, dir_path: Path, file_name: str, case_sensitive: bool) -> Path | None:
  58. """
  59. Find a file within path's directory matching filename, optionally ignoring case.
  60. Args:
  61. dir_path: Directory path.
  62. file_name: File name.
  63. case_sensitive: Whether to search for file name case sensitively.
  64. Returns:
  65. Whether file path or `None` if file does not exist in directory.
  66. """
  67. for f in dir_path.iterdir():
  68. if f.name == file_name:
  69. return f
  70. elif not case_sensitive and f.name.lower() == file_name.lower():
  71. return f
  72. return None
  73. def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
  74. """
  75. Gets the value for field from secret file and a flag to determine whether value is complex.
  76. Args:
  77. field: The field.
  78. field_name: The field name.
  79. Returns:
  80. A tuple that contains the value (`None` if the file does not exist), key, and
  81. a flag to determine whether value is complex.
  82. """
  83. for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name):
  84. # paths reversed to match the last-wins behaviour of `env_file`
  85. for secrets_path in reversed(self.secrets_paths):
  86. path = self.find_case_path(secrets_path, env_name, self.case_sensitive)
  87. if not path:
  88. # path does not exist, we currently don't return a warning for this
  89. continue
  90. if path.is_file():
  91. return path.read_text().strip(), field_key, value_is_complex
  92. else:
  93. warnings.warn(
  94. f'attempted to load secret file "{path}" but found a {path_type_label(path)} instead.',
  95. stacklevel=4,
  96. )
  97. return None, field_key, value_is_complex
  98. def __repr__(self) -> str:
  99. return f'{self.__class__.__name__}(secrets_dir={self.secrets_dir!r})'