Загрузить файлы в «venv/Lib/site-packages/pydantic_settings/sources/providers»

This commit is contained in:
2026-07-02 20:11:15 +00:00
parent e8cd06806e
commit 977c9f68b8
5 changed files with 2085 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
"""Package containing individual source implementations."""
from .aws import AWSSecretsManagerSettingsSource
from .azure import AzureKeyVaultSettingsSource
from .cli import (
CliDualFlag,
CliExplicitFlag,
CliImplicitFlag,
CliMutuallyExclusiveGroup,
CliPositionalArg,
CliSettingsSource,
CliSubCommand,
CliSuppress,
CliToggleFlag,
)
from .dotenv import DotEnvSettingsSource
from .env import EnvSettingsSource
from .gcp import GoogleSecretManagerSettingsSource
from .json import JsonConfigSettingsSource
from .pyproject import PyprojectTomlConfigSettingsSource
from .secrets import SecretsSettingsSource
from .toml import TomlConfigSettingsSource
from .yaml import YamlConfigSettingsSource
__all__ = [
'AWSSecretsManagerSettingsSource',
'AzureKeyVaultSettingsSource',
'CliExplicitFlag',
'CliImplicitFlag',
'CliToggleFlag',
'CliDualFlag',
'CliMutuallyExclusiveGroup',
'CliPositionalArg',
'CliSettingsSource',
'CliSubCommand',
'CliSuppress',
'DotEnvSettingsSource',
'EnvSettingsSource',
'GoogleSecretManagerSettingsSource',
'JsonConfigSettingsSource',
'PyprojectTomlConfigSettingsSource',
'SecretsSettingsSource',
'TomlConfigSettingsSource',
'YamlConfigSettingsSource',
]

View File

@@ -0,0 +1,86 @@
from __future__ import annotations as _annotations # important for BaseSettings import to work
import json
from collections.abc import Mapping
from typing import TYPE_CHECKING
from ..utils import parse_env_vars
from .env import EnvSettingsSource
if TYPE_CHECKING:
from pydantic_settings.main import BaseSettings
boto3_client = None
SecretsManagerClient = None
def import_aws_secrets_manager() -> None:
global boto3_client
global SecretsManagerClient
try:
from boto3 import client as boto3_client
from types_boto3_secretsmanager.client import SecretsManagerClient
except ImportError as e: # pragma: no cover
raise ImportError(
'AWS Secrets Manager dependencies are not installed, run `pip install pydantic-settings[aws-secrets-manager]`'
) from e
class AWSSecretsManagerSettingsSource(EnvSettingsSource):
_secret_id: str
_secretsmanager_client: SecretsManagerClient # type: ignore
def __init__(
self,
settings_cls: type[BaseSettings],
secret_id: str,
region_name: str | None = None,
endpoint_url: str | None = None,
case_sensitive: bool | None = True,
env_prefix: str | None = None,
env_nested_delimiter: str | None = '--',
env_parse_none_str: str | None = None,
env_parse_enums: bool | None = None,
version_id: str | None = None,
) -> None:
import_aws_secrets_manager()
self._secretsmanager_client = boto3_client('secretsmanager', region_name=region_name, endpoint_url=endpoint_url) # type: ignore
self._secret_id = secret_id
self._version_id = version_id
super().__init__(
settings_cls,
case_sensitive=case_sensitive,
env_prefix=env_prefix,
env_nested_delimiter=env_nested_delimiter,
env_ignore_empty=False,
env_parse_none_str=env_parse_none_str,
env_parse_enums=env_parse_enums,
)
def _load_env_vars(self) -> Mapping[str, str | None]:
request = {'SecretId': self._secret_id}
if self._version_id:
request['VersionId'] = self._version_id
response = self._secretsmanager_client.get_secret_value(**request) # type: ignore
return parse_env_vars(
json.loads(response['SecretString']),
self.case_sensitive,
self.env_ignore_empty,
self.env_parse_none_str,
)
def __repr__(self) -> str:
return (
f'{self.__class__.__name__}(secret_id={self._secret_id!r}, '
f'env_nested_delimiter={self.env_nested_delimiter!r})'
)
__all__ = [
'AWSSecretsManagerSettingsSource',
]

View File

@@ -0,0 +1,159 @@
"""Azure Key Vault settings source."""
from __future__ import annotations as _annotations
from collections.abc import Iterator, Mapping
from typing import TYPE_CHECKING
from pydantic.alias_generators import to_snake
from pydantic.fields import FieldInfo
from .env import EnvSettingsSource
if TYPE_CHECKING:
from azure.core.credentials import TokenCredential
from azure.core.exceptions import ResourceNotFoundError
from azure.keyvault.secrets import SecretClient
from pydantic_settings.main import BaseSettings
else:
TokenCredential = None
ResourceNotFoundError = None
SecretClient = None
def import_azure_key_vault() -> None:
global TokenCredential
global SecretClient
global ResourceNotFoundError
try:
from azure.core.credentials import TokenCredential
from azure.core.exceptions import ResourceNotFoundError
from azure.keyvault.secrets import SecretClient
except ImportError as e: # pragma: no cover
raise ImportError(
'Azure Key Vault dependencies are not installed, run `pip install pydantic-settings[azure-key-vault]`'
) from e
class AzureKeyVaultMapping(Mapping[str, str | None]):
_loaded_secrets: dict[str, str | None]
_secret_client: SecretClient
_secret_names: list[str]
def __init__(
self,
secret_client: SecretClient,
case_sensitive: bool,
snake_case_conversion: bool,
env_prefix: str | None,
) -> None:
self._loaded_secrets = {}
self._secret_client = secret_client
self._case_sensitive = case_sensitive
self._snake_case_conversion = snake_case_conversion
self._env_prefix = env_prefix if env_prefix else ''
self._secret_map: dict[str, str] = self._load_remote()
def _load_remote(self) -> dict[str, str]:
secret_names: Iterator[str] = (
secret.name for secret in self._secret_client.list_properties_of_secrets() if secret.name and secret.enabled
)
if self._snake_case_conversion:
name_map: dict[str, str] = {}
for name in secret_names:
if name.startswith(self._env_prefix):
name_map[f'{self._env_prefix}{to_snake(name[len(self._env_prefix) :])}'] = name
else:
name_map[to_snake(name)] = name
return name_map
if self._case_sensitive:
return {name: name for name in secret_names}
return {name.lower(): name for name in secret_names}
def __getitem__(self, key: str) -> str | None:
new_key = key
if self._snake_case_conversion:
if key.startswith(self._env_prefix):
new_key = f'{self._env_prefix}{to_snake(key[len(self._env_prefix) :])}'
else:
new_key = to_snake(key)
elif not self._case_sensitive:
new_key = key.lower()
if new_key not in self._loaded_secrets:
if new_key in self._secret_map:
self._loaded_secrets[new_key] = self._secret_client.get_secret(self._secret_map[new_key]).value
else:
raise KeyError(key)
return self._loaded_secrets[new_key]
def __len__(self) -> int:
return len(self._secret_map)
def __iter__(self) -> Iterator[str]:
return iter(self._secret_map.keys())
class AzureKeyVaultSettingsSource(EnvSettingsSource):
_url: str
_credential: TokenCredential
def __init__(
self,
settings_cls: type[BaseSettings],
url: str,
credential: TokenCredential,
dash_to_underscore: bool = False,
case_sensitive: bool | None = None,
snake_case_conversion: bool = False,
env_prefix: str | None = None,
env_parse_none_str: str | None = None,
env_parse_enums: bool | None = None,
) -> None:
import_azure_key_vault()
self._url = url
self._credential = credential
self._dash_to_underscore = dash_to_underscore
self._snake_case_conversion = snake_case_conversion
super().__init__(
settings_cls,
case_sensitive=True if snake_case_conversion else case_sensitive,
env_prefix=env_prefix,
env_nested_delimiter='__' if snake_case_conversion else '--',
env_ignore_empty=False,
env_parse_none_str=env_parse_none_str,
env_parse_enums=env_parse_enums,
)
def _load_env_vars(self) -> Mapping[str, str | None]:
secret_client = SecretClient(vault_url=self._url, credential=self._credential)
return AzureKeyVaultMapping(
secret_client=secret_client,
case_sensitive=self.case_sensitive,
snake_case_conversion=self._snake_case_conversion,
env_prefix=self.env_prefix,
)
def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]:
if self._snake_case_conversion:
field_info = list((x[0], x[1], x[2]) for x in super()._extract_field_info(field, field_name))
return field_info
if self._dash_to_underscore:
return list((x[0], x[1].replace('_', '-'), x[2]) for x in super()._extract_field_info(field, field_name))
return super()._extract_field_info(field, field_name)
def __repr__(self) -> str:
return f'{self.__class__.__name__}(url={self._url!r}, env_nested_delimiter={self.env_nested_delimiter!r})'
__all__ = ['AzureKeyVaultMapping', 'AzureKeyVaultSettingsSource']

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,193 @@
"""Dotenv file settings source."""
from __future__ import annotations as _annotations
import os
import warnings
from collections.abc import Mapping
from pathlib import Path
from typing import TYPE_CHECKING, Any
from dotenv import dotenv_values
from pydantic._internal._typing_extra import ( # type: ignore[attr-defined]
get_origin,
)
from typing_inspection.introspection import is_union_origin
from ..types import ENV_FILE_SENTINEL, DotenvFiltering, DotenvType, EnvPrefixTarget
from ..utils import (
_annotation_is_complex,
_union_is_complex,
parse_env_vars,
)
from .env import EnvSettingsSource
if TYPE_CHECKING:
from pydantic_settings.main import BaseSettings
class DotEnvSettingsSource(EnvSettingsSource):
"""
Source class for loading settings values from env files.
"""
def __init__(
self,
settings_cls: type[BaseSettings],
env_file: DotenvType | None = ENV_FILE_SENTINEL,
env_file_encoding: str | None = None,
dotenv_filtering: DotenvFiltering | None = None,
case_sensitive: bool | None = None,
env_prefix: str | None = None,
env_prefix_target: EnvPrefixTarget | None = None,
env_nested_delimiter: str | None = None,
env_nested_max_split: int | None = None,
env_ignore_empty: bool | None = None,
env_parse_none_str: str | None = None,
env_parse_enums: bool | None = None,
) -> None:
self.env_file = env_file if env_file != ENV_FILE_SENTINEL else settings_cls.model_config.get('env_file')
self.env_file_encoding = (
env_file_encoding if env_file_encoding is not None else settings_cls.model_config.get('env_file_encoding')
)
self.dotenv_filtering = (
dotenv_filtering if dotenv_filtering is not None else settings_cls.model_config.get('dotenv_filtering')
)
super().__init__(
settings_cls,
case_sensitive,
env_prefix,
env_prefix_target,
env_nested_delimiter,
env_nested_max_split,
env_ignore_empty,
env_parse_none_str,
env_parse_enums,
)
def _load_env_vars(self) -> Mapping[str, str | None]:
return self._read_env_files()
@staticmethod
def _static_read_env_file(
file_path: Path,
*,
encoding: str | None = None,
case_sensitive: bool = False,
ignore_empty: bool = False,
parse_none_str: str | None = None,
) -> Mapping[str, str | None]:
file_vars: dict[str, str | None] = dotenv_values(file_path, encoding=encoding or 'utf8')
return parse_env_vars(file_vars, case_sensitive, ignore_empty, parse_none_str)
def _read_env_file(
self,
file_path: Path,
) -> Mapping[str, str | None]:
return self._static_read_env_file(
file_path,
encoding=self.env_file_encoding,
case_sensitive=self.case_sensitive,
ignore_empty=self.env_ignore_empty,
parse_none_str=self.env_parse_none_str,
)
def _read_env_files(self) -> Mapping[str, str | None]:
env_files = self.env_file
if env_files is None:
return {}
if isinstance(env_files, (str, os.PathLike)):
env_files = [env_files]
dotenv_vars: dict[str, str | None] = {}
for env_file in env_files:
env_path = Path(env_file).expanduser()
if env_path.is_file() or env_path.is_fifo():
dotenv_vars.update(self._read_env_file(env_path))
return dotenv_vars
def __call__(self) -> dict[str, Any]: # noqa: C901
data: dict[str, Any] = super().__call__()
if self.dotenv_filtering == 'only_existing':
# This case behaves like the EnvSettingsSource, only return existing fields
return data
if self.dotenv_filtering == 'match_prefix':
# In this case add all env vars that match the prefix, stripping the prefix.
prefix = self._apply_case_sensitive(self.env_prefix)
for env_name, env_value in self.env_vars.items():
if env_name.startswith(prefix):
normalized_env_name = env_name[len(self.env_prefix) :]
if (
self.env_nested_delimiter
and self.env_nested_delimiter in normalized_env_name
and normalized_env_name.partition(self.env_nested_delimiter)[0] in data
):
continue
if normalized_env_name not in data:
data[normalized_env_name] = env_value
return data
is_extra_allowed = self.config.get('extra') != 'forbid'
# As `extra` config is allowed in dotenv settings source, We have to
# update data with extra env variables from dotenv file.
for env_name, env_value in self.env_vars.items():
if not env_value or env_name in data or (self.env_prefix and env_name in self.settings_cls.model_fields):
continue
env_used = False
for field_name, field in self.settings_cls.model_fields.items():
for _, field_env_name, _ in self._extract_field_info(field, field_name):
if env_name == field_env_name or (
(
_annotation_is_complex(field.annotation, field.metadata)
or (
is_union_origin(get_origin(field.annotation))
and _union_is_complex(field.annotation, field.metadata)
)
)
and env_name.startswith(field_env_name)
):
env_used = True
break
if env_used:
break
if not env_used:
if is_extra_allowed and env_name.startswith(self.env_prefix):
# env_prefix should be respected and removed from the env_name
normalized_env_name = env_name[len(self.env_prefix) :]
data[normalized_env_name] = env_value
else:
data[env_name] = env_value
return data
def __repr__(self) -> str:
return (
f'{self.__class__.__name__}(env_file={self.env_file!r}, env_file_encoding={self.env_file_encoding!r}, '
f'env_nested_delimiter={self.env_nested_delimiter!r}, env_prefix_len={self.env_prefix_len!r})'
)
def read_env_file(
file_path: Path,
*,
encoding: str | None = None,
case_sensitive: bool = False,
ignore_empty: bool = False,
parse_none_str: str | None = None,
) -> Mapping[str, str | None]:
warnings.warn(
'read_env_file will be removed in the next version, use DotEnvSettingsSource._static_read_env_file if you must',
DeprecationWarning,
)
return DotEnvSettingsSource._static_read_env_file(
file_path,
encoding=encoding,
case_sensitive=case_sensitive,
ignore_empty=ignore_empty,
parse_none_str=parse_none_str,
)
__all__ = ['DotEnvSettingsSource', 'read_env_file']