diff --git a/docs/index.md b/docs/index.md index f5ddb2ec..dd668ac1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2457,6 +2457,62 @@ For nested models, Secret Manager supports the `env_nested_delimiter` setting as For more details on creating and managing secrets in Google Cloud Secret Manager, see the [official Google Cloud documentation](https://cloud.google.com/secret-manager/docs). +## Keyring + +This integration allows you to securely load sensitive values such as passwords, API tokens, and other secrets directly from the system keyring. Instead of storing secrets in .env files or configuration files, you can fetch them at runtime using the operating system's secure credential store. + +The Keyring integration requires additional dependencies: + +```bash +pip install "pydantic-settings[keyring]" +``` + +There is one mandatory parameter: +- `keyring_service`: keyring service name + +Add `KeyringConfigSettingsSource` to your settings sources: + +```python +from pydantic import SecretStr +from pydantic_settings import ( + BaseSettings, + KeyringConfigSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, +) + +class Settings(BaseSettings): + secret_key: SecretStr + + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + keyring_settings = KeyringConfigSettingsSource( + settings_cls, + keyring_service=os.environ["KEYRING_SERVICE"], + ) + + return ( + init_settings, + env_settings, + dotenv_settings, + file_secret_settings, + keyring_settings, + ) +``` + +To set a secret, you can use `keyring set ` in your terminal: + +```bash +$ keyring set myapp secret_key +``` + ## Other settings source Other settings sources are available for common configuration files: diff --git a/pydantic_settings/__init__.py b/pydantic_settings/__init__.py index 21124260..413868cf 100644 --- a/pydantic_settings/__init__.py +++ b/pydantic_settings/__init__.py @@ -20,6 +20,7 @@ GoogleSecretManagerSettingsSource, InitSettingsSource, JsonConfigSettingsSource, + KeyringConfigSettingsSource, NestedSecretsSettingsSource, NoDecode, PydanticBaseSettingsSource, @@ -53,6 +54,7 @@ 'GoogleSecretManagerSettingsSource', 'InitSettingsSource', 'JsonConfigSettingsSource', + 'KeyringConfigSettingsSource', 'NestedSecretsSettingsSource', 'NoDecode', 'PydanticBaseSettingsSource', diff --git a/pydantic_settings/sources/__init__.py b/pydantic_settings/sources/__init__.py index 3a557f7c..5098fca3 100644 --- a/pydantic_settings/sources/__init__.py +++ b/pydantic_settings/sources/__init__.py @@ -27,6 +27,7 @@ from .providers.env import EnvSettingsSource from .providers.gcp import GoogleSecretManagerSettingsSource from .providers.json import JsonConfigSettingsSource +from .providers.keyring import KeyringConfigSettingsSource from .providers.nested_secrets import NestedSecretsSettingsSource from .providers.pyproject import PyprojectTomlConfigSettingsSource from .providers.secrets import SecretsSettingsSource @@ -58,6 +59,7 @@ 'GoogleSecretManagerSettingsSource', 'InitSettingsSource', 'JsonConfigSettingsSource', + 'KeyringConfigSettingsSource', 'NestedSecretsSettingsSource', 'NoDecode', 'PathType', diff --git a/pydantic_settings/sources/providers/__init__.py b/pydantic_settings/sources/providers/__init__.py index 9be7a546..3dccb988 100644 --- a/pydantic_settings/sources/providers/__init__.py +++ b/pydantic_settings/sources/providers/__init__.py @@ -17,6 +17,7 @@ from .env import EnvSettingsSource from .gcp import GoogleSecretManagerSettingsSource from .json import JsonConfigSettingsSource +from .keyring import KeyringConfigSettingsSource from .pyproject import PyprojectTomlConfigSettingsSource from .secrets import SecretsSettingsSource from .toml import TomlConfigSettingsSource @@ -38,6 +39,7 @@ 'EnvSettingsSource', 'GoogleSecretManagerSettingsSource', 'JsonConfigSettingsSource', + 'KeyringConfigSettingsSource', 'PyprojectTomlConfigSettingsSource', 'SecretsSettingsSource', 'TomlConfigSettingsSource', diff --git a/pydantic_settings/sources/providers/keyring.py b/pydantic_settings/sources/providers/keyring.py new file mode 100644 index 00000000..3d1078dc --- /dev/null +++ b/pydantic_settings/sources/providers/keyring.py @@ -0,0 +1,62 @@ +from typing import TYPE_CHECKING, Mapping + +from pydantic_settings import BaseSettings, EnvSettingsSource + +if TYPE_CHECKING: + import keyring +else: + keyring = None + +def import_keyring(): + global keyring + if keyring is not None: + return + try: + import keyring + + return + except ImportError as e: + raise ImportError("Keyring is not installed, run `pip install keyring`") from e + + +class KeyringConfigSettingsSource(EnvSettingsSource): + def __init__( + self, + settings_cls: type[BaseSettings], + keyring_service: str, + case_sensitive: bool | None = None, + env_prefix: str | None = None, + ): + self.keyring_service = ( + keyring_service if case_sensitive else keyring_service.lower() + ) + super().__init__( + settings_cls, case_sensitive=case_sensitive, env_prefix=env_prefix + ) + + def _load_env_vars(self) -> Mapping[str, str | None]: + import_keyring() + + prefix = self.env_prefix + if not self.case_sensitive: + prefix = self.env_prefix.lower() + env_vars = {} + for field in self.settings_cls.model_fields.keys(): + if not self.case_sensitive: + field = field.lower() + credential = keyring.get_credential(self.keyring_service, prefix + field) + if credential is not None: + key = credential.username + if not self.case_sensitive: + key = key.lower() + env_vars[key] = credential.password + + return env_vars + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(keyring_service={self.keyring_service})" + + +__all__ = [ + "KeyringConfigSettingsSource", +] diff --git a/pyproject.toml b/pyproject.toml index f473b312..e0644fcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ aws-secrets-manager = ["boto3>=1.35.0", "boto3-stubs[secretsmanager]"] gcp-secret-manager = [ "google-cloud-secret-manager>=2.23.1", ] +keyring = ["keyring>=25.7.0"] [project.urls] Homepage = 'https://github.com/pydantic/pydantic-settings'