From b883884b8d319586c080a226f0e45f260e4575c7 Mon Sep 17 00:00:00 2001 From: Femi Adebayo Date: Wed, 11 Jun 2025 13:42:55 -0400 Subject: [PATCH 1/2] feat: expose case_sensitive on aws secrets source --- pydantic_settings/sources/providers/aws.py | 15 +++++-- tests/test_source_aws_secrets_manager.py | 52 ++++++++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/pydantic_settings/sources/providers/aws.py b/pydantic_settings/sources/providers/aws.py index dc5c0cd0..ae6ab42c 100644 --- a/pydantic_settings/sources/providers/aws.py +++ b/pydantic_settings/sources/providers/aws.py @@ -1,9 +1,12 @@ -from __future__ import annotations as _annotations # important for BaseSettings import to work +from __future__ import ( + annotations as _annotations, +) # important for BaseSettings import to work import json from collections.abc import Mapping from typing import TYPE_CHECKING, Optional +from ..utils import parse_env_vars from .env import EnvSettingsSource if TYPE_CHECKING: @@ -36,6 +39,7 @@ def __init__( settings_cls: type[BaseSettings], secret_id: str, region_name: str | None = None, + case_sensitive: bool | None = True, env_prefix: str | None = None, env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, @@ -45,7 +49,7 @@ def __init__( self._secret_id = secret_id super().__init__( settings_cls, - case_sensitive=True, + case_sensitive=case_sensitive, env_prefix=env_prefix, env_nested_delimiter='--', env_ignore_empty=False, @@ -56,7 +60,12 @@ def __init__( def _load_env_vars(self) -> Mapping[str, Optional[str]]: response = self._secretsmanager_client.get_secret_value(SecretId=self._secret_id) # type: ignore - return json.loads(response['SecretString']) + return parse_env_vars( + json.loads(response['SecretString']), + self.case_sensitive, + self.env_ignore_empty, + self.env_parse_none_str, + ) def __repr__(self) -> str: return ( diff --git a/tests/test_source_aws_secrets_manager.py b/tests/test_source_aws_secrets_manager.py index 46160070..1fe1a1a5 100644 --- a/tests/test_source_aws_secrets_manager.py +++ b/tests/test_source_aws_secrets_manager.py @@ -39,7 +39,10 @@ pytest.skip('PyYAML is not installed', allow_module_level=True) -@pytest.mark.skipif(not aws_secrets_manager, reason='pydantic-settings[aws-secrets-manager] is not installed') +@pytest.mark.skipif( + not aws_secrets_manager, + reason='pydantic-settings[aws-secrets-manager] is not installed', +) class TestAWSSecretsManagerSettingsSource: """Test AWSSecretsManagerSettingsSource.""" @@ -76,7 +79,10 @@ class AWSSecretsManagerSettings(BaseSettings): sql_server_user: str = Field(..., alias='SqlServerUser') sql_server: SqlServer = Field(..., alias='SqlServer') - secret_data = {'SqlServerUser': 'test-user', 'SqlServer--Password': 'test-password'} + secret_data = { + 'SqlServerUser': 'test-user', + 'SqlServer--Password': 'test-password', + } client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString=json.dumps(secret_data)) @@ -88,6 +94,43 @@ class AWSSecretsManagerSettings(BaseSettings): assert settings['SqlServerUser'] == 'test-user' assert settings['SqlServer']['Password'] == 'test-password' + @mock_aws + def test_secret_manager_case_insensitive_success(self) -> None: + """Test secret manager getitem case insensitive success.""" + + class SqlServer(BaseModel): + password: str = Field(..., alias='Password') + + class AWSSecretsManagerSettings(BaseSettings): + """AWSSecretsManager settings.""" + + sql_server_user: str + sql_server: SqlServer + + @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 (AWSSecretsManagerSettingsSource(settings_cls, 'test-secret', case_sensitive=False),) + + secret_data = { + 'SQL_SERVER_USER': 'test-user', + 'SQL_SERVER--PASSWORD': 'test-password', + } + + client = boto3.client('secretsmanager') + client.create_secret(Name='test-secret', SecretString=json.dumps(secret_data)) + + settings = AWSSecretsManagerSettings() # type: ignore + + assert settings.sql_server_user == 'test-user' + assert settings.sql_server.password == 'test-password' + @mock_aws def test_aws_secrets_manager_settings_source(self) -> None: """Test AWSSecretsManagerSettingsSource.""" @@ -112,7 +155,10 @@ def settings_customise_sources( ) -> tuple[PydanticBaseSettingsSource, ...]: return (AWSSecretsManagerSettingsSource(settings_cls, 'test-secret'),) - secret_data = {'SqlServerUser': 'test-user', 'SqlServer--Password': 'test-password'} + secret_data = { + 'SqlServerUser': 'test-user', + 'SqlServer--Password': 'test-password', + } client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString=json.dumps(secret_data)) From 1704fd72bc78bd8a02c4c6c9620572144aafad4d Mon Sep 17 00:00:00 2001 From: Femi Adebayo Date: Fri, 20 Jun 2025 15:14:53 -0400 Subject: [PATCH 2/2] fix: revert additional formatting changes --- pydantic_settings/sources/providers/aws.py | 4 +--- tests/test_source_aws_secrets_manager.py | 15 +++------------ 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/pydantic_settings/sources/providers/aws.py b/pydantic_settings/sources/providers/aws.py index ae6ab42c..c1004ec5 100644 --- a/pydantic_settings/sources/providers/aws.py +++ b/pydantic_settings/sources/providers/aws.py @@ -1,6 +1,4 @@ -from __future__ import ( - annotations as _annotations, -) # important for BaseSettings import to work +from __future__ import annotations as _annotations # important for BaseSettings import to work import json from collections.abc import Mapping diff --git a/tests/test_source_aws_secrets_manager.py b/tests/test_source_aws_secrets_manager.py index 1fe1a1a5..c68aa928 100644 --- a/tests/test_source_aws_secrets_manager.py +++ b/tests/test_source_aws_secrets_manager.py @@ -39,10 +39,7 @@ pytest.skip('PyYAML is not installed', allow_module_level=True) -@pytest.mark.skipif( - not aws_secrets_manager, - reason='pydantic-settings[aws-secrets-manager] is not installed', -) +@pytest.mark.skipif(not aws_secrets_manager, reason='pydantic-settings[aws-secrets-manager] is not installed') class TestAWSSecretsManagerSettingsSource: """Test AWSSecretsManagerSettingsSource.""" @@ -79,10 +76,7 @@ class AWSSecretsManagerSettings(BaseSettings): sql_server_user: str = Field(..., alias='SqlServerUser') sql_server: SqlServer = Field(..., alias='SqlServer') - secret_data = { - 'SqlServerUser': 'test-user', - 'SqlServer--Password': 'test-password', - } + secret_data = {'SqlServerUser': 'test-user', 'SqlServer--Password': 'test-password'} client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString=json.dumps(secret_data)) @@ -155,10 +149,7 @@ def settings_customise_sources( ) -> tuple[PydanticBaseSettingsSource, ...]: return (AWSSecretsManagerSettingsSource(settings_cls, 'test-secret'),) - secret_data = { - 'SqlServerUser': 'test-user', - 'SqlServer--Password': 'test-password', - } + secret_data = {'SqlServerUser': 'test-user', 'SqlServer--Password': 'test-password'} client = boto3.client('secretsmanager') client.create_secret(Name='test-secret', SecretString=json.dumps(secret_data))