Skip to content

Commit 3c487fb

Browse files
committed
Dotenv-rw changes
1 parent 77e5e6c commit 3c487fb

4 files changed

Lines changed: 259 additions & 35 deletions

File tree

LICENSE.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
1+
django-dotenv-rw
2+
Copyright (c) 2013, Ted Tieken
3+
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without modification,
7+
are permitted provided that the following conditions are met:
8+
9+
* Redistributions of source code must retain the above copyright notice,
10+
this list of conditions and the following disclaimer.
11+
* Redistributions in binary form must reproduce the above copyright notice,
12+
this list of conditions and the following disclaimer in the documentation
13+
and/or other materials provided with the distribution.
14+
* Neither the name of django-dotenv nor the names of its contributors
15+
may be used to endorse or promote products derived from this software
16+
without specific prior written permission.
17+
18+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
22+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
30+
Original django-dotenv
131
Copyright (c) 2013, Jacob Kaplan-Moss
232

333
All rights reserved.

README.md

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,101 @@
1-
# django-dotenv
1+
# django-dotenv-rw
2+
3+
Forked from the simpler [django-dotenv](https://github.com/jacobian/django-dotenv). Removes black magic, changes a few things, adds remote .env file management capabilities. Works as a drop-in replacement for django-dotenv (if you weren't using the black magic trick).
24

35
[foreman](https://github.com/ddollar/foreman) reads from `.env`. `manage.py`
46
doesn't. Let's fix that.
57

8+
[heroku config](https://devcenter.heroku.com/articles/config-vars)Lets you add/delete env variables on your remote server from your local command line. django-dotenv-rw when used with fabric lets you do the same ```heroku config:set DJANGO_ENV="PRODUCTION" ``` becomes ```fab config:set,DJANGO_ENV,PRODUCTION```
9+
610
## Installation
711

812
```
9-
pip install django-dotenv
13+
pip install pip install git+ssh://git@github.com/tedtieken/django-dotenv-rw.git
1014
```
1115

12-
## Usage
16+
## Usage: loading settins from a .env file into your django environment
1317

14-
Pop open `manage.py`. Add:
18+
Option 1 (suggested): Near the top of `settings.py`. Add:
1519

1620
```
21+
import os
1722
import dotenv
18-
dotenv.read_dotenv()
23+
PROJECT_PATH = os.path.dirname(os.path.dirname(__file__))
24+
dotenv.load_dotenv(os.path.join(PROJECT_PATH, ".env"))
25+
```
26+
27+
Option 2: If you want your server to set the env variables and only use `dotenv` when you're using `manage.py`: in `manage.py` add:
28+
```
29+
import dotenv
30+
dotenv.load_dotenv(os.path.join(os.path.dirname(__file__), ".env"))
31+
```
32+
33+
34+
## Usage: setting remote config
35+
36+
This is a first pass, will likely change.
37+
38+
Put the `dotenv.py` file in the same folder on your server as the `.env` file and `manage.py`. Then you can add a config task to your local fabfile:
39+
```
40+
@task
41+
def config(action=None,key=None,value=None):
42+
command = env.django_path + "dotenv.py "
43+
command += env.django_path + ".env "
44+
command += action + " " if action else ""
45+
command += key + " " if key else ""
46+
command += value + " " if value else ""
47+
python(command)
48+
49+
```
50+
51+
Usage is designed to mirror the heroku config api very closely.
52+
53+
Get all your remote config info with `fab config`
54+
```
55+
$ fab config
56+
[...webfactional.com] Executing task 'config'
57+
[...webfactional.com] run: python2.7 /home/me/webapps/myapp/myapp/dotenv.py /home/me/webapps/myapp/myapp/.env
58+
[...webfactional.com] out: DJANGO_DEBUG="true"
59+
[...webfactional.com] out: DJANGO_ENV="test"
60+
```
61+
62+
Set remote config variables with `fab config:set,[key],[value]`
63+
```
64+
$ fab config:set,hello,world
65+
[...webfactional.com] Executing task 'config'
66+
[...webfactional.com] run: python2.7 /home/me/webapps/myapp/myapp/dotenv.py /home/me/webapps/myapp/myapp/.env set hello world
67+
[...webfactional.com] out: hello: world
68+
```
69+
70+
Get a single remote config variables with `fab config:get,[key]`
71+
```
72+
$ fab config:get,hello,world
73+
[...webfactional.com] Executing task 'config'
74+
[...webfactional.com] run: python2.7 /home/me/webapps/myapp/myapp/dotenv.py /home/me/webapps/myapp/myapp/.env get hello
75+
[...webfactional.com] out: hello
76+
[...webfactional.com] out: world
1977
```
2078

21-
You can also pass `read_dotenv()` an explicit path to the .env file, or to the directory where it lives. It's smart, it'll figure it out.
79+
Delete a remote config variables with `fab config:unset,[key]`
80+
```
81+
$ fab config:unset,hello
82+
[...webfactional.com] Executing task 'config'
83+
[...webfactional.com] run: python2.7 /home/me/webapps/myapp/myapp/dotenv.py /home/me/webapps/myapp/myapp/.env unset hello
84+
[...webfactional.com] out: unset hello
85+
```
86+
87+
Thanks entirely to fabric and not one bit to this project, you can chain commands like so`fab config:set,[key1],[value1] config:set,[key2],[value2]`
88+
```
89+
$ fab config:set,hello,world config:set,foo,bar config:set,fizz,buzz
90+
[...webfactional.com] Executing task 'config'
91+
[...webfactional.com] run: python2.7 /home/me/webapps/myapp/myapp/dotenv.py /home/me/webapps/myapp/myapp/.env set hello world
92+
[...webfactional.com] out: hello: world
93+
[...webfactional.com] Executing task 'config'
94+
[...webfactional.com] run: python2.7 /home/me/webapps/myapp/myapp/dotenv.py /home/me/webapps/myapp/myapp/.env set foo bar
95+
[...webfactional.com] out: foo: bar
96+
[...webfactional.com] Executing task 'config'
97+
[...webfactional.com] run: python2.7 /home/me/webapps/myapp/myapp/dotenv.py /home/me/webapps/myapp/myapp/.env set fizz buzz
98+
[...webfactional.com] out: fizz: buzz
99+
```
22100

23-
That's it. Now go 12 factor the crap out of something.
101+
That's it. Webfaction, or whoever your non-paas host is, is now 1 facor closer to an easy 12 factor app.

dotenv.py

Lines changed: 137 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,147 @@
11
import os
22
import sys
33
import warnings
4+
from collections import OrderedDict
45

5-
def read_dotenv(dotenv=None):
6+
def load_dotenv(dotenv_path):
67
"""
7-
Read a .env file into os.environ.
8-
9-
If not given a path to a dotenv path, does filthy magic stack backtracking
10-
to find manage.py and then find the dotenv.
8+
Read a .env file and load into os.environ.
119
"""
12-
if dotenv is None:
13-
frame = sys._getframe()
14-
dotenv = os.path.join(os.path.dirname(frame.f_back.f_code.co_filename), '.env')
15-
if not os.path.exists(dotenv):
16-
warnings.warn("not reading %s - it doesn't exist." % dotenv)
17-
return
18-
for k, v in parse_dotenv(dotenv):
10+
if not os.path.exists(dotenv_path):
11+
warnings.warn("can't read %s - it doesn't exist." % dotenv_path)
12+
return None
13+
for k, v in parse_dotenv(dotenv_path):
1914
os.environ.setdefault(k, v)
15+
return True
16+
17+
18+
def read_dotenv(dotenv_path):
19+
warnings.warn("read_dotenv deprecated, use load_dotenv instead")
20+
return load_dotenv(dotenv_path)
2021

21-
def write_dotenv(dotenv=None):
22+
23+
def get_key(dotenv_path, key_to_get):
24+
"""
25+
Gets the value of a given key from the given .env
2226
27+
If the .env path given doesn't exist, fails
28+
"""
29+
key_to_get = str(key_to_get)
30+
if not os.path.exists(dotenv_path):
31+
warnings.warn("can't read %s - it doesn't exist." % dotenv_path)
32+
return None
33+
dotenv_as_dict = OrderedDict(parse_dotenv(dotenv_path))
34+
if dotenv_as_dict.has_key(key_to_get):
35+
return dotenv_as_dict[key_to_get]
36+
else:
37+
warnings.warn("key %s not found in %s." % (key_to_get, dotenv_path))
38+
return None
39+
40+
41+
def set_key(dotenv_path, key_to_set, value_to_set):
42+
"""
43+
Adds or Updates a key/value to the given .env
44+
45+
If the .env path given doesn't exist, fails instead of risking creating
46+
an orphan .env somewhere in the filesystem
47+
"""
48+
key_to_set = str(key_to_set)
49+
value_to_set = str(value_to_set).strip("'").strip('"')
50+
if not os.path.exists(dotenv_path):
51+
warnings.warn("can't write to %s - it doesn't exist." % dotenv_path)
52+
return None
53+
dotenv_as_dict = OrderedDict(parse_dotenv(dotenv_path))
54+
dotenv_as_dict[key_to_set] = value_to_set
55+
success = flatten_and_write(dotenv_path, dotenv_as_dict)
56+
return success, key_to_set, value_to_set
2357

24-
def parse_dotenv(dotenv):
25-
for line in open(dotenv):
26-
line = line.strip()
27-
if not line or line.startswith('#') or '=' not in line:
28-
continue
29-
k, v = line.split('=', 1)
30-
v = v.strip("'").strip('"')
31-
yield k, v
58+
59+
def unset_key(dotenv_path, key_to_unset):
60+
"""
61+
Removes a given key from the given .env
62+
63+
If the .env path given doesn't exist, fails
64+
If the given key doesn't exist in the .env, fails
65+
"""
66+
key_to_unset = str(key_to_unset)
67+
if not os.path.exists(dotenv_path):
68+
warnings.warn("can't delete from %s - it doesn't exist." % dotenv_path)
69+
return None
70+
dotenv_as_dict = OrderedDict(parse_dotenv(dotenv_path))
71+
if dotenv_as_dict.has_key(key_to_unset):
72+
dotenv_as_dict.pop(key_to_unset, None)
73+
else:
74+
warnings.warn("key %s not removed from %s - key doesn't exist." % (key_to_unset, dotenv_path))
75+
return None
76+
success = flatten_and_write(dotenv_path, dotenv_as_dict)
77+
return success, key_to_unset
78+
79+
80+
def parse_dotenv(dotenv_path):
81+
with open(dotenv_path) as f:
82+
for line in f:
83+
line = line.strip()
84+
if not line or line.startswith('#') or '=' not in line:
85+
continue
86+
k, v = line.split('=', 1)
87+
v = v.strip("'").strip('"')
88+
yield k, v
89+
90+
91+
def flatten_and_write(dotenv_path, dotenv_as_dict):
92+
with open(dotenv_path, "w") as f:
93+
for k, v in dotenv_as_dict.items():
94+
f.write('%s="%s"\r\n' % (k, v))
95+
return True
96+
97+
if __name__ == "__main__":
98+
import argparse
99+
parser = argparse.ArgumentParser()
100+
parser.add_argument("file_path", help="the absolute path of the .env file you want to use")
101+
parser.add_argument("action", help="what you want to do with the .env file (get, set, unset)", nargs='?')
102+
parser.add_argument("key", help="the environment key you want to set", nargs='?')
103+
parser.add_argument("value", help="the value you want to set 'key' to", nargs='?')
104+
parser.add_argument("--force", help="force writing even if the file at the given path doesn't end in .env")
105+
args = parser.parse_args()
106+
107+
if not os.path.exists(args.file_path):
108+
warnings.warn("there doesn't appear to be a file at %s" % args.file_path)
109+
exit(1)
110+
if not args.force:
111+
if not args.file_path.endswith(".env"):
112+
warnings.warn("the file %s doesn't appear to be a .env file, use --force to proceed" % args.file_path)
113+
exit(1)
114+
115+
if args.action == None:
116+
with open(args.file_path) as f:
117+
print f.read()
118+
exit(0)
119+
elif args.action == "get":
120+
stored_value = get_key(args.file_path, args.key)
121+
if stored_value != None:
122+
print(args.key)
123+
print(stored_value)
124+
else:
125+
exit(1)
126+
elif args.action == "set":
127+
success, key, value = set_key(args.file_path, args.key, args.value)
128+
if success != None:
129+
print("%s: %s" % (key, value))
130+
else:
131+
exit(1)
132+
elif args.action == "unset":
133+
success, key = unset_key(args.file_path, args.key)
134+
if success != None:
135+
print("unset %s" % key)
136+
else:
137+
exit(1)
138+
# Need to investigate if this can actually work or if the scope of the new environ variables
139+
# Expires when python exits
140+
#
141+
# elif args.action == "load":
142+
# success = load_dotenv(args.file_path)
143+
# if success != None:
144+
# print("loaded %s into environment" % args.file_path)
145+
# else:
146+
# exit(1)
147+
exit(0)

setup.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from setuptools import setup
22

33
setup(
4-
name = "django-dotenv",
5-
description = "foreman reads from .env. manage.py doesn't. Let's fix that.",
6-
version = "1.2",
7-
author = "Jacob Kaplan-Moss",
8-
author_email = "jacob@jacobian.org",
9-
url = "http://github.com/jacobian/django-dotenv",
4+
name = "django-dotenv-rw",
5+
description = ".env file settings for non-heroku setups",
6+
version = "0.1",
7+
author = "Ted Tieken",
8+
author_email = "ted@sittingaround.com",
9+
url = "http://github.com/tedtieken/django-dotenv-rw",
1010
py_modules = ['dotenv'],
1111
classifiers = [
12-
'Development Status :: 4 - Beta',
12+
'Development Status :: 3 - Alpha',
1313
'Environment :: Web Environment',
1414
'Intended Audience :: Developers',
1515
'License :: OSI Approved :: BSD License',

0 commit comments

Comments
 (0)