Skip to content

Commit

Permalink
Add py.typed marker (#18)
Browse files Browse the repository at this point in the history
* Add py.typed marker
* Add Mypy to dev pipeline
* Fix typing bugs
* Extend interface to allow monkey patching original SecretsSettingsSource
  • Loading branch information
makukha authored Sep 3, 2024
1 parent 971cd01 commit d68162d
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 35 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This package unties secrets from environment variables config options and implem
## Features

* Use secret file source in nested settings models
* Drop-in replacement of standard `SecretsSettingsSource`
* Drop-in replacement of standard `SecretsSettingsSource` (up to monkey patching)
* 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)
* Has `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 separately
Expand Down
14 changes: 10 additions & 4 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,31 @@ tasks:
install:
desc: Install dev python environment.
cmds:
- task: dep:lock
- task: dependencies:lock
- pdm install --check --dev

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

lint:
desc: Run linters and code formatters.
desc: Run linters.
cmds:
- pdm run mypy src
- ruff check
- ruff format --check

format:
desc: Run code formatters.
cmds:
- ruff format

test:
desc: Run tests.
deps: [install]
Expand Down
14 changes: 7 additions & 7 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.
8 changes: 4 additions & 4 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.
70 changes: 67 additions & 3 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ build = [
"bump-my-version; python_version>='3.12'",
"genbadge[coverage,tests]; python_version>='3.12'",
]
lint = [
"mypy>=1.11.2",
]
test = [
"pytest; python_version>='3.12'",
"pytest-cov; python_version>='3.12'",
Expand Down
40 changes: 24 additions & 16 deletions src/pydantic_file_secrets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
from typing import Any, Literal
import warnings

from pydantic_settings import BaseSettings, EnvSettingsSource, SecretsSettingsSource, SettingsError
from pydantic_settings import (
BaseSettings,
EnvSettingsSource,
SecretsSettingsSource,
SettingsError,
)
from pydantic_settings.sources import parse_env_vars
from pydantic_settings.utils import path_type_label

Expand All @@ -14,38 +19,39 @@
__all__ = ['__version__', 'FileSecretsSettingsSource']


type SecretsDirMissing = Literal['ok', 'warn', 'error']


class FileSecretsSettingsSource(EnvSettingsSource):
def __init__(
self,
file_secret_settings: SecretsSettingsSource | BaseSettings,
file_secret_settings: SecretsSettingsSource | type[BaseSettings],
secrets_dir: str | Path | list[str | Path] | None = None,
secrets_dir_missing: SecretsDirMissing | None = None,
secrets_dir_missing: Literal['ok', 'warn', 'error'] | None = None,
secrets_dir_max_size: int | None = None,
secrets_case_sensitive: bool | None = None,
secrets_prefix: str | None = None,
secrets_nested_delimiter: str | None = None,
secrets_nested_subdir: bool | None = None,
# args for compatibility with SecretsSettingsSource, don't use directly
case_sensitive: bool | None = None,
env_prefix: str | None = None,
) -> None:
if isinstance(file_secret_settings, BaseSettings):
# We allow the first argument to be settings_cls like original
# SecretsSettingsSource. However, it is recommended to pass
# SecretsSettingsSource instance instead (as it is shown in usage examples),
# otherwise `_secrets_dir` arg passed to Settings() constructor
# will be ignored.
settings_cls = file_secret_settings
else:
settings_cls = file_secret_settings.settings_cls
# We allow the first argument to be settings_cls like original
# SecretsSettingsSource. However, it is recommended to pass
# SecretsSettingsSource instance instead (as it is shown in usage examples),
# otherwise `_secrets_dir` arg passed to Settings() constructor
# will be ignored.
settings_cls: type[BaseSettings] = getattr(
file_secret_settings,
'settings_cls',
file_secret_settings, # type: ignore[arg-type]
)
# config options
conf = settings_cls.model_config
self.secrets_dir: str | Path | list[str | Path] | None = first_not_none(
getattr(file_secret_settings, 'secrets_dir', None),
secrets_dir,
conf.get('secrets_dir'),
)
self.secrets_dir_missing: SecretsDirMissing | None = first_not_none(
self.secrets_dir_missing: Literal['ok', 'warn', 'error'] = first_not_none(
secrets_dir_missing,
conf.get('secrets_dir_missing'),
'warn',
Expand All @@ -58,12 +64,14 @@ def __init__(
self.case_sensitive: bool = first_not_none(
secrets_case_sensitive,
conf.get('secrets_case_sensitive'),
case_sensitive,
conf.get('case_sensitive'),
False,
)
self.secrets_prefix: str = first_not_none(
secrets_prefix,
conf.get('secrets_prefix'),
env_prefix,
conf.get('env_prefix'),
'',
)
Expand Down
Empty file.
29 changes: 29 additions & 0 deletions tests/test_dropin_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

import pydantic_settings.main # import to monkeypatch
from pydantic_file_secrets import FileSecretsSettingsSource


@pytest.fixture
def monkeypatch_settings(monkeypatch):
monkeypatch.setattr(
pydantic_settings.main,
'SecretsSettingsSource',
FileSecretsSettingsSource,
)


def test_dropin(settings_model, monkeypatch_settings, secrets_dir):
secrets_dir.add_files(
('dir1/key1', 'secret1'),
('dir2/key2', 'secret2'),
)

class Settings(pydantic_settings.BaseSettings):
key1: str
key2: str

conf = Settings(_secrets_dir=[secrets_dir / 'dir1', secrets_dir / 'dir2'])

assert conf.key1 == 'secret1'
assert conf.key2 == 'secret2'

0 comments on commit d68162d

Please sign in to comment.