diff --git a/env_values.py b/env_values.py deleted file mode 100644 index 20db913..0000000 --- a/env_values.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -from dotenv import find_dotenv, dotenv_values - -# Find an environment file, either given by the env `CY_ENV_FILE` or find a file -# called `.env`. Same for goes for an override file. -CY_ENV_FILE = os.environ.get('CY_ENV_FILE', find_dotenv('.env')) -CY_ENV_OVERRIDE_FILE = os.environ.get('CY_ENV_OVERRIDE_FILE', find_dotenv('.env.override')) - -def env_values(): - """Retrieve environment variables by combining `os.environ`, `CY_ENV_FILE` and `CY_ENV_OVERRIDE_FILE`. - - Get environment variables by looking at (in order): - 1. os.environ - 2. .env.override - 3. .env - - When a variable is present in `os.environ` and `.env`, the one in `.env` will be ignored and the one in `os.environ` - will "win". - - Calling `env_values()` has no side effects, eg `os.environ` not be altered. - - Returns: - :dict: Env values. - - Examples: - >>> env = env_values() - >>> - >>> env.get('CY_DEBUG') - >>> env.get('CY_DEBUG', False) - >>> env['CY_DEBUG'] - """ - env_file = dotenv_values(CY_ENV_FILE) if CY_ENV_FILE else {} - env_override_file = dotenv_values(CY_ENV_OVERRIDE_FILE) if CY_ENV_OVERRIDE_FILE else {} - - return { - **env_file, - **env_override_file, - **os.environ, - } diff --git a/env_values/__init__.py b/env_values/__init__.py new file mode 100644 index 0000000..c7d9777 --- /dev/null +++ b/env_values/__init__.py @@ -0,0 +1 @@ +from .env_values import env_values diff --git a/env_values/env_values.py b/env_values/env_values.py new file mode 100644 index 0000000..eae5f96 --- /dev/null +++ b/env_values/env_values.py @@ -0,0 +1,75 @@ +from dotenv import dotenv_values +import os +import yaml + +def _gen_values(values, prefix=''): + for key, value in values.items(): + yield (f'{prefix}{key}', value) + if isinstance(value, dict): + yield from _gen_values(value, prefix=f'{prefix}{key}_') + +def yaml_values(path): + """ + Parse a JSON or YAML file and return its content as a dict. + + The returned dict will contain all nested values on the + root level as well, identified by keys that are concatenations + of the keys of their ancestors. + + For example, the following structure will produce four values: + `FOO`, `FOO_BAR`, `FOO_BAR_BAZ`, and `BAZ`. + ``` + FOO: + BAR: + BAZ: baz + FOO_BAR: 1337 + BAZ: ['foo', 'bar'] + ``` + + Parameters: + path: Absolute or relative path to the file. + """ + with open(path) as f: + values = yaml.safe_load(f) + if not isinstance(values, dict): + return {} + return dict(_gen_values(values)) + +def env_values(*paths): + """Retrieve environment variables by combining `os.environ` and the provided env files. + + Get environment variables by looking at (in order): + 1. os.environ + 2. file #n + 3. file #n-1 + ... + n+1. file #1 + + When a variable is present in `os.environ` and any of the files, the ones in the files will be ignored and the one in `os.environ` + will "win". + + Calling `env_values()` has no side effects, eg `os.environ` not be altered. + + Returns: + :dict: Env values. + + Example: + >>> env = env_values('.env') + >>> + >>> env.get('CY_DEBUG') + >>> env.get('CY_DEBUG', False) + >>> env['CY_DEBUG'] + """ + values = {} + for path in paths: + if not path: + continue + if path.endswith('.json') or path.endswith('.yml') or path.endswith('.yaml'): + values.update(yaml_values(path)) + else: + values.update(dotenv_values(path)) + + return { + **values, + **os.environ + } diff --git a/setup.py b/setup.py index f7eb797..d0b748d 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,15 @@ from setuptools import setup -version = '1.0' +version = '2.0' + +with open("README.md") as f: + long_description = f.read() setup( name='env_values', version=version, description="Load env values", - long_description='\n'.join([ - open("README.md").read(), - ]), + long_description=long_description, keywords='env', author='Burhan Zainuddin', author_email='burhan@codeyellow.nl', @@ -19,6 +20,6 @@ python_requires='~=3.3', include_package_data=True, zip_safe=True, - install_requires=['python-dotenv'], + install_requires=['python-dotenv', 'pyyaml'], py_modules=['env_values'], ) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..c0fa5e5 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +from .test_env import * +from .test_yaml import * diff --git a/tests/fixtures/nested.env b/tests/fixtures/nested.env new file mode 100644 index 0000000..85f7a51 --- /dev/null +++ b/tests/fixtures/nested.env @@ -0,0 +1,8 @@ +FOO_BAR=FOOBAR +FOO_BAR_BAZ=FOOBARBAZ +BAR_BAZ=BAZ +BAR_BAZ=BARBAZ +BAZ=BAZ +BAZ_69_1337=LEET +BAZ_69_False=False +BAZ_69_None diff --git a/tests/fixtures/nested.yml b/tests/fixtures/nested.yml new file mode 100644 index 0000000..5e0ef54 --- /dev/null +++ b/tests/fixtures/nested.yml @@ -0,0 +1,11 @@ +FOO: + BAR: + BAZ: foobarbaz +BAR: + BAZ: barbaz +BAR_BAZ: baz +BAZ: + 69: + 1337: '1337' + true: 'True' + null: 'None' diff --git a/tests/fixtures/simple.env b/tests/fixtures/simple.env new file mode 100644 index 0000000..cf1d426 --- /dev/null +++ b/tests/fixtures/simple.env @@ -0,0 +1,7 @@ +FOO=FOO +BAR=123 +QUX=1.23 +FOO_BAR=false +FOO_QUX=null +FOO_BAR_BAZ=foo,bar,baz +# TEST=TEST diff --git a/tests/fixtures/simple.yml b/tests/fixtures/simple.yml new file mode 100644 index 0000000..7198d0e --- /dev/null +++ b/tests/fixtures/simple.yml @@ -0,0 +1,7 @@ +FOO: foo +BAR: 123 +BAZ: 1.23 +FOO_BAR: false +FOO_BAZ: null +FOO_BAR_BAZ: ['foo', 'bar', 'baz'] +# TEST: test diff --git a/tests/test_env.py b/tests/test_env.py new file mode 100644 index 0000000..c124df6 --- /dev/null +++ b/tests/test_env.py @@ -0,0 +1,133 @@ +import os +import unittest +from env_values import * + +class OverrideTest(unittest.TestCase): + + def test_yaml_dotenv(self): + env = env_values( + 'tests/fixtures/simple.yml', + 'tests/fixtures/simple.env' + ) + self.assertEqual(env['FOO'], 'FOO') + self.assertEqual(env['BAR'], '123') + self.assertEqual(env['BAZ'], 1.23) + self.assertEqual(env['QUX'], '1.23') + self.assertEqual(env['FOO_BAR'], 'false') + self.assertEqual(env['FOO_BAZ'], None) + self.assertEqual(env['FOO_QUX'], 'null') + self.assertEqual(env['FOO_BAR_BAZ'], 'foo,bar,baz') + self.assertFalse('TEST' in env) + + def test_dotenv_yaml(self): + env = env_values( + 'tests/fixtures/simple.env', + 'tests/fixtures/simple.yml', + ) + self.assertEqual(env['FOO'], 'foo') + self.assertEqual(env['BAR'], 123) + self.assertEqual(env['BAZ'], 1.23) + self.assertEqual(env['QUX'], '1.23') + self.assertEqual(env['FOO_BAR'], False) + self.assertEqual(env['FOO_BAZ'], None) + self.assertEqual(env['FOO_QUX'], 'null') + self.assertEqual(env['FOO_BAR_BAZ'], ['foo', 'bar', 'baz']) + self.assertFalse('TEST' in env) + +class NestedOverrideTest(unittest.TestCase): + + def setUp(self): + self.env = env_values( + 'tests/fixtures/nested.yml', + 'tests/fixtures/nested.env', + ) + + def test_nesting(self): + self.assertEqual(self.env['FOO'], {'BAR': {'BAZ': 'foobarbaz'}}) + self.assertEqual(self.env['FOO_BAR'], 'FOOBAR') + self.assertEqual(self.env['FOO_BAR_BAZ'], 'FOOBARBAZ') + + def test_duplicate_keys(self): + self.assertEqual(self.env['BAR'], {'BAZ': 'barbaz'}) + self.assertNotEqual(self.env['BAR_BAZ'], 'barbaz') + self.assertNotEqual(self.env['BAR_BAZ'], 'baz') + self.assertNotEqual(self.env['BAR_BAZ'], 'BAZ') + self.assertEqual(self.env['BAR_BAZ'], 'BARBAZ') + + def test_nonstring_keys(self): + self.assertEqual(self.env['BAZ'], 'BAZ') + self.assertEqual(self.env['BAZ_69'], {1337: '1337', True: 'True', None: 'None'}) + self.assertEqual(self.env['BAZ_69_1337'], 'LEET') + self.assertEqual(self.env['BAZ_69_True'], 'True') + self.assertEqual(self.env['BAZ_69_False'], 'False') + self.assertEqual(self.env['BAZ_69_None'], None) + +class OsEnvTest(unittest.TestCase): + + def test_loading(self): + environ = os.environ.copy() + env_values( + 'tests/fixtures/simple.yml', + 'tests/fixtures/simple.env', + ) + self.assertEqual(os.environ, environ) + + def test_env(self): + os.environ.update({ + 'FOO': 'foo', + 'BAR': 'bar', + 'FOO_BAR': 'foo_bar', + 'FOO_BAR_BAZ': 'foo_bar_baz', + 'TEST': 'test', + }) + env = env_values() + self.assertEqual(env['FOO'], 'foo') + self.assertEqual(env['BAR'], 'bar') + self.assertEqual(env['FOO_BAR'], 'foo_bar') + self.assertEqual(env['FOO_BAR_BAZ'], 'foo_bar_baz') + self.assertEqual(env['TEST'], 'test') + os.environ.clear() + + def test_yaml_env(self): + os.environ.update({ + 'FOO': 'foo', + 'BAR': 'bar', + 'QUX': 'qux', + 'FOO_BAR': 'foo_bar', + 'FOO_QUX': 'foo_qux', + 'FOO_BAR_BAZ': 'foo_bar_baz', + 'TEST': 'test', + }) + env = env_values('tests/fixtures/simple.yml') + self.assertEqual(env['FOO'], 'foo') + self.assertEqual(env['BAR'], 'bar') + self.assertEqual(env['BAZ'], 1.23) + self.assertEqual(env['QUX'], 'qux') + self.assertEqual(env['FOO_BAR'], 'foo_bar') + self.assertEqual(env['FOO_BAZ'], None) + self.assertEqual(env['FOO_QUX'], 'foo_qux') + self.assertEqual(env['FOO_BAR_BAZ'], 'foo_bar_baz') + self.assertEqual(env['TEST'], 'test') + os.environ.clear() + + def test_dotenv_env(self): + os.environ.update({ + 'FOO': 'foo', + 'BAR': 'bar', + 'BAZ': 'baz', + 'FOO_BAR': 'foo_bar', + 'FOO_BAZ': 'foo_baz', + 'FOO_BAR_BAZ': 'foo_bar_baz', + 'TEST': 'test', + }) + env = env_values('tests/fixtures/simple.env') + self.assertEqual(env['FOO'], 'foo') + self.assertEqual(env['BAR'], 'bar') + self.assertEqual(env['BAZ'], 'baz') + self.assertEqual(env['QUX'], '1.23') + self.assertEqual(env['FOO_BAR'], 'foo_bar') + self.assertEqual(env['FOO_BAZ'], 'foo_baz') + self.assertEqual(env['FOO_QUX'], 'null') + self.assertEqual(env['FOO_BAR_BAZ'], 'foo_bar_baz') + self.assertEqual(env['TEST'], 'test') + os.environ.clear() diff --git a/tests/test_yaml.py b/tests/test_yaml.py new file mode 100644 index 0000000..a0f125f --- /dev/null +++ b/tests/test_yaml.py @@ -0,0 +1,38 @@ +import unittest +from env_values import * + +class SimpleYamlTest(unittest.TestCase): + + def setUp(self): + self.env = env_values('tests/fixtures/simple.yml') + + def test_values(self): + self.assertEqual(self.env['FOO'], 'foo') + self.assertEqual(self.env['BAR'], 123) + self.assertEqual(self.env['BAZ'], 1.23) + self.assertEqual(self.env['FOO_BAR'], False) + self.assertEqual(self.env['FOO_BAZ'], None) + self.assertEqual(self.env['FOO_BAR_BAZ'], ['foo', 'bar', 'baz']) + self.assertFalse('TEST' in self.env) + +class NestedYamlTest(unittest.TestCase): + + def setUp(self): + self.env = env_values('tests/fixtures/nested.yml') + + def test_nesting(self): + self.assertEqual(self.env['FOO'], {'BAR': {'BAZ': 'foobarbaz'}}) + self.assertEqual(self.env['FOO_BAR'], {'BAZ': 'foobarbaz'}) + self.assertEqual(self.env['FOO_BAR_BAZ'], 'foobarbaz') + + def test_duplicate_keys(self): + self.assertEqual(self.env['BAR'], {'BAZ': 'barbaz'}) + self.assertNotEqual(self.env['BAR_BAZ'], 'barbaz') + self.assertEqual(self.env['BAR_BAZ'], 'baz') + + def test_nonstring_keys(self): + self.assertEqual(self.env['BAZ'], {69: {1337: '1337', True: 'True', None: 'None'}}) + self.assertEqual(self.env['BAZ_69'], {1337: '1337', True: 'True', None: 'None'}) + self.assertEqual(self.env['BAZ_69_1337'], '1337') + self.assertEqual(self.env['BAZ_69_True'], 'True') + self.assertEqual(self.env['BAZ_69_None'], 'None')