pydantic-file-secrets


Namepydantic-file-secrets JSON
Version 0.4.1 PyPI version JSON
download
home_pageNone
SummaryUse file secrets in nested Pydantic Settings models, drop-in replacement for SecretsSettingsSource.
upload_time2024-11-26 14:04:07
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseMIT
keywords python python3 pydantic pydantic-v2 pydantic-settings settings configuration config validation secrets docker-secret docker file-secrets
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # pydantic-file-secrets 🔑

> Use file secrets in nested [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) models, drop-in replacement for `SecretsSettingsSource`.

![GitHub License](https://img.shields.io/github/license/makukha/pydantic-file-secrets)
[![Tests](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/v0.4.1/docs/badge/tests.svg)](https://github.com/makukha/pydantic-file-secrets)
[![Coverage](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/v0.4.1/docs/badge/coverage.svg)](https://github.com/makukha/pydantic-file-secrets)
[![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v1.json)](https://github.com/astral-sh/ruff)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) \
[![pypi](https://img.shields.io/pypi/v/pydantic-file-secrets.svg#0.4.1)](https://pypi.python.org/pypi/pydantic-file-secrets)
[![versions](https://img.shields.io/pypi/pyversions/pydantic-file-secrets.svg)](https://pypi.org/project/pydantic-file-secrets)
[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) \
[![PyPI - Downloads](https://img.shields.io/pypi/dw/pydantic-file-secrets)](https://pypistats.org/packages/pydantic-file-secrets)


This project is inspired by discussions in Pydantic Settings and solves problems in issues [#30](https://github.com/pydantic/pydantic-settings/issues/30), [#154](https://github.com/pydantic/pydantic-settings/issues/154).


## Features

* Use secret file source in nested settings models
* Plain or nested directory layout: `/run/secrets/dir__key` or `/run/secrets/dir/key`
* Respects `env_prefix`, `env_nested_delimiter` and other [config options](https://github.com/makukha/pydantic-file-secrets?tab=readme-ov-file#configuration-options)
* Implements config options `secrets_prefix`, `secrets_nested_delimiter`, [etc.](https://github.com/makukha/pydantic-file-secrets?tab=readme-ov-file#configuration-options) to configure secrets and env vars independently
* Drop-in replacement of standard `SecretsSettingsSource`
* Can be used to monkey patch `SecretsSettingsSource`
* Pure Python thin wrapper over standard `EnvSettingsSource`
* No third party dependencies except `pydantic-settings`
* 100% test coverage


## 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:

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

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

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

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

Pydantic Settings has a corresponding data source, [`SecretsSettingsSource`](https://docs.pydantic.dev/latest/api/pydantic_settings/#pydantic_settings.SecretsSettingsSource), but it does not load secrets in nested models. For things that DO NOT work in original Pydantic Settings, see [test_pydantic_motivation.py](https://github.com/makukha/pydantic-file-secrets/blob/main/tests/test_pydantic_motivation.py).


## Solution

The new `FileSecretsSettingsSource` is a drop-in replacement of stock `SecretsSettingsSource`.

### Installation

```shell
$ pip install pydantic-file-secrets
```

### Plain secrets directory layout

```text
# /run/secrets/app_key
secret1

# /run/secrets/db__password
secret2
```

```python
from pydantic import BaseModel, Secret
from pydantic_file_secrets import FileSecretsSettingsSource
from pydantic_settings import BaseSettings, SettingsConfigDict

class DbSettings(BaseModel):
    user: str
    password: Secret[str]

class Settings(BaseSettings):
    db: DbSettings
    app_key: Secret[str]

    model_config = SettingsConfigDict(
        secrets_dir='/run/secrets',
        env_nested_delimiter='__',
    )
    
    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls,
        init_settings,
        env_settings,
        dotenv_settings,
        file_secret_settings,
    ):
        return (
            init_settings,
            env_settings,
            dotenv_settings,
            FileSecretsSettingsSource(file_secret_settings),
        )

```

### 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:

```text
# /run/secrets/app_key
secret1

# /run/secrets/db/password
secret2
```

```python
...
    model_config = SettingsConfigDict(
        secrets_dir='/run/secrets',
        secrets_nested_subdir=True,
    )
...
```

### Multiple `secrets_dir`

When passing `list` to `secrets_dir`, last match wins.

```python
...
    model_config = SettingsConfigDict(
        secrets_dir=['/run/configs/', '/run/secrets'],
    )
...
```

## 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 8 MiB.

`FileSecretsSettingsSource` is a thin wrapper around [`EnvSettingsSource`](https://docs.pydantic.dev/latest/api/pydantic_settings/#pydantic_settings.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`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#case-sensitivity), but works for secrets only. If not specified, defaults to `case_sensitive`.

### secrets_nested_delimiter

Same as [`env_nested_delimiter`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#parsing-environment-variable-values), but works for secrets only. If not specified, defaults to `env_nested_delimiter`. This option is used to implement [nested secrets directory layout](https://github.com/makukha/pydantic-file-secrets?tab=readme-ov-file#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`](https://docs.python.org/3/library/os.html#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`](https://docs.pydantic.dev/latest/api/pydantic_settings/#pydantic_settings.SecretsSettingsSource) interface are actually [not working](https://github.com/makukha/pydantic-file-secrets/blob/main/tests/test_pydantic_source.py) and are not supported in `FileSecretsSettingsSource`:

* `env_ignore_empty`
* `env_parse_none_str`
* `env_parse_enums`

However, we [make sure](https://github.com/makukha/pydantic-file-secrets/blob/main/tests/test_ignored_options.py) 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 ensured](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/main/tox.ini) for latest stable Python release (3.12).

Tests are run for all minor Pydantic Settings v2 versions and all minor Python 3 versions supported by Pydantic Settings:

* Python v. 3.{8,9,10,11,12,13}
* pydantic-settings v. 2.{2,3,4,5,6}


## History

* Several `secrets_dir` features were [ported](https://github.com/pydantic/pydantic-settings/releases/tag/v2.5.0) to `pydantic-settings` version 2.5.0


## Authors

* [Michael Makukha](https://github.com/makukha)

## License

[MIT License](https://github.com/makukha/pydantic-file-secrets/blob/main/LICENSE)

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "pydantic-file-secrets",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "python, python3, pydantic, pydantic-v2, pydantic-settings, settings, configuration, config, validation, secrets, docker-secret, docker, file-secrets",
    "author": null,
    "author_email": "Michael Makukha <m.makukha@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/67/c1/f0e5dbaa98d9aedbbf08dfe20185d6f7de5afba68acebc61eaa7a0079b02/pydantic_file_secrets-0.4.1.tar.gz",
    "platform": null,
    "description": "# pydantic-file-secrets \ud83d\udd11\n\n> Use file secrets in nested [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) models, drop-in replacement for `SecretsSettingsSource`.\n\n![GitHub License](https://img.shields.io/github/license/makukha/pydantic-file-secrets)\n[![Tests](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/v0.4.1/docs/badge/tests.svg)](https://github.com/makukha/pydantic-file-secrets)\n[![Coverage](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/v0.4.1/docs/badge/coverage.svg)](https://github.com/makukha/pydantic-file-secrets)\n[![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v1.json)](https://github.com/astral-sh/ruff)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) \\\n[![pypi](https://img.shields.io/pypi/v/pydantic-file-secrets.svg#0.4.1)](https://pypi.python.org/pypi/pydantic-file-secrets)\n[![versions](https://img.shields.io/pypi/pyversions/pydantic-file-secrets.svg)](https://pypi.org/project/pydantic-file-secrets)\n[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) \\\n[![PyPI - Downloads](https://img.shields.io/pypi/dw/pydantic-file-secrets)](https://pypistats.org/packages/pydantic-file-secrets)\n\n\nThis project is inspired by discussions in Pydantic Settings and solves problems in issues [#30](https://github.com/pydantic/pydantic-settings/issues/30), [#154](https://github.com/pydantic/pydantic-settings/issues/154).\n\n\n## Features\n\n* Use secret file source in nested settings models\n* Plain or nested directory layout: `/run/secrets/dir__key` or `/run/secrets/dir/key`\n* Respects `env_prefix`, `env_nested_delimiter` and other [config options](https://github.com/makukha/pydantic-file-secrets?tab=readme-ov-file#configuration-options)\n* Implements config options `secrets_prefix`, `secrets_nested_delimiter`, [etc.](https://github.com/makukha/pydantic-file-secrets?tab=readme-ov-file#configuration-options) to configure secrets and env vars independently\n* Drop-in replacement of standard `SecretsSettingsSource`\n* Can be used to monkey patch `SecretsSettingsSource`\n* Pure Python thin wrapper over standard `EnvSettingsSource`\n* No third party dependencies except `pydantic-settings`\n* 100% test coverage\n\n\n## Motivation\n\nNested 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:\n\n```python\nfrom pydantic import BaseModel, Secret\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nclass DbSettings(BaseModel):\n    user: str\n    password: Secret[str]  # secret in nested model\n\nclass Settings(BaseSettings):\n    db: DbSettings\n    app_key: Secret[str]  # secret in root config\n\n    model_config = SettingsConfigDict(\n        secrets_dir='/run/secrets',\n    )\n```\n\nPydantic Settings has a corresponding data source, [`SecretsSettingsSource`](https://docs.pydantic.dev/latest/api/pydantic_settings/#pydantic_settings.SecretsSettingsSource), but it does not load secrets in nested models. For things that DO NOT work in original Pydantic Settings, see [test_pydantic_motivation.py](https://github.com/makukha/pydantic-file-secrets/blob/main/tests/test_pydantic_motivation.py).\n\n\n## Solution\n\nThe new `FileSecretsSettingsSource` is a drop-in replacement of stock `SecretsSettingsSource`.\n\n### Installation\n\n```shell\n$ pip install pydantic-file-secrets\n```\n\n### Plain secrets directory layout\n\n```text\n# /run/secrets/app_key\nsecret1\n\n# /run/secrets/db__password\nsecret2\n```\n\n```python\nfrom pydantic import BaseModel, Secret\nfrom pydantic_file_secrets import FileSecretsSettingsSource\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nclass DbSettings(BaseModel):\n    user: str\n    password: Secret[str]\n\nclass Settings(BaseSettings):\n    db: DbSettings\n    app_key: Secret[str]\n\n    model_config = SettingsConfigDict(\n        secrets_dir='/run/secrets',\n        env_nested_delimiter='__',\n    )\n    \n    @classmethod\n    def settings_customise_sources(\n        cls,\n        settings_cls,\n        init_settings,\n        env_settings,\n        dotenv_settings,\n        file_secret_settings,\n    ):\n        return (\n            init_settings,\n            env_settings,\n            dotenv_settings,\n            FileSecretsSettingsSource(file_secret_settings),\n        )\n\n```\n\n### Nested secrets directory layout\n\nConfig 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:\n\n```text\n# /run/secrets/app_key\nsecret1\n\n# /run/secrets/db/password\nsecret2\n```\n\n```python\n...\n    model_config = SettingsConfigDict(\n        secrets_dir='/run/secrets',\n        secrets_nested_subdir=True,\n    )\n...\n```\n\n### Multiple `secrets_dir`\n\nWhen passing `list` to `secrets_dir`, last match wins.\n\n```python\n...\n    model_config = SettingsConfigDict(\n        secrets_dir=['/run/configs/', '/run/secrets'],\n    )\n...\n```\n\n## Configuration options\n\n### secrets_dir\n\nPath 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).\n\n### secrets_dir_missing\n\nIf `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:\n\n* `'ok'` \u2014 do nothing if `secrets_dir` does not exist\n* `'warn'` (default) \u2014 print warning, same as `SecretsSettingsSource`\n* `'error'` \u2014 raise `SettingsError`\n\nIf multiple `secrets_dir` passed, the same `secrets_dir_missing` action applies to each of them.\n\n### secrets_dir_max_size\n\nLimit the size of `secrets_dir` for security reasons, defaults to 8 MiB.\n\n`FileSecretsSettingsSource` is a thin wrapper around [`EnvSettingsSource`](https://docs.pydantic.dev/latest/api/pydantic_settings/#pydantic_settings.EnvSettingsSource), which loads all potential secrets on initialization. This could lead to `MemoryError` if we mount a large file under `secrets_dir`.\n\nIf multiple `secrets_dir` passed, the limit applies to each directory independently.\n\n### secrets_case_sensitive\n\nSame as [`case_sensitive`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#case-sensitivity), but works for secrets only. If not specified, defaults to `case_sensitive`.\n\n### secrets_nested_delimiter\n\nSame as [`env_nested_delimiter`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/#parsing-environment-variable-values), but works for secrets only. If not specified, defaults to `env_nested_delimiter`. This option is used to implement [nested secrets directory layout](https://github.com/makukha/pydantic-file-secrets?tab=readme-ov-file#nested-secrets-directory-layout) and allows to do even nastier things like `/run/secrets/model/delim/nested1/delim/nested2`.\n\n### secrets_nested_subdir\n\nBoolean flag to turn on nested secrets directory mode, `False` by default. If `True`, sets `secrets_nested_delimiter` to [`os.sep`](https://docs.python.org/3/library/os.html#os.sep). Raises `SettingsError` if `secrets_nested_delimiter` is already specified.\n\n### secrets_prefix\n\nSecret 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'`.\n\n\n### Not supported config options\n\nSome config options that are declared in [`SecretsSettingsSource`](https://docs.pydantic.dev/latest/api/pydantic_settings/#pydantic_settings.SecretsSettingsSource) interface are actually [not working](https://github.com/makukha/pydantic-file-secrets/blob/main/tests/test_pydantic_source.py) and are not supported in `FileSecretsSettingsSource`:\n\n* `env_ignore_empty`\n* `env_parse_none_str`\n* `env_parse_enums`\n\nHowever, we [make sure](https://github.com/makukha/pydantic-file-secrets/blob/main/tests/test_ignored_options.py) 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`).\n\n\n## Testing\n\n100% test coverage [is ensured](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/main/tox.ini) for latest stable Python release (3.12).\n\nTests are run for all minor Pydantic Settings v2 versions and all minor Python 3 versions supported by Pydantic Settings:\n\n* Python v. 3.{8,9,10,11,12,13}\n* pydantic-settings v. 2.{2,3,4,5,6}\n\n\n## History\n\n* Several `secrets_dir` features were [ported](https://github.com/pydantic/pydantic-settings/releases/tag/v2.5.0) to `pydantic-settings` version 2.5.0\n\n\n## Authors\n\n* [Michael Makukha](https://github.com/makukha)\n\n## License\n\n[MIT License](https://github.com/makukha/pydantic-file-secrets/blob/main/LICENSE)\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Use file secrets in nested Pydantic Settings models, drop-in replacement for SecretsSettingsSource. ",
    "version": "0.4.1",
    "project_urls": {
        "Changelog": "https://github.com/makukha/pydantic-file-secrets/releases",
        "Documentation": "https://github.com/makukha/pydantic-file-secrets#readme",
        "Issues": "https://github.com/makukha/pydantic-file-secrets/issues",
        "Source": "https://github.com/makukha/pydantic-file-secrets"
    },
    "split_keywords": [
        "python",
        " python3",
        " pydantic",
        " pydantic-v2",
        " pydantic-settings",
        " settings",
        " configuration",
        " config",
        " validation",
        " secrets",
        " docker-secret",
        " docker",
        " file-secrets"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "598d5f22e866826eca83ff1b38cbe0e4d5d35e9c9790672582b994805edc9c17",
                "md5": "2b148d799b82dd088dd480f680688ed8",
                "sha256": "df9f4654a05ce3f453e222933626b3b09c27c3f2e988ee943d412b4e8dd757ca"
            },
            "downloads": -1,
            "filename": "pydantic_file_secrets-0.4.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "2b148d799b82dd088dd480f680688ed8",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 7628,
            "upload_time": "2024-11-26T14:04:05",
            "upload_time_iso_8601": "2024-11-26T14:04:05.975915Z",
            "url": "https://files.pythonhosted.org/packages/59/8d/5f22e866826eca83ff1b38cbe0e4d5d35e9c9790672582b994805edc9c17/pydantic_file_secrets-0.4.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "67c1f0e5dbaa98d9aedbbf08dfe20185d6f7de5afba68acebc61eaa7a0079b02",
                "md5": "7cc847aaa3379e2499f8f3b51ed684ec",
                "sha256": "009fcb5b26f17487032856516d2838414a5aeefbf2d74b93f8b6af1d6049f6c3"
            },
            "downloads": -1,
            "filename": "pydantic_file_secrets-0.4.1.tar.gz",
            "has_sig": false,
            "md5_digest": "7cc847aaa3379e2499f8f3b51ed684ec",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 12589,
            "upload_time": "2024-11-26T14:04:07",
            "upload_time_iso_8601": "2024-11-26T14:04:07.436103Z",
            "url": "https://files.pythonhosted.org/packages/67/c1/f0e5dbaa98d9aedbbf08dfe20185d6f7de5afba68acebc61eaa7a0079b02/pydantic_file_secrets-0.4.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-11-26 14:04:07",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "makukha",
    "github_project": "pydantic-file-secrets",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "tox": true,
    "lcname": "pydantic-file-secrets"
}
        
Elapsed time: 0.55989s