Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ class Settings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return (MyCustomSource(settings_cls),)

Expand Down Expand Up @@ -749,6 +750,7 @@ class Settings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return env_settings, CliSettingsSource(settings_cls, cli_parse_args=True)

Expand Down Expand Up @@ -1817,6 +1819,7 @@ class AWSSecretsManagerSettings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
aws_secrets_manager_settings = AWSSecretsManagerSettingsSource(
settings_cls,
Expand Down Expand Up @@ -1920,6 +1923,7 @@ class AzureKeyVaultSettings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
az_key_vault_settings = AzureKeyVaultSettingsSource(
settings_cls,
Expand Down Expand Up @@ -1983,6 +1987,7 @@ To use Google Cloud Secret Manager, you need to:
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
# Create the GCP Secret Manager settings source
gcp_settings = GoogleSecretManagerSettingsSource(
Expand All @@ -2000,6 +2005,7 @@ To use Google Cloud Secret Manager, you need to:
env_settings,
dotenv_settings,
file_secret_settings,
loadcredential_settings,
gcp_settings,
)
```
Expand Down Expand Up @@ -2072,6 +2078,7 @@ class Settings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return (TomlConfigSettingsSource(settings_cls),)
```
Expand Down Expand Up @@ -2115,6 +2122,7 @@ class Settings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return (PyprojectTomlConfigSettingsSource(settings_cls),)

Expand Down Expand Up @@ -2177,6 +2185,7 @@ class DiscoverSettings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return (PyprojectTomlConfigSettingsSource(settings_cls),)

Expand All @@ -2194,6 +2203,7 @@ class ExplicitFilePathSettings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return (
PyprojectTomlConfigSettingsSource(
Expand Down Expand Up @@ -2245,8 +2255,14 @@ class Settings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return env_settings, init_settings, file_secret_settings
return (
env_settings,
init_settings,
file_secret_settings,
loadcredential_settings,
)


print(Settings(database_dsn='postgres://postgres@localhost:5432/kwargs_db'))
Expand Down Expand Up @@ -2327,12 +2343,14 @@ class Settings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return (
init_settings,
JsonConfigSettingsSource(settings_cls),
env_settings,
file_secret_settings,
loadcredential_settings,
)


Expand Down Expand Up @@ -2391,9 +2409,10 @@ class Settings(BaseSettings):
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
# here we choose to ignore arguments from init_settings
return env_settings, file_secret_settings
return env_settings, file_secret_settings, loadcredential_settings


try:
Expand Down
10 changes: 9 additions & 1 deletion pydantic_settings/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from pydantic.dataclasses import is_pydantic_dataclass
from pydantic.main import BaseModel

from pydantic_settings.sources.providers.loadcredential import LoadCredentialSettingsSource

from .exceptions import SettingsError
from .sources import (
ENV_FILE_SENTINEL,
Expand Down Expand Up @@ -220,6 +222,7 @@ def settings_customise_sources(
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
loadcredential_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
"""
Define the sources and their order for loading the settings values.
Expand All @@ -234,7 +237,7 @@ def settings_customise_sources(
Returns:
A tuple containing the sources and their order for loading the settings values.
"""
return init_settings, env_settings, dotenv_settings, file_secret_settings
return init_settings, env_settings, dotenv_settings, file_secret_settings, loadcredential_settings

def _settings_build_values(
self,
Expand Down Expand Up @@ -374,13 +377,18 @@ def _settings_build_values(
file_secret_settings = SecretsSettingsSource(
self.__class__, secrets_dir=secrets_dir, case_sensitive=case_sensitive, env_prefix=env_prefix
)

loadcredential_settings = LoadCredentialSettingsSource(
self.__class__, case_sensitive=case_sensitive, env_prefix=env_prefix
)
# Provide a hook to set built-in sources priority and add / remove sources
sources = self.settings_customise_sources(
self.__class__,
init_settings=init_settings,
env_settings=env_settings,
dotenv_settings=dotenv_settings,
file_secret_settings=file_secret_settings,
loadcredential_settings=loadcredential_settings,
) + (default_settings,)
if not any([source for source in sources if isinstance(source, CliSettingsSource)]):
if isinstance(cli_settings_source, CliSettingsSource):
Expand Down
2 changes: 2 additions & 0 deletions pydantic_settings/sources/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .env import EnvSettingsSource
from .gcp import GoogleSecretManagerSettingsSource
from .json import JsonConfigSettingsSource
from .loadcredential import LoadCredentialSettingsSource
from .pyproject import PyprojectTomlConfigSettingsSource
from .secrets import SecretsSettingsSource
from .toml import TomlConfigSettingsSource
Expand All @@ -34,6 +35,7 @@
'EnvSettingsSource',
'GoogleSecretManagerSettingsSource',
'JsonConfigSettingsSource',
'LoadCredentialSettingsSource',
'PyprojectTomlConfigSettingsSource',
'SecretsSettingsSource',
'TomlConfigSettingsSource',
Expand Down
118 changes: 118 additions & 0 deletions pydantic_settings/sources/providers/loadcredential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""LoadCredential settings source"""

from __future__ import annotations as _annotations

import os
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Any

from pydantic.fields import FieldInfo

from pydantic_settings.utils import path_type_label

from ...exceptions import SettingsError
from ..base import PydanticBaseEnvSettingsSource

if TYPE_CHECKING:
from pydantic_settings.main import BaseSettings


class LoadCredentialSettingsSource(PydanticBaseEnvSettingsSource):
"""
Source class for loading settings values from secret files.
"""

def __init__(
self,
settings_cls: type[BaseSettings],
case_sensitive: bool | None = None,
env_prefix: str | None = None,
env_ignore_empty: bool | None = None,
env_parse_none_str: str | None = None,
env_parse_enums: bool | None = None,
) -> None:
super().__init__(
settings_cls,
case_sensitive,
env_prefix,
env_ignore_empty,
env_parse_none_str,
env_parse_enums,
)
self.secrets_dir = os.environ.get('CREDENTIALS_DIRECTORY')

def __call__(self) -> dict[str, Any]:
"""
Build fields from "secrets" files.
"""
secrets: dict[str, str | None] = {}

if self.secrets_dir is None:
return secrets

self.secrets_path = Path(self.secrets_dir)

if not self.secrets_path.exists():
warnings.warn(f'directory "{self.secrets_path}" does not exist')
return secrets

if not self.secrets_path.is_dir():
raise SettingsError(
f'secrets_dir must reference a directory, not a {path_type_label(self.secrets_path)}'
)

return super().__call__()

@classmethod
def find_case_path(
cls, dir_path: Path, file_name: str, case_sensitive: bool
) -> Path | None:
"""
Find a file within path's directory matching filename, optionally ignoring case.

Args:
dir_path: Directory path.
file_name: File name.
case_sensitive: Whether to search for file name case sensitively.

Returns:
Whether file path or `None` if file does not exist in directory.
"""
for f in dir_path.iterdir():
if f.name == file_name:
return f
elif not case_sensitive and f.name.lower() == file_name.lower():
return f
return None

def get_field_value(
self, field: FieldInfo, field_name: str
) -> tuple[Any, str, bool]:
"""
Gets the value for field from credentials and a flag to determine whether value is complex.

Args:
field: The field.
field_name: The field name.

Returns:
A tuple that contains the value (`None` if the credential does not exist), key, and
a flag to determine whether value is complex.
"""

for field_key, env_name, value_is_complex in self._extract_field_info(
field, field_name
):
path = self.find_case_path(self.secrets_path, env_name, self.case_sensitive)

if path:
if path.is_file():
return path.read_text().strip(), field_key, value_is_complex
else:
warnings.warn(
f'attempted to load credential "{path}" but found a {path_type_label(path)} instead of a file.',
stacklevel=4,
)

return None, field_key, value_is_complex
Loading
Loading