Skip to content
56 changes: 56 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2460,6 +2460,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 <keyring_service> <field_name>` in your terminal:

```bash
$ keyring set myapp secret_key
```

## Other settings source

Other settings sources are available for common configuration files:
Expand Down
2 changes: 2 additions & 0 deletions pydantic_settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
GoogleSecretManagerSettingsSource,
InitSettingsSource,
JsonConfigSettingsSource,
KeyringConfigSettingsSource,
NestedSecretsSettingsSource,
NoDecode,
PydanticBaseSettingsSource,
Expand Down Expand Up @@ -53,6 +54,7 @@
'GoogleSecretManagerSettingsSource',
'InitSettingsSource',
'JsonConfigSettingsSource',
'KeyringConfigSettingsSource',
'NestedSecretsSettingsSource',
'NoDecode',
'PydanticBaseSettingsSource',
Expand Down
2 changes: 2 additions & 0 deletions pydantic_settings/sources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -58,6 +59,7 @@
'GoogleSecretManagerSettingsSource',
'InitSettingsSource',
'JsonConfigSettingsSource',
'KeyringConfigSettingsSource',
'NestedSecretsSettingsSource',
'NoDecode',
'PathType',
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 @@ -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
Expand All @@ -38,6 +39,7 @@
'EnvSettingsSource',
'GoogleSecretManagerSettingsSource',
'JsonConfigSettingsSource',
'KeyringConfigSettingsSource',
'PyprojectTomlConfigSettingsSource',
'SecretsSettingsSource',
'TomlConfigSettingsSource',
Expand Down
62 changes: 62 additions & 0 deletions pydantic_settings/sources/providers/keyring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from collections.abc import Mapping
from typing import TYPE_CHECKING

from .env import EnvSettingsSource

if TYPE_CHECKING:
import keyring

from pydantic_settings import BaseSettings
else:
keyring = None


def import_keyring() -> None:
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: dict[str, str | None] = {}
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',
]
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading