From a1e856c2253bb8ea4ad918b6555139b584d45f5b Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 8 Nov 2023 11:42:26 -0700 Subject: [PATCH 1/3] Add RepositoryString --- decouple.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/decouple.py b/decouple.py index 9873fc9..5a866e4 100644 --- a/decouple.py +++ b/decouple.py @@ -188,6 +188,79 @@ def __getitem__(self, key): return self.data[key] +class RepositoryString(RepositoryEmpty): + """ + Repository class to retrieve options from a string. + + Parses a string formatted like a `.env` file into a dictionary of options. + This class is an extension of the `RepositoryEmpty` class that provides a + way to read configuration keys from an environment string. + + Attributes: + data (dict): A dictionary to hold the parsed key-value pairs. + """ + + def __init__(self, source): + """ + Initializes the RepositoryString with a given string source. + + The provided string should have one "KEY=value" pair per line, similar + to a `.env` file format. Lines starting with `#` are considered as + comments and ignored. Surrounding whitespace is stripped from keys + and values. + + Args: + source (str): The string source to parse. + """ + self.data = {} + source_lines = source.split('\n') + + for line in source_lines: + line = line.strip() + + if not line or line.startswith('#') or '=' not in line: + continue + + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + + if len(value) >= 2 and ( + (value[0] == "'" and value[-1] == "'") + or (value[0] == '"' and value[-1] == '"') + ): + value = value[1:-1] + + self.data[key] = value + + def __contains__(self, key): + """ + Check if a key is present in the repository or the environment. + + Args: + key (str): The key to check for presence. + + Returns: + bool: True if key is in the repository or os.environ, False otherwise. + """ + return key in os.environ or key in self.data + + def __getitem__(self, key): + """ + Retrieve the value associated with the given key. + + Args: + key (str): The key to retrieve the value for. + + Returns: + str: The value associated with the key. + + Raises: + KeyError: If the key is not found in the repository. + """ + return self.data[key] + + class AutoConfig(object): """ Autodetects the config file and type. From d785d9d599788e0335f1f143b301c4e6acfa3061 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 8 Nov 2023 11:42:38 -0700 Subject: [PATCH 2/3] Add tests for RepositoryString --- tests/test_string.py | 105 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/test_string.py diff --git a/tests/test_string.py b/tests/test_string.py new file mode 100644 index 0000000..03c1425 --- /dev/null +++ b/tests/test_string.py @@ -0,0 +1,105 @@ +# coding: utf-8 +import os +import pytest +from decouple import Config, RepositoryString, UndefinedValueError + +ENVSTRING = ''' +KeyTrue=True\nKeyOne=1\nKeyYes=yes +KeyY=y +KeyOn=on + +KeyFalse=False +KeyZero=0 +KeyNo=no +KeyN=n +KeyOff=off +KeyEmpty= + +# CommentedKey=None +KeyWithSpaces = Some Value With Spaces +KeyWithQuotes="Quoted Value" +''' + + +@pytest.fixture(scope='module') +def config(): + return Config(RepositoryString(ENVSTRING)) + + +def test_string_comment(config): + with pytest.raises(UndefinedValueError): + config('CommentedKey') + + +def test_string_bool_true(config): + assert config('KeyTrue', cast=bool) + assert config('KeyOne', cast=bool) + assert config('KeyYes', cast=bool) + assert config('KeyY', cast=bool) + assert config('KeyOn', cast=bool) + + +def test_string_bool_false(config): + assert not config('KeyFalse', cast=bool) + assert not config('KeyZero', cast=bool) + assert not config('KeyNo', cast=bool) + assert not config('KeyOff', cast=bool) + assert not config('KeyN', cast=bool) + assert not config('KeyEmpty', cast=bool) + + +def test_string_undefined(config): + with pytest.raises(UndefinedValueError): + config('UndefinedKey') + + +def test_string_default_none(config): + assert config('UndefinedKey', default=None) is None + + +def test_string_default_bool(config): + assert not config('UndefinedKey', default=False, cast=bool) + assert config('UndefinedKey', default=True, cast=bool) + + +def test_string_default(config): + assert not config('UndefinedKey', default=False) + assert config('UndefinedKey', default=True) + + +def test_string_default_invalid_bool(config): + with pytest.raises(ValueError): + config('UndefinedKey', default='NotBool', cast=bool) + + +def test_string_empty(config): + assert config('KeyEmpty', default=None) == '' + + +def test_string_support_space(config): + assert config('KeyWithSpaces') == 'Some Value With Spaces' + + +def test_string_os_environ(config): + os.environ['KeyOverrideByEnv'] = 'This' + assert config('KeyOverrideByEnv') == 'This' + del os.environ['KeyOverrideByEnv'] + + +def test_string_undefined_but_present_in_os_environ(config): + os.environ['KeyOnlyEnviron'] = '' + assert config('KeyOnlyEnviron') == '' + del os.environ['KeyOnlyEnviron'] + + +def test_string_empty_string_means_false(config): + assert not config('KeyEmpty', cast=bool) + + +def test_string_repo_keyerror(config): + with pytest.raises(KeyError): + config.repository['UndefinedKey'] + + +def test_string_quoted_value(config): + assert config('KeyWithQuotes') == 'Quoted Value' From 72c799d458fdc726807549cb16a0ccf3f46f207e Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 20 Jan 2024 13:30:40 -0700 Subject: [PATCH 3/3] Rename class to RepositoryGoogleSecretManager --- decouple.py | 52 ++++++--------------- tests/test_gsm.py | 105 +++++++++++++++++++++++++++++++++++++++++++ tests/test_string.py | 105 ------------------------------------------- 3 files changed, 118 insertions(+), 144 deletions(-) create mode 100644 tests/test_gsm.py delete mode 100644 tests/test_string.py diff --git a/decouple.py b/decouple.py index 5a866e4..238c217 100644 --- a/decouple.py +++ b/decouple.py @@ -188,29 +188,30 @@ def __getitem__(self, key): return self.data[key] -class RepositoryString(RepositoryEmpty): +class RepositoryGoogleSecretManager(RepositoryEnv): """ - Repository class to retrieve options from a string. + Repository class for retrieving configuration options from Google Secret Manager. - Parses a string formatted like a `.env` file into a dictionary of options. - This class is an extension of the `RepositoryEmpty` class that provides a - way to read configuration keys from an environment string. + This class extends `RepositoryEnv` to specifically handle configurations stored in + Google Secret Manager. It parses strings formatted in a similar way to `.env` files, + converting them into a dictionary of configuration options. Attributes: - data (dict): A dictionary to hold the parsed key-value pairs. + data (dict): A dictionary holding the parsed key-value pairs from the Google + Secret Manager source. """ def __init__(self, source): """ - Initializes the RepositoryString with a given string source. + Initialize RepositoryGoogleSecretManager with a Google Secret Manager source. - The provided string should have one "KEY=value" pair per line, similar - to a `.env` file format. Lines starting with `#` are considered as - comments and ignored. Surrounding whitespace is stripped from keys - and values. + The source string is expected to have one "KEY=value" pair per line, akin to + the `.env` file format. Lines beginning with `#` are treated as comments and + are disregarded. Keys and values are trimmed of surrounding whitespace for + accurate parsing. Args: - source (str): The string source to parse. + source (str): The string source from Google Secret Manager to be parsed. """ self.data = {} source_lines = source.split('\n') @@ -233,33 +234,6 @@ def __init__(self, source): self.data[key] = value - def __contains__(self, key): - """ - Check if a key is present in the repository or the environment. - - Args: - key (str): The key to check for presence. - - Returns: - bool: True if key is in the repository or os.environ, False otherwise. - """ - return key in os.environ or key in self.data - - def __getitem__(self, key): - """ - Retrieve the value associated with the given key. - - Args: - key (str): The key to retrieve the value for. - - Returns: - str: The value associated with the key. - - Raises: - KeyError: If the key is not found in the repository. - """ - return self.data[key] - class AutoConfig(object): """ diff --git a/tests/test_gsm.py b/tests/test_gsm.py new file mode 100644 index 0000000..e3dde38 --- /dev/null +++ b/tests/test_gsm.py @@ -0,0 +1,105 @@ +# coding: utf-8 +import os +import pytest +from decouple import Config, RepositoryGoogleSecretManager, UndefinedValueError + +ENVSTRING = """ +KeyTrue=True\nKeyOne=1\nKeyYes=yes +KeyY=y +KeyOn=on + +KeyFalse=False +KeyZero=0 +KeyNo=no +KeyN=n +KeyOff=off +KeyEmpty= + +# CommentedKey=None +KeyWithSpaces = Some Value With Spaces +KeyWithQuotes="Quoted Value" +""" + + +@pytest.fixture(scope="module") +def config(): + return Config(RepositoryGoogleSecretManager(ENVSTRING)) + + +def test_string_comment(config): + with pytest.raises(UndefinedValueError): + config("CommentedKey") + + +def test_string_bool_true(config): + assert config("KeyTrue", cast=bool) + assert config("KeyOne", cast=bool) + assert config("KeyYes", cast=bool) + assert config("KeyY", cast=bool) + assert config("KeyOn", cast=bool) + + +def test_string_bool_false(config): + assert not config("KeyFalse", cast=bool) + assert not config("KeyZero", cast=bool) + assert not config("KeyNo", cast=bool) + assert not config("KeyOff", cast=bool) + assert not config("KeyN", cast=bool) + assert not config("KeyEmpty", cast=bool) + + +def test_string_undefined(config): + with pytest.raises(UndefinedValueError): + config("UndefinedKey") + + +def test_string_default_none(config): + assert config("UndefinedKey", default=None) is None + + +def test_string_default_bool(config): + assert not config("UndefinedKey", default=False, cast=bool) + assert config("UndefinedKey", default=True, cast=bool) + + +def test_string_default(config): + assert not config("UndefinedKey", default=False) + assert config("UndefinedKey", default=True) + + +def test_string_default_invalid_bool(config): + with pytest.raises(ValueError): + config("UndefinedKey", default="NotBool", cast=bool) + + +def test_string_empty(config): + assert config("KeyEmpty", default=None) == "" + + +def test_string_support_space(config): + assert config("KeyWithSpaces") == "Some Value With Spaces" + + +def test_string_os_environ(config): + os.environ["KeyOverrideByEnv"] = "This" + assert config("KeyOverrideByEnv") == "This" + del os.environ["KeyOverrideByEnv"] + + +def test_string_undefined_but_present_in_os_environ(config): + os.environ["KeyOnlyEnviron"] = "" + assert config("KeyOnlyEnviron") == "" + del os.environ["KeyOnlyEnviron"] + + +def test_string_empty_string_means_false(config): + assert not config("KeyEmpty", cast=bool) + + +def test_string_repo_keyerror(config): + with pytest.raises(KeyError): + config.repository["UndefinedKey"] + + +def test_string_quoted_value(config): + assert config("KeyWithQuotes") == "Quoted Value" diff --git a/tests/test_string.py b/tests/test_string.py deleted file mode 100644 index 03c1425..0000000 --- a/tests/test_string.py +++ /dev/null @@ -1,105 +0,0 @@ -# coding: utf-8 -import os -import pytest -from decouple import Config, RepositoryString, UndefinedValueError - -ENVSTRING = ''' -KeyTrue=True\nKeyOne=1\nKeyYes=yes -KeyY=y -KeyOn=on - -KeyFalse=False -KeyZero=0 -KeyNo=no -KeyN=n -KeyOff=off -KeyEmpty= - -# CommentedKey=None -KeyWithSpaces = Some Value With Spaces -KeyWithQuotes="Quoted Value" -''' - - -@pytest.fixture(scope='module') -def config(): - return Config(RepositoryString(ENVSTRING)) - - -def test_string_comment(config): - with pytest.raises(UndefinedValueError): - config('CommentedKey') - - -def test_string_bool_true(config): - assert config('KeyTrue', cast=bool) - assert config('KeyOne', cast=bool) - assert config('KeyYes', cast=bool) - assert config('KeyY', cast=bool) - assert config('KeyOn', cast=bool) - - -def test_string_bool_false(config): - assert not config('KeyFalse', cast=bool) - assert not config('KeyZero', cast=bool) - assert not config('KeyNo', cast=bool) - assert not config('KeyOff', cast=bool) - assert not config('KeyN', cast=bool) - assert not config('KeyEmpty', cast=bool) - - -def test_string_undefined(config): - with pytest.raises(UndefinedValueError): - config('UndefinedKey') - - -def test_string_default_none(config): - assert config('UndefinedKey', default=None) is None - - -def test_string_default_bool(config): - assert not config('UndefinedKey', default=False, cast=bool) - assert config('UndefinedKey', default=True, cast=bool) - - -def test_string_default(config): - assert not config('UndefinedKey', default=False) - assert config('UndefinedKey', default=True) - - -def test_string_default_invalid_bool(config): - with pytest.raises(ValueError): - config('UndefinedKey', default='NotBool', cast=bool) - - -def test_string_empty(config): - assert config('KeyEmpty', default=None) == '' - - -def test_string_support_space(config): - assert config('KeyWithSpaces') == 'Some Value With Spaces' - - -def test_string_os_environ(config): - os.environ['KeyOverrideByEnv'] = 'This' - assert config('KeyOverrideByEnv') == 'This' - del os.environ['KeyOverrideByEnv'] - - -def test_string_undefined_but_present_in_os_environ(config): - os.environ['KeyOnlyEnviron'] = '' - assert config('KeyOnlyEnviron') == '' - del os.environ['KeyOnlyEnviron'] - - -def test_string_empty_string_means_false(config): - assert not config('KeyEmpty', cast=bool) - - -def test_string_repo_keyerror(config): - with pytest.raises(KeyError): - config.repository['UndefinedKey'] - - -def test_string_quoted_value(config): - assert config('KeyWithQuotes') == 'Quoted Value'