From c6a3531d48c3f219099c6d8f21463092b9bc2883 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Sun, 1 Jun 2025 21:37:06 -0400 Subject: [PATCH 1/5] Expose case sensitive --- pydantic_settings/sources/providers/gcp.py | 24 +++++++++++++++------- tests/test_source_gcp_secret_manager.py | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pydantic_settings/sources/providers/gcp.py b/pydantic_settings/sources/providers/gcp.py index ba202222..284401bb 100644 --- a/pydantic_settings/sources/providers/gcp.py +++ b/pydantic_settings/sources/providers/gcp.py @@ -4,6 +4,7 @@ from functools import cached_property from typing import TYPE_CHECKING, Optional +from ..utils import parse_env_vars from .env import EnvSettingsSource if TYPE_CHECKING: @@ -37,10 +38,11 @@ class GoogleSecretManagerMapping(Mapping[str, Optional[str]]): _loaded_secrets: dict[str, str | None] _secret_client: SecretManagerServiceClient - def __init__(self, secret_client: SecretManagerServiceClient, project_id: str) -> None: + def __init__(self, secret_client: SecretManagerServiceClient, project_id: str, case_sensitive: bool) -> None: self._loaded_secrets = {} self._secret_client = secret_client self._project_id = project_id + self._case_sensitive = case_sensitive @property def _gcp_project_path(self) -> str: @@ -48,15 +50,20 @@ def _gcp_project_path(self) -> str: @cached_property def _secret_names(self) -> list[str]: - return [ - self._secret_client.parse_secret_path(secret.name).get('secret', '') - for secret in self._secret_client.list_secrets(parent=self._gcp_project_path) - ] + rv: list[str] = [] + for secret in self._secret_client.list_secrets(parent=self._gcp_project_path): + name = self._secret_client.parse_secret_path(secret.name).get('secret', '') + if not self._case_sensitive: + name = name.lower() + rv.append(name) + return rv def _secret_version_path(self, key: str, version: str = 'latest') -> str: return self._secret_client.secret_version_path(self._project_id, key, version) def __getitem__(self, key: str) -> str | None: + if not self._case_sensitive: + key = key.lower() if key not in self._loaded_secrets: # If we know the key isn't available in secret manager, raise a key error if key not in self._secret_names: @@ -92,6 +99,7 @@ def __init__( env_parse_none_str: str | None = None, env_parse_enums: bool | None = None, secret_client: SecretManagerServiceClient | None = None, + case_sensitive: bool | None = True, ) -> None: # Import Google Packages if they haven't already been imported if SecretManagerServiceClient is None or Credentials is None or google_auth_default is None: @@ -124,7 +132,7 @@ def __init__( super().__init__( settings_cls, - case_sensitive=True, + case_sensitive=case_sensitive, env_prefix=env_prefix, env_ignore_empty=False, env_parse_none_str=env_parse_none_str, @@ -132,7 +140,9 @@ def __init__( ) def _load_env_vars(self) -> Mapping[str, Optional[str]]: - return GoogleSecretManagerMapping(self._secret_client, project_id=self._project_id) + return GoogleSecretManagerMapping( + self._secret_client, project_id=self._project_id, case_sensitive=self.case_sensitive + ) def __repr__(self) -> str: return f'{self.__class__.__name__}(project_id={self._project_id!r}, env_nested_delimiter={self.env_nested_delimiter!r})' diff --git a/tests/test_source_gcp_secret_manager.py b/tests/test_source_gcp_secret_manager.py index d5b13b1f..0dcb68bb 100644 --- a/tests/test_source_gcp_secret_manager.py +++ b/tests/test_source_gcp_secret_manager.py @@ -57,7 +57,7 @@ def mock_access_secret_version(name: str): @pytest.fixture def secret_manager_mapping(mock_secret_client): - return GoogleSecretManagerMapping(mock_secret_client, 'test-project') + return GoogleSecretManagerMapping(mock_secret_client, project_id='test-project', case_sensitive=True) @pytest.fixture From 83b9a69cfb55fa9ffe5af822b7b077862c7b1846 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Sun, 1 Jun 2025 21:39:00 -0400 Subject: [PATCH 2/5] Remove import --- pydantic_settings/sources/providers/gcp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pydantic_settings/sources/providers/gcp.py b/pydantic_settings/sources/providers/gcp.py index 284401bb..a127672d 100644 --- a/pydantic_settings/sources/providers/gcp.py +++ b/pydantic_settings/sources/providers/gcp.py @@ -4,7 +4,6 @@ from functools import cached_property from typing import TYPE_CHECKING, Optional -from ..utils import parse_env_vars from .env import EnvSettingsSource if TYPE_CHECKING: From 843c2149ce138292add4a44f931e4719e25b62f2 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Sun, 1 Jun 2025 21:52:55 -0400 Subject: [PATCH 3/5] Update tests --- tests/test_source_gcp_secret_manager.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_source_gcp_secret_manager.py b/tests/test_source_gcp_secret_manager.py index 0dcb68bb..f28f2dfa 100644 --- a/tests/test_source_gcp_secret_manager.py +++ b/tests/test_source_gcp_secret_manager.py @@ -96,6 +96,11 @@ def test_secret_manager_mapping_getitem_success(self, secret_manager_mapping): value = secret_manager_mapping['test-secret'] assert value == 'test-value' + def test_secret_manager_mapping_getitem_case_insensitive_success(self, mock_secret_client): + case_insensitive_mapping = GoogleSecretManagerMapping(mock_secret_client, project_id='test-project', case_sensitive=False) + value = case_insensitive_mapping['TEST-SECRET'] + assert value == 'test-value' + def test_secret_manager_mapping_getitem_nonexistent_key(self, secret_manager_mapping): with pytest.raises(KeyError): _ = secret_manager_mapping['nonexistent-secret'] From c8315393de8fb98fa6aafc4f2b87ae49c14f700a Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Sun, 1 Jun 2025 21:53:52 -0400 Subject: [PATCH 4/5] Format --- tests/test_source_gcp_secret_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_source_gcp_secret_manager.py b/tests/test_source_gcp_secret_manager.py index f28f2dfa..e43c45ad 100644 --- a/tests/test_source_gcp_secret_manager.py +++ b/tests/test_source_gcp_secret_manager.py @@ -97,7 +97,9 @@ def test_secret_manager_mapping_getitem_success(self, secret_manager_mapping): assert value == 'test-value' def test_secret_manager_mapping_getitem_case_insensitive_success(self, mock_secret_client): - case_insensitive_mapping = GoogleSecretManagerMapping(mock_secret_client, project_id='test-project', case_sensitive=False) + case_insensitive_mapping = GoogleSecretManagerMapping( + mock_secret_client, project_id='test-project', case_sensitive=False + ) value = case_insensitive_mapping['TEST-SECRET'] assert value == 'test-value' From d84cfc7578c0bd6dfe52c9cebf0ce78bfe3eef75 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Sun, 1 Jun 2025 22:00:41 -0400 Subject: [PATCH 5/5] Improve readability --- pydantic_settings/sources/providers/gcp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydantic_settings/sources/providers/gcp.py b/pydantic_settings/sources/providers/gcp.py index a127672d..62f356a7 100644 --- a/pydantic_settings/sources/providers/gcp.py +++ b/pydantic_settings/sources/providers/gcp.py @@ -50,7 +50,9 @@ def _gcp_project_path(self) -> str: @cached_property def _secret_names(self) -> list[str]: rv: list[str] = [] - for secret in self._secret_client.list_secrets(parent=self._gcp_project_path): + + secrets = self._secret_client.list_secrets(parent=self._gcp_project_path) + for secret in secrets: name = self._secret_client.parse_secret_path(secret.name).get('secret', '') if not self._case_sensitive: name = name.lower()