Skip to content

Commit 492513f

Browse files
authored
Merge branch 'main' into warning-quotes
2 parents 6b232f9 + 4239ea4 commit 492513f

28 files changed

+2968
-937
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
fail-fast: false
3737
matrix:
3838
os: [ubuntu-latest, macos-latest, windows-latest]
39-
python: ['3.9', '3.10', '3.11', '3.12', '3.13']
39+
python: ['3.10', '3.11', '3.12', '3.13']
4040

4141
env:
4242
PYTHON: ${{ matrix.python }}

docs/index.md

Lines changed: 308 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,29 @@ CliApp.run(Git, cli_args=['clone', 'repo', 'dir']).model_dump() == {
11731173

11741174
When executing a subcommand with an asynchronous cli_cmd, Pydantic settings automatically detects whether the current thread already has an active event loop. If so, the async command is run in a fresh thread to avoid conflicts. Otherwise, it uses asyncio.run() in the current thread. This handling ensures your asynchronous subcommands "just work" without additional manual setup.
11751175

1176+
### Serializing CLI Arguments
1177+
1178+
An instantiated Pydantic model can be serialized into its CLI arguments using the `CliApp.serialize` method.
1179+
1180+
```py
1181+
from pydantic import BaseModel
1182+
1183+
from pydantic_settings import CliApp
1184+
1185+
1186+
class Nested(BaseModel):
1187+
that: int
1188+
1189+
1190+
class Settings(BaseModel):
1191+
this: str
1192+
nested: Nested
1193+
1194+
1195+
print(CliApp.serialize(Settings(this='hello', nested=Nested(that=123))))
1196+
#> ['--this', 'hello', '--nested.that', '123']
1197+
```
1198+
11761199
### Mutually Exclusive Groups
11771200

11781201
CLI mutually exclusive groups can be created by inheriting from the `CliMutuallyExclusiveGroup` class.
@@ -1331,7 +1354,9 @@ print(Settings().model_dump())
13311354

13321355
#### CLI Kebab Case for Arguments
13331356

1334-
Change whether CLI arguments should use kebab case by enabling `cli_kebab_case`.
1357+
Change whether CLI arguments should use kebab case by enabling `cli_kebab_case`. By default, `cli_kebab_case=True` will
1358+
ignore enum fields, and is equivalent to `cli_kebab_case='no_enums'`. To apply kebab case to everything, including
1359+
enums, use `cli_kebab_case='all'`.
13351360

13361361
```py
13371362
import sys
@@ -1691,8 +1716,6 @@ class Settings(BaseSettings):
16911716

16921717
If a shortcut collides (is mapped to multiple fields), it will apply to the first matching field in the model.
16931718

1694-
See the [test cases](../tests/test_source_cli.py) for more advanced usage and edge cases.
1695-
16961719
### Integrating with Existing Parsers
16971720

16981721
A CLI settings source can be integrated with existing parsers by overriding the default CLI settings source with a user
@@ -1836,6 +1859,248 @@ Last, run your application inside a Docker container and supply your newly creat
18361859
docker service create --name pydantic-with-secrets --secret my_secret_data pydantic-app:latest
18371860
```
18381861

1862+
## Nested Secrets
1863+
1864+
The default secrets implementation, `SecretsSettingsSource`, has behaviour that is not always desired or sufficient.
1865+
For example, the default implementation does not support secret fields in nested submodels.
1866+
1867+
`NestedSecretsSettingsSource` can be used as a drop-in replacement to `SecretsSettingsSource` to adjust the default behaviour.
1868+
All differences are summarized in the table below.
1869+
1870+
| `SecretsSettingsSource` | `NestedSecretsSettingsSourcee` |
1871+
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
1872+
| Secret fields must belong to a top level model. | Secrets can be fields of nested models. |
1873+
| Secret files can be placed in `secrets_dir`s only. | Secret files can be placed in subdirectories for nested models. |
1874+
| Secret files discovery is based on the same configuration options that are used by `EnvSettingsSource`: `case_sensitive`, `env_nested_delimiter`, `env_prefix`. | Default options are respected, but can be overridden with `secrets_case_sensitive`, `secrets_nested_delimiter`, `secrets_prefix`. |
1875+
| When `secrets_dir` is missing on the file system, a warning is generated. | Use `secrets_dir_missing` options to choose whether to issue warning, raise error, or silently ignore. |
1876+
1877+
### Use Case: Plain Directory Layout
1878+
1879+
```text
1880+
📂 secrets
1881+
├── 📄 app_key
1882+
└── 📄 db_passwd
1883+
```
1884+
1885+
In the example below, secrets nested delimiter `'_'` is different from env nested delimiter `'__'`.
1886+
Value for `Settings.db.user` can be passed in env variable `MY_DB__USER`.
1887+
1888+
```py
1889+
from pydantic import BaseModel, SecretStr
1890+
1891+
from pydantic_settings import (
1892+
BaseSettings,
1893+
NestedSecretsSettingsSource,
1894+
SettingsConfigDict,
1895+
)
1896+
1897+
1898+
class AppSettings(BaseModel):
1899+
key: SecretStr
1900+
1901+
1902+
class DbSettings(BaseModel):
1903+
user: str
1904+
passwd: SecretStr
1905+
1906+
1907+
class Settings(BaseSettings):
1908+
app: AppSettings
1909+
db: DbSettings
1910+
1911+
model_config = SettingsConfigDict(
1912+
env_prefix='MY_',
1913+
env_nested_delimiter='__',
1914+
secrets_dir='secrets',
1915+
secrets_nested_delimiter='_',
1916+
)
1917+
1918+
@classmethod
1919+
def settings_customise_sources(
1920+
cls,
1921+
settings_cls,
1922+
init_settings,
1923+
env_settings,
1924+
dotenv_settings,
1925+
file_secret_settings,
1926+
):
1927+
return (
1928+
init_settings,
1929+
env_settings,
1930+
dotenv_settings,
1931+
NestedSecretsSettingsSource(file_secret_settings),
1932+
)
1933+
```
1934+
1935+
### Use Case: Nested Directory Layout
1936+
1937+
```text
1938+
📂 secrets
1939+
├── 📂 app
1940+
│ └── 📄 key
1941+
└── 📂 db
1942+
└── 📄 passwd
1943+
```
1944+
```py
1945+
from pydantic import BaseModel, SecretStr
1946+
1947+
from pydantic_settings import (
1948+
BaseSettings,
1949+
NestedSecretsSettingsSource,
1950+
SettingsConfigDict,
1951+
)
1952+
1953+
1954+
class AppSettings(BaseModel):
1955+
key: SecretStr
1956+
1957+
1958+
class DbSettings(BaseModel):
1959+
user: str
1960+
passwd: SecretStr
1961+
1962+
1963+
class Settings(BaseSettings):
1964+
app: AppSettings
1965+
db: DbSettings
1966+
1967+
model_config = SettingsConfigDict(
1968+
env_prefix='MY_',
1969+
env_nested_delimiter='__',
1970+
secrets_dir='secrets',
1971+
secrets_nested_subdir=True,
1972+
)
1973+
1974+
@classmethod
1975+
def settings_customise_sources(
1976+
cls,
1977+
settings_cls,
1978+
init_settings,
1979+
env_settings,
1980+
dotenv_settings,
1981+
file_secret_settings,
1982+
):
1983+
return (
1984+
init_settings,
1985+
env_settings,
1986+
dotenv_settings,
1987+
NestedSecretsSettingsSource(file_secret_settings),
1988+
)
1989+
```
1990+
1991+
### Use Case: Multiple Nested Directories
1992+
1993+
```text
1994+
📂 secrets
1995+
├── 📂 default
1996+
│ ├── 📂 app
1997+
│ │ └── 📄 key
1998+
│ └── 📂 db
1999+
│ └── 📄 passwd
2000+
└── 📂 override
2001+
├── 📂 app
2002+
│ └── 📄 key
2003+
└── 📂 db
2004+
└── 📄 passwd
2005+
```
2006+
```py
2007+
from pydantic import BaseModel, SecretStr
2008+
2009+
from pydantic_settings import (
2010+
BaseSettings,
2011+
NestedSecretsSettingsSource,
2012+
SettingsConfigDict,
2013+
)
2014+
2015+
2016+
class AppSettings(BaseModel):
2017+
key: SecretStr
2018+
2019+
2020+
class DbSettings(BaseModel):
2021+
user: str
2022+
passwd: SecretStr
2023+
2024+
2025+
class Settings(BaseSettings):
2026+
app: AppSettings
2027+
db: DbSettings
2028+
2029+
model_config = SettingsConfigDict(
2030+
env_prefix='MY_',
2031+
env_nested_delimiter='__',
2032+
secrets_dir=['secrets/default', 'secrets/override'],
2033+
secrets_nested_subdir=True,
2034+
)
2035+
2036+
@classmethod
2037+
def settings_customise_sources(
2038+
cls,
2039+
settings_cls,
2040+
init_settings,
2041+
env_settings,
2042+
dotenv_settings,
2043+
file_secret_settings,
2044+
):
2045+
return (
2046+
init_settings,
2047+
env_settings,
2048+
dotenv_settings,
2049+
NestedSecretsSettingsSource(file_secret_settings),
2050+
)
2051+
```
2052+
2053+
### Configuration Options
2054+
2055+
#### secrets_dir
2056+
2057+
Path to secrets directory, same as `SecretsSettingsSource.secrets_dir`. If `list`, the last match wins.
2058+
If `secrets_dir` is passed in both source constructor and model config, values are not merged (constructor wins).
2059+
2060+
#### secrets_dir_missing
2061+
2062+
If `secrets_dir` does not exist, original `SecretsSettingsSource` issues a warning.
2063+
However, this may be undesirable, for example if we don't mount Docker Secrets in e.g. dev environment.
2064+
Use `secrets_dir_missing` to choose:
2065+
2066+
* `'ok'` — do nothing if `secrets_dir` does not exist
2067+
* `'warn'` (default) — print warning, same as `SecretsSettingsSource`
2068+
* `'error'` — raise `SettingsError`
2069+
2070+
If multiple `secrets_dir` passed, the same `secrets_dir_missing` action applies to each of them.
2071+
2072+
#### secrets_dir_max_size
2073+
2074+
Limit the size of `secrets_dir` for security reasons, defaults to `SECRETS_DIR_MAX_SIZE` equal to 16 MiB.
2075+
2076+
`NestedSecretsSettingsSource` is a thin wrapper around `EnvSettingsSource`,
2077+
which loads all potential secrets on initialization. This could lead to `MemoryError` if we mount
2078+
a large file under `secrets_dir`.
2079+
2080+
If multiple `secrets_dir` passed, the limit applies to each directory independently.
2081+
2082+
#### secrets_case_sensitive
2083+
2084+
Same as `case_sensitive`, but works for secrets only. If not specified, defaults to `case_sensitive`.
2085+
2086+
#### secrets_nested_delimiter
2087+
2088+
Same as `env_nested_delimiter`, but works for secrets only. If not specified, defaults to `env_nested_delimiter`.
2089+
This option is used to implement _nested secrets directory_ layout and allows to do even nasty things
2090+
like `/run/secrets/model/delim/nested1/delim/nested2`.
2091+
2092+
#### secrets_nested_subdir
2093+
2094+
Boolean flag to turn on _nested secrets directory_ mode, `False` by default. If `True`, sets `secrets_nested_delimiter`
2095+
to `os.sep`. Raises `SettingsError` if `secrets_nested_delimiter` is already specified.
2096+
2097+
#### secrets_prefix
2098+
2099+
Secret path prefix, similar to `env_prefix`, but works for secrets only. Defaults to `env_prefix`
2100+
if not specified. Works in both plain and nested directory modes, like
2101+
`'/run/secrets/prefix_model__nested'` and `'/run/secrets/prefix_model/nested'`.
2102+
2103+
18392104
## AWS Secrets Manager
18402105

18412106
You must set one parameter:
@@ -1949,6 +2214,45 @@ class AzureKeyVaultSettings(BaseSettings):
19492214
)
19502215
```
19512216

2217+
### Snake case conversion
2218+
2219+
The Azure Key Vault source accepts a `snake_case_convertion` option, disabled by default, to convert Key Vault secret names by mapping them to Python's snake_case field names, without the need to use aliases.
2220+
2221+
```py
2222+
import os
2223+
2224+
from azure.identity import DefaultAzureCredential
2225+
2226+
from pydantic_settings import (
2227+
AzureKeyVaultSettingsSource,
2228+
BaseSettings,
2229+
PydanticBaseSettingsSource,
2230+
)
2231+
2232+
2233+
class AzureKeyVaultSettings(BaseSettings):
2234+
my_setting: str
2235+
2236+
@classmethod
2237+
def settings_customise_sources(
2238+
cls,
2239+
settings_cls: type[BaseSettings],
2240+
init_settings: PydanticBaseSettingsSource,
2241+
env_settings: PydanticBaseSettingsSource,
2242+
dotenv_settings: PydanticBaseSettingsSource,
2243+
file_secret_settings: PydanticBaseSettingsSource,
2244+
) -> tuple[PydanticBaseSettingsSource, ...]:
2245+
az_key_vault_settings = AzureKeyVaultSettingsSource(
2246+
settings_cls,
2247+
os.environ['AZURE_KEY_VAULT_URL'],
2248+
DefaultAzureCredential(),
2249+
snake_case_conversion=True,
2250+
)
2251+
return (az_key_vault_settings,)
2252+
```
2253+
2254+
This setup will load Azure Key Vault secrets (e.g., `MySetting`, `mySetting`, `my-secret` or `MY-SECRET`), mapping them to the snake case version (`my_setting` in this case).
2255+
19522256
### Dash to underscore mapping
19532257

19542258
The Azure Key Vault source accepts a `dash_to_underscore` option, disabled by default, to support Key Vault kebab-case secret names by mapping them to Python's snake_case field names. When enabled, dashes (`-`) in secret names are mapped to underscores (`_`) in field names during validation.
@@ -2400,7 +2704,7 @@ print(Settings())
24002704
#> foobar='test'
24012705
```
24022706

2403-
#### Accesing the result of previous sources
2707+
#### Accessing the result of previous sources
24042708

24052709
Each source of settings can access the output of the previous ones.
24062710

pydantic_settings/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
GoogleSecretManagerSettingsSource,
1919
InitSettingsSource,
2020
JsonConfigSettingsSource,
21+
NestedSecretsSettingsSource,
2122
NoDecode,
2223
PydanticBaseSettingsSource,
2324
PyprojectTomlConfigSettingsSource,
@@ -48,6 +49,7 @@
4849
'GoogleSecretManagerSettingsSource',
4950
'InitSettingsSource',
5051
'JsonConfigSettingsSource',
52+
'NestedSecretsSettingsSource',
5153
'NoDecode',
5254
'PydanticBaseSettingsSource',
5355
'PyprojectTomlConfigSettingsSource',

0 commit comments

Comments
 (0)