Skip to content

Commit

Permalink
Merge pull request #6 from makukha/1-make-initial-release
Browse files Browse the repository at this point in the history
Initial release
  • Loading branch information
makukha authored Aug 15, 2024
2 parents 934b20e + 9d03339 commit c028f2a
Show file tree
Hide file tree
Showing 26 changed files with 1,964 additions and 2 deletions.
8 changes: 8 additions & 0 deletions .bumpversion.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[tool.bumpversion]
current_version = "0.1.0a1"
allow_dirty = true
files = [
{filename = "src/pydantic_file_secrets/__version__.py"},
{filename = "pyproject.toml"},
{filename = "README.md"},
]
11 changes: 11 additions & 0 deletions .dev/Brewfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# dev core
brew "keyring"
brew "pdm"
brew "pyenv"
brew "ruff"
brew "scorecard"
brew "tox"

# dev ux
cask "alacritty"
cask "font-jetbrains-mono-nerd-font"
7 changes: 7 additions & 0 deletions .dev/alacritty.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh

alacritty --hold \
--config-file alacritty.toml \
--working-directory $(pwd)/.. \
--title $(basename $(pwd)/..) \
&
11 changes: 11 additions & 0 deletions .dev/alacritty.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[window]
decorations = "Buttonless"
dynamic_title = false
dimensions = { columns = 120, lines = 120 }
position = { x = 0, y = 0 }
padding = { x = 10, y = 10 }
opacity = 0.9

[font]
normal = { family = "JetBrainsMono Nerd Font Mono", style = "Regular" }
size = 12
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
root = true

[*]
indent_style = space
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{py,toml}]
charset = utf-8
indent_size = 4

[*.md]
trim_trailing_whitespace = false

[*.{ini,yml}]
indent_size = 2
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Brewfile.lock.json
dist/
*.egg-info/
.idea/
.pdm-build/
.pdm-python
__pycache__/
.task/
tmp/
.tox/
.venv/

# OS generated
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12.5
42 changes: 42 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
exclude = [
".git",
".pdm-build",
".tox",
".venv",
"dist",
"tmp",
]
# Same as Black.
line-length = 88
indent-width = 4
# Assume Python 3.12
target-version = "py312"

[lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F", "S"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[lint.per-file-ignores]
"tests/**/*.py" = [
"ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
"FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
]
"tests/**/test_*.py" = [
"S101", # Asserts allowed in tests...
]

[format]
quote-style = "single"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = true
docstring-code-line-length = "dynamic"
135 changes: 133 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,133 @@
# pydantic-settings-subsecret
Pydantic settings source for secrets in nested submodels.
# pydantic-file-secrets 📁🔑
> Use file secrets in nested models of Pydantic Settings.
[![license](https://img.shields.io/github/license/makukha/pydantic-file-secrets.svg)](https://github.com/makukha/pydantic-file-secrets/blob/main/LICENSE)
[![Tests](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/0.1.0a1/docs/badge/tests.svg)](https://github.com/makukha/pydantic-file-secrets)
[![Coverage](https://raw.githubusercontent.com/makukha/pydantic-file-secrets/0.1.0a1/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.1.0a1)](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)


This package is inspired by and based on discussions in [pydantic-settings issue #154](https://github.com/pydantic/pydantic-settings/issues/154).


## Features

* Use secret file source in nested settings models
* Drop-in replacement of standard `SecretsSettingsSource`
* Plain or nested directory layout: `/run/secrets/dir__key` or `/run/secrets/dir/key`
* Respects `env_prefix`, `env_nested_delimiter` and other [config options](#configuration-options)
* Has `secrets_prefix`, `secrets_nested_delimiter`, [etc.](#configuration-options) to configure secrets and env vars separately
* 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 methods that ***do not*** work in original Pydantic Settings, see [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 directory layout

| file | content |
|-----------------------------|-----------|
| `/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 (
env_settings,
init_settings,
FileSecretsSettingsSource(settings_cls),
)

```

### Secrets in subdirectories

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:

| file | content |
|----------------------------|-----------|
| `/run/secrets/app_key` | `secret1` |
| `/run/secrets/db/password` | `secret2` |

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

## Configuration options

TODO


## Roadmap

* Support `_FILE` environment variables to set secret file name.
* Per-field secret file name override.


## Changelog
80 changes: 80 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
version: '3'

vars:
GITHUB_REPO: makukha/pydantic-file-secrets

tasks:

init:
desc: Initialize dev environment.
cmds:
- cd .dev && brew bundle
- task: init:python
- pyenv install --skip-existing $(pyenv local)
- pdm venv create $(pyenv prefix $(pyenv local))

init:python:
internal: true
sources:
- tox.ini
vars:
PYTHON_VERSIONS:
sh: tox --listenvs | sed -e 's/^py\([23]\)\.\{0,1\}\(.*\)-.*/\1.\2/' | sort -mu | tr '\n' ' '
cmds:
- cmd: pyenv install --skip-existing {{.ITEM}}
for: {var: PYTHON_VERSIONS}

install:
desc: Install dev python environment.
cmds:
- task: dep:lock
- pdm install --check --dev

# dependencies lock in PDM is slow, run only when pyproject.toml changes
dep:lock:
internal: true
sources:
- pyproject.toml
generates:
- pdm.lock
cmds:
- pdm lock

lint:
desc: Run linters and code formatters.
cmds:
- ruff check
- ruff format --check

test:
desc: Run tests.
deps: [install]
cmds:
- tox run-parallel --parallel-live

test:pdb:
desc: Run tests and open debugger on errors.
deps: [install]
cmds:
- pdm run pytest --pdb {{.CLI_ARGS}}

version:
desc: Bump project version. Use task version -- patch|minor|major|...
cmds:
- pdm run bump-my-version bump {{.CLI_ARGS}}

publish:
desc: Publish package on PyPi.
preconditions:
- test $(git rev-parse --abbrev-ref HEAD) = main
cmds:
- pdm publish

scorecard:
desc: Update security scorecard
vars:
GITHUB_TOKEN: {sh: keyring get token.github.caseutil scorecard}
env:
GITHUB_TOKEN: '{{.GITHUB_TOKEN}}'
cmds:
- scorecard --repo={{.GITHUB_REPO}}
21 changes: 21 additions & 0 deletions docs/badge/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions docs/badge/tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit c028f2a

Please sign in to comment.