Skip to main content

Use file secrets in nested pydantic-settings models instead of built-in SecretsSettingsSource

Project description

pydantic-file-secrets ๐Ÿ”‘

Pydantic v2

Use file secrets in nested pydantic-settings models instead of built-in SecretsSettingsSource

license pypi python versions tests coverage tested with multipython uses docsub mypy uv ruff openssf best practices

pypi downloads

This project is inspired by discussions in Pydantic Settings repository and proposes solution to #30 and #154.

Features

  • Plain or nested directory layout: secrets/dir__key or secrets/dir/key
  • Respects env_prefix, env_nested_delimiter and other config options
  • Implements config options secrets_prefix, secrets_nested_delimiter and more to configure secrets and env vars independently
  • Drop-in replacement of standard SecretsSettingsSource
  • Pure Python thin wrapper over standard EnvSettingsSource
  • No third party dependencies except pydantic-settings
  • Fully typed
  • 100% test coverage

Installation

$ pip install pydantic-file-secrets

Motivation

Nested Pydantic config can contain nested models with secret entries, as well as secrets in top level config. In dockerized environment, these entries may be read from file system, e.g. /run/secrets when using Docker Secrets:

from pydantic import BaseModel, Secret
from pydantic_settings import BaseSettings, SettingsConfigDict

class DbSettings(BaseModel):
    user: str
    passwd: Secret[str]  # secret in nested model

class Settings(BaseSettings):
    app_key: Secret[str]  # secret in root model
    db: DbSettings

    model_config = SettingsConfigDict(
        secrets_dir='/run/secrets',
    )

Usage

Plain secrets directory layout

๐Ÿ“‚ secrets
โ”œโ”€โ”€ ๐Ÿ“„ app_key
โ””โ”€โ”€ ๐Ÿ“„ db__passwd
from pydantic import BaseModel, SecretStr
from pydantic_file_secrets import FileSecretsSettingsSource, SettingsConfigDict
from pydantic_settings import BaseSettings
from pydantic_settings.sources import PydanticBaseSettingsSource


class DbSettings(BaseModel):
    passwd: SecretStr


class Settings(BaseSettings):
    app_key: SecretStr
    db: DbSettings

    model_config = SettingsConfigDict(
        secrets_dir='secrets',
        secrets_nested_delimiter='__',
    )

    @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, ...]:
        return (
            init_settings,
            env_settings,
            dotenv_settings,
            FileSecretsSettingsSource(file_secret_settings),
        )
>>> Settings().model_dump()
{'app_key': SecretStr('**********'), 'db': {'passwd': SecretStr('**********')}}

Nested secrets directory layout

Config option secrets_nested_delimiter overrides env_nested_delimiter for files. In particular, this allows to use nested directory layout along with environmemt variables for other non-secret settings:

๐Ÿ“‚ secrets
โ”œโ”€โ”€ ๐Ÿ“„ app_key
โ””โ”€โ”€ ๐Ÿ“‚ db
    โ””โ”€โ”€ ๐Ÿ“„ passwd
    model_config = SettingsConfigDict(
        secrets_dir='secrets',
        secrets_nested_subdir=True,
    )
>>> Settings().model_dump()
{'app_key': SecretStr('**********'), 'db': {'passwd': SecretStr('**********')}}

Multiple secrets_dir

๐Ÿ“‚ secrets
โ”œโ”€โ”€ ๐Ÿ“‚ layer1
โ”‚   โ””โ”€โ”€ ๐Ÿ“„ app_key
โ””โ”€โ”€ ๐Ÿ“‚ layer2
    โ””โ”€โ”€ ๐Ÿ“„ db__passwd
    model_config = SettingsConfigDict(
        secrets_dir=['secrets/layer1', 'secrets/layer2'],
        secrets_nested_delimiter='__',
    )
>>> Settings().model_dump()
{'app_key': SecretStr('**********'), 'db': {'passwd': SecretStr('**********')}}

Experimental syntactic sugar ๐Ÿงช

[!CAUTION] This syntax may change at any time. Pin current pydantic-file-secrets version if decided to use it.

Few important things to note:

  • @with_builtin_sources decorator enables NamedTuple argument src: BuiltinSources encapsulating default builtins settings sources
  • BaseSource alias is shorter than PydanticBaseSettingsSource and is easier to use in type hints
  • settings_cls was removed from settings_customise_sources signature: cls seems to be sufficient
from pydantic import BaseModel, SecretStr
from pydantic_file_secrets import (
    BaseSource,
    BuiltinSources,
    FileSecretsSettingsSource,
    SettingsConfigDict,
    with_builtin_sources,
)
from pydantic_settings import BaseSettings


class DbSettings(BaseModel):
    passwd: SecretStr


class Settings(BaseSettings):
    app_key: SecretStr
    db: DbSettings

    model_config = SettingsConfigDict(
        secrets_dir='secrets',
        secrets_nested_delimiter='__',
    )

    @classmethod
    @with_builtin_sources
    def settings_customise_sources(cls, src: BuiltinSources) -> tuple[BaseSource, ...]:
        return (
            src.init_settings,
            src.env_settings,
            src.dotenv_settings,
            FileSecretsSettingsSource(src.file_secret_settings),
        )
>>> Settings().model_dump()
{'app_key': SecretStr('**********'), 'db': {'passwd': SecretStr('**********')}}

Configuration options

secrets_dir

Path to secrets directory. Same as SecretsSettingsSource.secrets_dir if str or Path. If list, the last match wins. If secrets_dir is passed in both source constructor and model config, values are not merged (constructor takes priority).

secrets_dir_missing

If secrets_dir does not exist, original SecretsSettingsSource issues a warning. However, this may be undesirable, for example if we don't mount Docker Secrets in e.g. dev environment. Now you have a choice:

  • 'ok' โ€” do nothing if secrets_dir does not exist
  • 'warn' (default) โ€” print warning, same as SecretsSettingsSource
  • 'error' โ€” raise SettingsError

If multiple secrets_dir passed, the same secrets_dir_missing action applies to each of them.

secrets_dir_max_size

Limit the size of secrets_dir for security reasons, defaults to SECRETS_DIR_MAX_SIZE equal to 16 MiB.

FileSecretsSettingsSource is a thin wrapper around EnvSettingsSource, which loads all potential secrets on initialization. This could lead to MemoryError if we mount a large file under secrets_dir.

If multiple secrets_dir passed, the limit applies to each directory independently.

secrets_case_sensitive

Same as case_sensitive, but works for secrets only. If not specified, defaults to case_sensitive.

secrets_nested_delimiter

Same as env_nested_delimiter, but works for secrets only. If not specified, defaults to env_nested_delimiter. This option is used to implement nested secrets directory layout and allows to do even nastier things like /run/secrets/model/delim/nested1/delim/nested2.

secrets_nested_subdir

Boolean flag to turn on nested secrets directory mode, False by default. If True, sets secrets_nested_delimiter to os.sep. Raises SettingsError if secrets_nested_delimiter is already specified.

secrets_prefix

Secret path prefix, similar to env_prefix, but works for secrets only. Defaults to env_prefix if not specified. Works in both plain and nested directory modes, like '/run/secrets/prefix_model__nested' and '/run/secrets/prefix_model/nested'.

Not supported config options

Some config options that are declared in SecretsSettingsSource interface are actually not working and are not supported in FileSecretsSettingsSource:

  • env_ignore_empty
  • env_parse_none_str
  • env_parse_enums

However, we make sure that the behaviour of FileSecretsSettingsSource matches SecretsSettingsSource to provide a drop-in replacement, although it is somewhat wierd (e.g. env_parse_enums is always True).

Testing

100% test coverage is provided for latest Python and pydantic-settings version. Tests are run for all minor pydantic-settings v2 versions and all minor Python 3 versions supported by them:

  • pyXY โ€” Python 3.{8,9,10,11,12,13}
  • psXY โ€” pydantic-settings v2.{2,3,4,5,6,7,8}
ps29 ps28 ps27 ps26 ps25 ps24 ps23 ps22
py313 โœณ๏ธ โœณ๏ธ โœ… โœ… โœ… โ˜‘๏ธ โœณ๏ธ โ˜‘๏ธ
py312 โœ… โœ… โœ… โœ… โœ… โ˜‘๏ธ โ˜‘๏ธ โ˜‘๏ธ
py311 โœ… โœ… โœ… โœ… โœ… โ˜‘๏ธ โ˜‘๏ธ โ˜‘๏ธ
py310 โœ… โœ… โœ… โœ… โœ… โ˜‘๏ธ โ˜‘๏ธ โ˜‘๏ธ
py39 โœ… โœ… โœ… โœ… โœ… โ˜‘๏ธ โ˜‘๏ธ โ˜‘๏ธ
py38 โœ… โœ… โœ… โœ… โœ… โ˜‘๏ธ โ˜‘๏ธ โ˜‘๏ธ
  • โœณ๏ธ pytest and mypy passing, coverage report generated
  • โœ… pytest and mypy passing
  • โ˜‘๏ธ pytest passing, mypy not attempted
  • โŒ tests failing or not attempted

History

Contributing

Pull requests, feature requests, and bug reports are welcome!

Authors

  • Michael Makukha

See also

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_file_secrets-0.4.3.tar.gz (99.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_file_secrets-0.4.3-py3-none-any.whl (9.9 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_file_secrets-0.4.3.tar.gz.

File metadata

File hashes

Hashes for pydantic_file_secrets-0.4.3.tar.gz
Algorithm Hash digest
SHA256 3beecbadcd774be9a8c859eb4b3cdf3c91444fb292fe433b3504d1544442b2e7
MD5 d3b23bdf8ac7cc35663c894d1e20f9e8
BLAKE2b-256 58c2b9be53d256fef5c26282cd1e661b0ed1c640c9baba8dcbacc3c99fffabf5

See more details on using hashes here.

File details

Details for the file pydantic_file_secrets-0.4.3-py3-none-any.whl.

File metadata

File hashes

Hashes for pydantic_file_secrets-0.4.3-py3-none-any.whl
Algorithm Hash digest
SHA256 6450e25c4f107e7d1d0efafa4cf4e47cb956a90d1a13601c460c006b3b06abe9
MD5 8c9171df6798f5dd6a8cb40d981d3930
BLAKE2b-256 b86a48531a73882071ee028d0ba48d4dd888c991d2a4d4f3564fea56b9c47954

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page