|
19 | 19 | Any,
|
20 | 20 | Callable,
|
21 | 21 | Generic,
|
| 22 | + Iterator, |
22 | 23 | List,
|
23 | 24 | Mapping,
|
| 25 | + Optional, |
24 | 26 | Sequence,
|
25 | 27 | Tuple,
|
26 | 28 | TypeVar,
|
@@ -83,6 +85,21 @@ def import_toml() -> None:
|
83 | 85 | import tomllib
|
84 | 86 |
|
85 | 87 |
|
| 88 | +def import_azure_key_vault() -> None: |
| 89 | + global TokenCredential |
| 90 | + global SecretClient |
| 91 | + global ResourceNotFoundError |
| 92 | + |
| 93 | + try: |
| 94 | + from azure.core.credentials import TokenCredential |
| 95 | + from azure.core.exceptions import ResourceNotFoundError |
| 96 | + from azure.keyvault.secrets import SecretClient |
| 97 | + except ImportError as e: |
| 98 | + raise ImportError( |
| 99 | + 'Azure Key Vault dependencies are not installed, run `pip install pydantic-settings[azure-key-vault]`' |
| 100 | + ) from e |
| 101 | + |
| 102 | + |
86 | 103 | DotenvType = Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]
|
87 | 104 | PathType = Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]
|
88 | 105 | DEFAULT_PATH: PathType = Path('')
|
@@ -1725,6 +1742,70 @@ def _read_file(self, file_path: Path) -> dict[str, Any]:
|
1725 | 1742 | return yaml.safe_load(yaml_file) or {}
|
1726 | 1743 |
|
1727 | 1744 |
|
| 1745 | +class AzureKeyVaultMapping(Mapping[str, Optional[str]]): |
| 1746 | + _loaded_secrets: dict[str, str | None] |
| 1747 | + _secret_client: SecretClient # type: ignore |
| 1748 | + _secret_names: list[str] |
| 1749 | + |
| 1750 | + def __init__( |
| 1751 | + self, |
| 1752 | + secret_client: SecretClient, # type: ignore |
| 1753 | + ) -> None: |
| 1754 | + self._loaded_secrets = {} |
| 1755 | + self._secret_client = secret_client |
| 1756 | + self._secret_names: list[str] = [secret.name for secret in self._secret_client.list_properties_of_secrets()] |
| 1757 | + |
| 1758 | + def __getitem__(self, key: str) -> str | None: |
| 1759 | + if key not in self._loaded_secrets: |
| 1760 | + try: |
| 1761 | + self._loaded_secrets[key] = self._secret_client.get_secret(key).value |
| 1762 | + except ResourceNotFoundError: # type: ignore |
| 1763 | + raise KeyError(key) |
| 1764 | + |
| 1765 | + return self._loaded_secrets[key] |
| 1766 | + |
| 1767 | + def __len__(self) -> int: |
| 1768 | + return len(self._secret_names) |
| 1769 | + |
| 1770 | + def __iter__(self) -> Iterator[str]: |
| 1771 | + return iter(self._secret_names) |
| 1772 | + |
| 1773 | + |
| 1774 | +class AzureKeyVaultSettingsSource(EnvSettingsSource): |
| 1775 | + _url: str |
| 1776 | + _credential: TokenCredential # type: ignore |
| 1777 | + _secret_client: SecretClient # type: ignore |
| 1778 | + |
| 1779 | + def __init__( |
| 1780 | + self, |
| 1781 | + settings_cls: type[BaseSettings], |
| 1782 | + url: str, |
| 1783 | + credential: TokenCredential, # type: ignore |
| 1784 | + env_prefix: str | None = None, |
| 1785 | + env_parse_none_str: str | None = None, |
| 1786 | + env_parse_enums: bool | None = None, |
| 1787 | + ) -> None: |
| 1788 | + import_azure_key_vault() |
| 1789 | + self._url = url |
| 1790 | + self._credential = credential |
| 1791 | + super().__init__( |
| 1792 | + settings_cls, |
| 1793 | + case_sensitive=True, |
| 1794 | + env_prefix=env_prefix, |
| 1795 | + env_nested_delimiter='--', |
| 1796 | + env_ignore_empty=False, |
| 1797 | + env_parse_none_str=env_parse_none_str, |
| 1798 | + env_parse_enums=env_parse_enums, |
| 1799 | + ) |
| 1800 | + |
| 1801 | + def _load_env_vars(self) -> Mapping[str, Optional[str]]: |
| 1802 | + secret_client = SecretClient(vault_url=self._url, credential=self._credential) # type: ignore |
| 1803 | + return AzureKeyVaultMapping(secret_client) |
| 1804 | + |
| 1805 | + def __repr__(self) -> str: |
| 1806 | + return f'AzureKeyVaultSettingsSource(url={self._url!r}, ' f'env_nested_delimiter={self.env_nested_delimiter!r})' |
| 1807 | + |
| 1808 | + |
1728 | 1809 | def _get_env_var_key(key: str, case_sensitive: bool = False) -> str:
|
1729 | 1810 | return key if case_sensitive else key.lower()
|
1730 | 1811 |
|
|
0 commit comments