Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions env_values.py

This file was deleted.

1 change: 1 addition & 0 deletions env_values/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .env_values import env_values
75 changes: 75 additions & 0 deletions env_values/env_values.py
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 6 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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'],
)
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .test_env import *
from .test_yaml import *
8 changes: 8 additions & 0 deletions tests/fixtures/nested.env
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions tests/fixtures/nested.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FOO:
BAR:
BAZ: foobarbaz
BAR:
BAZ: barbaz
BAR_BAZ: baz
BAZ:
69:
1337: '1337'
true: 'True'
null: 'None'
7 changes: 7 additions & 0 deletions tests/fixtures/simple.env
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions tests/fixtures/simple.yml
Original file line number Diff line number Diff line change
@@ -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
133 changes: 133 additions & 0 deletions tests/test_env.py
Original file line number Diff line number Diff line change
@@ -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()
38 changes: 38 additions & 0 deletions tests/test_yaml.py
Original file line number Diff line number Diff line change
@@ -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')