From 894741fa5a88bbde1e835ec4122fe9801f3572f5 Mon Sep 17 00:00:00 2001 From: Andreu Codina <30506301+AndreuCodina@users.noreply.github.com> Date: Tue, 16 Sep 2025 23:57:37 +0200 Subject: [PATCH 1/8] Snake case conversion in Azure Key Vault --- pydantic_settings/sources/providers/azure.py | 34 ++++++++---------- tests/test_source_azure_key_vault.py | 37 +++++++++++--------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/pydantic_settings/sources/providers/azure.py b/pydantic_settings/sources/providers/azure.py index 04f0bee5..21c04e39 100644 --- a/pydantic_settings/sources/providers/azure.py +++ b/pydantic_settings/sources/providers/azure.py @@ -5,6 +5,7 @@ from collections.abc import Iterator, Mapping from typing import TYPE_CHECKING, Optional +from pydantic.alias_generators import to_snake from pydantic.fields import FieldInfo from .env import EnvSettingsSource @@ -44,27 +45,27 @@ class AzureKeyVaultMapping(Mapping[str, Optional[str]]): def __init__( self, secret_client: SecretClient, - case_sensitive: bool, ) -> None: self._loaded_secrets = {} self._secret_client = secret_client - self._case_sensitive = case_sensitive self._secret_map: dict[str, str] = self._load_remote() def _load_remote(self) -> dict[str, str]: secret_names: Iterator[str] = ( secret.name for secret in self._secret_client.list_properties_of_secrets() if secret.name and secret.enabled ) - if self._case_sensitive: - return {name: name for name in secret_names} - return {name.lower(): name for name in secret_names} + return {to_snake(name): name for name in secret_names} def __getitem__(self, key: str) -> str | None: - if not self._case_sensitive: - key = key.lower() - if key not in self._loaded_secrets and key in self._secret_map: - self._loaded_secrets[key] = self._secret_client.get_secret(self._secret_map[key]).value - return self._loaded_secrets[key] + key_snake = to_snake(key) + + if key_snake not in self._loaded_secrets and key_snake in self._secret_map: + self._loaded_secrets[key_snake] = self._secret_client.get_secret(self._secret_map[key_snake]).value + + try: + return self._loaded_secrets[key_snake] + except Exception: + raise KeyError(key) def __len__(self) -> int: return len(self._secret_map) @@ -82,8 +83,6 @@ def __init__( settings_cls: type[BaseSettings], url: str, credential: TokenCredential, - dash_to_underscore: bool = False, - case_sensitive: bool | None = None, env_prefix: str | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, @@ -91,12 +90,11 @@ def __init__( import_azure_key_vault() self._url = url self._credential = credential - self._dash_to_underscore = dash_to_underscore super().__init__( settings_cls, - case_sensitive=case_sensitive, + case_sensitive=False, env_prefix=env_prefix, - env_nested_delimiter='--', + env_nested_delimiter='__', env_ignore_empty=False, env_parse_none_str=env_parse_none_str, env_parse_enums=env_parse_enums, @@ -104,12 +102,10 @@ def __init__( def _load_env_vars(self) -> Mapping[str, Optional[str]]: secret_client = SecretClient(vault_url=self._url, credential=self._credential) - return AzureKeyVaultMapping(secret_client, self.case_sensitive) + return AzureKeyVaultMapping(secret_client) def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]: - if self._dash_to_underscore: - return list((x[0], x[1].replace('_', '-'), x[2]) for x in super()._extract_field_info(field, field_name)) - return super()._extract_field_info(field, field_name) + return list((x[0], x[0], x[2]) for x in super()._extract_field_info(field, field_name)) def __repr__(self) -> str: return f'{self.__class__.__name__}(url={self._url!r}, env_nested_delimiter={self.env_nested_delimiter!r})' diff --git a/tests/test_source_azure_key_vault.py b/tests/test_source_azure_key_vault.py index 89f48add..d914b21d 100644 --- a/tests/test_source_azure_key_vault.py +++ b/tests/test_source_azure_key_vault.py @@ -46,14 +46,13 @@ def test___call__(self, mocker: MockerFixture) -> None: """Test __call__.""" class SqlServer(BaseModel): - password: str = Field(..., alias='Password') + password: str class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" - SqlServerUser: str - sql_server_user: str = Field(..., alias='SqlServerUser') - sql_server: SqlServer = Field(..., alias='SqlServer') + sql_server_user: str + sql_server: SqlServer expected_secrets = [ type('', (), {'name': 'SqlServerUser', 'enabled': True}), @@ -74,14 +73,14 @@ class AzureKeyVaultSettings(BaseSettings): settings = obj() - assert settings['SqlServerUser'] == expected_secret_value - assert settings['SqlServer']['Password'] == expected_secret_value + assert settings['sql_server_user'] == expected_secret_value + assert settings['sql_server']['password'] == expected_secret_value def test_do_not_load_disabled_secrets(self, mocker: MockerFixture) -> None: class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" - SqlServerPassword: str + sql_server_password: str DisabledSqlServerPassword: str disabled_secret_name = 'SqlServerPassword' @@ -108,14 +107,13 @@ def test_azure_key_vault_settings_source(self, mocker: MockerFixture) -> None: """Test AzureKeyVaultSettingsSource.""" class SqlServer(BaseModel): - password: str = Field(..., alias='Password') + password: str class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" - SqlServerUser: str - sql_server_user: str = Field(..., alias='SqlServerUser') - sql_server: SqlServer = Field(..., alias='SqlServer') + sql_server_user: str + sql_server: SqlServer @classmethod def settings_customise_sources( @@ -148,7 +146,6 @@ def settings_customise_sources( settings = AzureKeyVaultSettings() # type: ignore - assert settings.SqlServerUser == expected_secret_value assert settings.sql_server_user == expected_secret_value assert settings.sql_server.password == expected_secret_value @@ -161,12 +158,17 @@ def _raise_resource_not_found_when_getting_parent_secret_name(self, secret_name: return key_vault_secret - def test_dash_to_underscore_translation(self, mocker: MockerFixture) -> None: - """Test that dashes in secret names are mapped to underscores in field names.""" + def test_snake_case_translation(self, mocker: MockerFixture) -> None: + """Test that secret names are mapped to snake case in field names.""" + + class NestedModel(BaseModel): + nested_field: str class AzureKeyVaultSettings(BaseSettings): my_field: str - alias_field: str = Field(..., alias='Secret-Alias') + alias_field: str = Field(alias='Secret-Alias') + alias_field_2: str = Field(alias='another-SECRET-AliaS') + nested_model: NestedModel @classmethod def settings_customise_sources( @@ -182,13 +184,14 @@ def settings_customise_sources( settings_cls, 'https://my-resource.vault.azure.net/', DefaultAzureCredential(), - dash_to_underscore=True, ), ) expected_secrets = [ type('', (), {'name': 'my-field', 'enabled': True}), type('', (), {'name': 'Secret-Alias', 'enabled': True}), + type('', (), {'name': 'another-SECRET-AliaS', 'enabled': True}), + type('', (), {'name': 'NestedModel--NestedField', 'enabled': True}), ] expected_secret_value = 'SecretValue' @@ -205,3 +208,5 @@ def settings_customise_sources( assert settings.my_field == expected_secret_value assert settings.alias_field == expected_secret_value + assert settings.alias_field_2 == expected_secret_value + assert settings.nested_model.nested_field == expected_secret_value From 81cff9d8abeacd37ba264dadf6b62dc4547aab63 Mon Sep 17 00:00:00 2001 From: Andreu Codina <30506301+AndreuCodina@users.noreply.github.com> Date: Wed, 17 Sep 2025 00:27:53 +0200 Subject: [PATCH 2/8] Solve comment --- pydantic_settings/sources/providers/azure.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pydantic_settings/sources/providers/azure.py b/pydantic_settings/sources/providers/azure.py index 21c04e39..0cf2c701 100644 --- a/pydantic_settings/sources/providers/azure.py +++ b/pydantic_settings/sources/providers/azure.py @@ -59,13 +59,13 @@ def _load_remote(self) -> dict[str, str]: def __getitem__(self, key: str) -> str | None: key_snake = to_snake(key) - if key_snake not in self._loaded_secrets and key_snake in self._secret_map: - self._loaded_secrets[key_snake] = self._secret_client.get_secret(self._secret_map[key_snake]).value + if key_snake not in self._loaded_secrets: + if key_snake in self._secret_map: + self._loaded_secrets[key_snake] = self._secret_client.get_secret(self._secret_map[key_snake]).value + else: + raise KeyError(key) - try: - return self._loaded_secrets[key_snake] - except Exception: - raise KeyError(key) + return self._loaded_secrets[key_snake] def __len__(self) -> int: return len(self._secret_map) From e12349e388403d5dd74ba6ab268ff2d36bf59947 Mon Sep 17 00:00:00 2001 From: Andreu Codina <30506301+AndreuCodina@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:00:05 +0200 Subject: [PATCH 3/8] Remove breaking changes --- pydantic_settings/sources/providers/azure.py | 54 +++++++++++++++----- tests/test_source_azure_key_vault.py | 13 ++--- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/pydantic_settings/sources/providers/azure.py b/pydantic_settings/sources/providers/azure.py index 0cf2c701..09658d5f 100644 --- a/pydantic_settings/sources/providers/azure.py +++ b/pydantic_settings/sources/providers/azure.py @@ -42,30 +42,41 @@ class AzureKeyVaultMapping(Mapping[str, Optional[str]]): _secret_client: SecretClient _secret_names: list[str] - def __init__( - self, - secret_client: SecretClient, - ) -> None: + def __init__(self, secret_client: SecretClient, case_sensitive: bool, snake_case_conversion: bool) -> None: self._loaded_secrets = {} self._secret_client = secret_client + self._case_sensitive = case_sensitive + self._snake_case_conversion = snake_case_conversion self._secret_map: dict[str, str] = self._load_remote() def _load_remote(self) -> dict[str, str]: secret_names: Iterator[str] = ( secret.name for secret in self._secret_client.list_properties_of_secrets() if secret.name and secret.enabled ) - return {to_snake(name): name for name in secret_names} + + if self._snake_case_conversion: + return {to_snake(name): name for name in secret_names} + + if self._case_sensitive: + return {name: name for name in secret_names} + + return {name.lower(): name for name in secret_names} def __getitem__(self, key: str) -> str | None: - key_snake = to_snake(key) + new_key = key + + if self._snake_case_conversion: + new_key = to_snake(key) + elif not self._case_sensitive: + new_key = key.lower() - if key_snake not in self._loaded_secrets: - if key_snake in self._secret_map: - self._loaded_secrets[key_snake] = self._secret_client.get_secret(self._secret_map[key_snake]).value + if new_key not in self._loaded_secrets: + if new_key in self._secret_map: + self._loaded_secrets[new_key] = self._secret_client.get_secret(self._secret_map[new_key]).value else: raise KeyError(key) - return self._loaded_secrets[key_snake] + return self._loaded_secrets[new_key] def __len__(self) -> int: return len(self._secret_map) @@ -83,6 +94,9 @@ def __init__( settings_cls: type[BaseSettings], url: str, credential: TokenCredential, + dash_to_underscore: bool = False, + case_sensitive: bool | None = None, + snake_case_conversion: bool = False, env_prefix: str | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, @@ -90,11 +104,13 @@ def __init__( import_azure_key_vault() self._url = url self._credential = credential + self._dash_to_underscore = dash_to_underscore + self._snake_case_conversion = snake_case_conversion super().__init__( settings_cls, - case_sensitive=False, + case_sensitive=False if snake_case_conversion else case_sensitive, env_prefix=env_prefix, - env_nested_delimiter='__', + env_nested_delimiter='__' if snake_case_conversion else '--', env_ignore_empty=False, env_parse_none_str=env_parse_none_str, env_parse_enums=env_parse_enums, @@ -102,10 +118,20 @@ def __init__( def _load_env_vars(self) -> Mapping[str, Optional[str]]: secret_client = SecretClient(vault_url=self._url, credential=self._credential) - return AzureKeyVaultMapping(secret_client) + return AzureKeyVaultMapping( + secret_client=secret_client, + case_sensitive=self.case_sensitive, + snake_case_conversion=self._snake_case_conversion, + ) def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]: - return list((x[0], x[0], x[2]) for x in super()._extract_field_info(field, field_name)) + if self._snake_case_conversion: + return list((x[0], x[0], x[2]) for x in super()._extract_field_info(field, field_name)) + + if self._dash_to_underscore: + return list((x[0], x[1].replace('_', '-'), x[2]) for x in super()._extract_field_info(field, field_name)) + + return super()._extract_field_info(field, field_name) def __repr__(self) -> str: return f'{self.__class__.__name__}(url={self._url!r}, env_nested_delimiter={self.env_nested_delimiter!r})' diff --git a/tests/test_source_azure_key_vault.py b/tests/test_source_azure_key_vault.py index d914b21d..622ffb89 100644 --- a/tests/test_source_azure_key_vault.py +++ b/tests/test_source_azure_key_vault.py @@ -46,17 +46,18 @@ def test___call__(self, mocker: MockerFixture) -> None: """Test __call__.""" class SqlServer(BaseModel): - password: str + password: str = Field(..., alias='Password') class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" - sql_server_user: str - sql_server: SqlServer + SqlServerUser: str + # sql_server_user: str = Field(..., alias='SqlServerUser') + # sql_server: SqlServer = Field(..., alias='SqlServer') expected_secrets = [ type('', (), {'name': 'SqlServerUser', 'enabled': True}), - type('', (), {'name': 'SqlServer--Password', 'enabled': True}), + # type('', (), {'name': 'SqlServer--Password', 'enabled': True}), ] expected_secret_value = 'SecretValue' mocker.patch( @@ -73,8 +74,8 @@ class AzureKeyVaultSettings(BaseSettings): settings = obj() - assert settings['sql_server_user'] == expected_secret_value - assert settings['sql_server']['password'] == expected_secret_value + assert settings['SqlServerUser'] == expected_secret_value + assert settings['SqlServer']['Password'] == expected_secret_value def test_do_not_load_disabled_secrets(self, mocker: MockerFixture) -> None: class AzureKeyVaultSettings(BaseSettings): From e3611ca94629cbd3344c9deff7f1d87c02f97efc Mon Sep 17 00:00:00 2001 From: Andreu Codina <30506301+AndreuCodina@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:01:21 +0200 Subject: [PATCH 4/8] Remove breaking changes --- pydantic_settings/sources/providers/azure.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pydantic_settings/sources/providers/azure.py b/pydantic_settings/sources/providers/azure.py index 09658d5f..c0c95064 100644 --- a/pydantic_settings/sources/providers/azure.py +++ b/pydantic_settings/sources/providers/azure.py @@ -42,7 +42,12 @@ class AzureKeyVaultMapping(Mapping[str, Optional[str]]): _secret_client: SecretClient _secret_names: list[str] - def __init__(self, secret_client: SecretClient, case_sensitive: bool, snake_case_conversion: bool) -> None: + def __init__( + self, + secret_client: SecretClient, + case_sensitive: bool, + snake_case_conversion: bool, + ) -> None: self._loaded_secrets = {} self._secret_client = secret_client self._case_sensitive = case_sensitive From d15ce67ad6c5a56e51060e935f2ed9e947e32a78 Mon Sep 17 00:00:00 2001 From: Andreu Codina <30506301+AndreuCodina@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:11:16 +0200 Subject: [PATCH 5/8] Remove breaking changes --- tests/test_source_azure_key_vault.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_source_azure_key_vault.py b/tests/test_source_azure_key_vault.py index 622ffb89..8dc81e51 100644 --- a/tests/test_source_azure_key_vault.py +++ b/tests/test_source_azure_key_vault.py @@ -52,12 +52,12 @@ class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" SqlServerUser: str - # sql_server_user: str = Field(..., alias='SqlServerUser') - # sql_server: SqlServer = Field(..., alias='SqlServer') + sql_server_user: str = Field(..., alias='SqlServerUser') + sql_server: SqlServer = Field(..., alias='SqlServer') expected_secrets = [ type('', (), {'name': 'SqlServerUser', 'enabled': True}), - # type('', (), {'name': 'SqlServer--Password', 'enabled': True}), + type('', (), {'name': 'SqlServer--Password', 'enabled': True}), ] expected_secret_value = 'SecretValue' mocker.patch( From 6ccbc90b96bb1f45baba5945b129bfd20b144e8b Mon Sep 17 00:00:00 2001 From: Andreu Codina <30506301+AndreuCodina@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:18:58 +0200 Subject: [PATCH 6/8] Remove breaking changes --- tests/test_source_azure_key_vault.py | 70 ++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/tests/test_source_azure_key_vault.py b/tests/test_source_azure_key_vault.py index 8dc81e51..4a602ad8 100644 --- a/tests/test_source_azure_key_vault.py +++ b/tests/test_source_azure_key_vault.py @@ -81,7 +81,7 @@ def test_do_not_load_disabled_secrets(self, mocker: MockerFixture) -> None: class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" - sql_server_password: str + SqlServerPassword: str DisabledSqlServerPassword: str disabled_secret_name = 'SqlServerPassword' @@ -108,13 +108,14 @@ def test_azure_key_vault_settings_source(self, mocker: MockerFixture) -> None: """Test AzureKeyVaultSettingsSource.""" class SqlServer(BaseModel): - password: str + password: str = Field(..., alias='Password') class AzureKeyVaultSettings(BaseSettings): """AzureKeyVault settings.""" - sql_server_user: str - sql_server: SqlServer + SqlServerUser: str + sql_server_user: str = Field(..., alias='SqlServerUser') + sql_server: SqlServer = Field(..., alias='SqlServer') @classmethod def settings_customise_sources( @@ -147,6 +148,7 @@ def settings_customise_sources( settings = AzureKeyVaultSettings() # type: ignore + assert settings.SqlServerUser == expected_secret_value assert settings.sql_server_user == expected_secret_value assert settings.sql_server.password == expected_secret_value @@ -159,14 +161,61 @@ def _raise_resource_not_found_when_getting_parent_secret_name(self, secret_name: return key_vault_secret - def test_snake_case_translation(self, mocker: MockerFixture) -> None: + def test_dash_to_underscore_translation(self, mocker: MockerFixture) -> None: + """Test that dashes in secret names are mapped to underscores in field names.""" + + class AzureKeyVaultSettings(BaseSettings): + my_field: str + alias_field: str = Field(..., alias='Secret-Alias') + + @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 ( + AzureKeyVaultSettingsSource( + settings_cls, + 'https://my-resource.vault.azure.net/', + DefaultAzureCredential(), + dash_to_underscore=True, + ), + ) + + expected_secrets = [ + type('', (), {'name': 'my-field', 'enabled': True}), + type('', (), {'name': 'Secret-Alias', 'enabled': True}), + ] + expected_secret_value = 'SecretValue' + + mocker.patch( + f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.list_properties_of_secrets.__qualname__}', + return_value=expected_secrets, + ) + mocker.patch( + f'{AzureKeyVaultSettingsSource.__module__}.{SecretClient.get_secret.__qualname__}', + return_value=KeyVaultSecret(SecretProperties(), expected_secret_value), + ) + + settings = AzureKeyVaultSettings() + + assert settings.my_field == expected_secret_value + assert settings.alias_field == expected_secret_value + + def test_snake_case_conversion(self, mocker: MockerFixture) -> None: """Test that secret names are mapped to snake case in field names.""" class NestedModel(BaseModel): nested_field: str class AzureKeyVaultSettings(BaseSettings): - my_field: str + my_field_from_kebab_case: str + my_field_from_pascal_case: str + my_field_from_camel_case: str alias_field: str = Field(alias='Secret-Alias') alias_field_2: str = Field(alias='another-SECRET-AliaS') nested_model: NestedModel @@ -185,11 +234,14 @@ def settings_customise_sources( settings_cls, 'https://my-resource.vault.azure.net/', DefaultAzureCredential(), + snake_case_conversion=True, ), ) expected_secrets = [ - type('', (), {'name': 'my-field', 'enabled': True}), + type('', (), {'name': 'my-field-from-kebab-case', 'enabled': True}), + type('', (), {'name': 'MyFieldFromPascalCase', 'enabled': True}), + type('', (), {'name': 'myFieldFromCamelCase', 'enabled': True}), type('', (), {'name': 'Secret-Alias', 'enabled': True}), type('', (), {'name': 'another-SECRET-AliaS', 'enabled': True}), type('', (), {'name': 'NestedModel--NestedField', 'enabled': True}), @@ -207,7 +259,9 @@ def settings_customise_sources( settings = AzureKeyVaultSettings() - assert settings.my_field == expected_secret_value + assert settings.my_field_from_kebab_case == expected_secret_value + assert settings.my_field_from_pascal_case == expected_secret_value + assert settings.my_field_from_camel_case == expected_secret_value assert settings.alias_field == expected_secret_value assert settings.alias_field_2 == expected_secret_value assert settings.nested_model.nested_field == expected_secret_value From af9ab7bf1d49bf20f38d0647575d8fbf62974f4d Mon Sep 17 00:00:00 2001 From: Andreu Codina <30506301+AndreuCodina@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:47:01 +0200 Subject: [PATCH 7/8] Update documentation --- docs/index.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/index.md b/docs/index.md index b78cfbd7..300fc24c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1970,6 +1970,46 @@ class AzureKeyVaultSettings(BaseSettings): ) ``` +### Snake case conversion + +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. + +```py +import os + +from azure.identity import DefaultAzureCredential +from pydantic import Field + +from pydantic_settings import ( + AzureKeyVaultSettingsSource, + BaseSettings, + PydanticBaseSettingsSource, +) + + +class AzureKeyVaultSettings(BaseSettings): + my_setting: str + + @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, ...]: + az_key_vault_settings = AzureKeyVaultSettingsSource( + settings_cls, + os.environ['AZURE_KEY_VAULT_URL'], + DefaultAzureCredential(), + snake_case_conversion=True, + ) + return (az_key_vault_settings,) +``` + +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). + ### Dash to underscore mapping 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. From 55b577657d57cd3e5452936993419b2a7cee2900 Mon Sep 17 00:00:00 2001 From: Andreu Codina <30506301+AndreuCodina@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:49:00 +0200 Subject: [PATCH 8/8] Update documentation --- docs/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 300fc24c..e5b39fa1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1978,7 +1978,6 @@ The Azure Key Vault source accepts a `snake_case_convertion` option, disabled by import os from azure.identity import DefaultAzureCredential -from pydantic import Field from pydantic_settings import ( AzureKeyVaultSettingsSource,