Skip to content

Commit 7d3879e

Browse files
committed
Merge branch 'feature/12-factor' into develop
* Allow MFR config keys to be overridden by environment variables. This will help support dockerizing the OSF environment.
2 parents 486c0fd + 46f9f25 commit 7d3879e

File tree

10 files changed

+125
-70
lines changed

10 files changed

+125
-70
lines changed

dev-requirements.txt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
-e git+https://github.com/centerforopenscience/aiohttpretty.git@0.0.2#egg=aiohttpretty
44
colorlog==2.5.0
5-
flake8==2.3.0
5+
flake8==3.0.4
66
ipdb
77
mccabe
8-
pep8
8+
pydevd==0.0.6
99
pyflakes
1010
pytest==2.8.2
1111
pytest-cov==2.2.0
1212
pyzmq==14.4.1
13-
14-

mfr/extensions/image/settings.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
try:
2-
from mfr import settings
3-
except ImportError:
4-
settings = {}
1+
from mfr import settings
52

6-
config = settings.get('IMAGE_EXTENSION_CONFIG', {})
3+
4+
config = settings.child('IMAGE_EXTENSION_CONFIG')
75

86
EXPORT_TYPE = config.get('EXPORT_TYPE', 'jpeg')
97
EXPORT_MAXIMUM_SIZE = config.get('EXPORT_MAXIMUM_SIZE', '1200x1200')

mfr/extensions/pdb/settings.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
try:
2-
from mfr import settings
3-
except ImportError:
4-
settings = {}
5-
6-
config = settings.get('PDB_EXTENSION_CONFIG', {})
1+
from mfr import settings
72

3+
config = settings.child('PDB_EXTENSION_CONFIG')
84

95
OPTIONS = config.get('OPTIONS', {
106
'width': 'auto',

mfr/extensions/tabular/settings.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1+
from mfr import settings
12
from mfr.extensions.tabular import libs
23

34

4-
try:
5-
from mfr import settings
6-
except ImportError:
7-
settings = {}
5+
config = settings.child('TABULAR_EXTENSION_CONFIG')
86

9-
config = settings.get('TABULAR_EXTENSION_CONFIG', {})
10-
11-
12-
MAX_SIZE = config.get('MAX_SIZE', 10000)
13-
TABLE_WIDTH = config.get('TABLE_WIDTH', 700)
14-
TABLE_HEIGHT = config.get('TABLE_HEIGHT', 600)
7+
MAX_SIZE = int(config.get('MAX_SIZE', 10000))
8+
TABLE_WIDTH = int(config.get('TABLE_WIDTH', 700))
9+
TABLE_HEIGHT = int(config.get('TABLE_HEIGHT', 600))
1510

1611
LIBS = config.get('LIBS', {
1712
'.csv': [libs.csv_stdlib],

mfr/extensions/unoconv/settings.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import os
22

3-
try:
4-
from mfr import settings
5-
except ImportError:
6-
settings = {}
3+
from mfr import settings
74

8-
config = settings.get('UNOCONV_EXTENSION_CONFIG', {})
5+
6+
config = settings.child('UNOCONV_EXTENSION_CONFIG')
97

108
UNOCONV_BIN = config.get('UNOCONV_BIN', '/usr/bin/unoconv')
119

mfr/providers/http/settings.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
try:
2-
from mfr import settings
3-
except ImportError:
4-
settings = {}
1+
from mfr import settings
52

6-
config = settings.get('HTTP_PROVIDER_CONFIG', {})
3+
4+
config = settings.child('HTTP_PROVIDER_CONFIG')

mfr/providers/osf/settings.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
try:
2-
from mfr import settings
3-
except ImportError:
4-
settings = {}
1+
from mfr import settings
52

6-
config = settings.get('OSF_PROVIDER_CONFIG', {})
73

8-
9-
# BASE_URL = config.get('BASE_URL', 'http://localhost:5001/')
4+
config = settings.child('OSF_PROVIDER_CONFIG')
105

116
MFR_IDENTIFYING_HEADER = config.get('MFR_IDENTIFYING_HEADER', 'X-Cos-Mfr-Render-Request')

mfr/server/settings.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,52 @@
22

33
import furl
44

5+
from mfr import settings
56

6-
try:
7-
from mfr import settings
8-
except ImportError:
9-
settings = {}
10-
11-
config = settings.get('SERVER_CONFIG', {})
127

8+
config = settings.child('SERVER_CONFIG')
139

1410
STATIC_PATH = config.get('STATIC_PATH', os.path.join(os.path.dirname(__file__), 'static'))
1511

1612
ADDRESS = config.get('ADDRESS', 'localhost')
1713
PORT = config.get('PORT', 7778)
1814

19-
DEBUG = config.get('DEBUG', False)
15+
DEBUG = config.get_bool('DEBUG', False)
2016

21-
SSL_CERT_FILE = config.get('SSL_CERT_FILE', None)
22-
SSL_KEY_FILE = config.get('SSL_KEY_FILE', None)
17+
SSL_CERT_FILE = config.get_nullable('SSL_CERT_FILE', None)
18+
SSL_KEY_FILE = config.get_nullable('SSL_KEY_FILE', None)
2319

24-
XHEADERS = config.get('XHEADERS', False)
20+
XHEADERS = config.get_bool('XHEADERS', False)
2521
CORS_ALLOW_ORIGIN = config.get('CORS_ALLOW_ORIGIN', '*')
2622

27-
CHUNK_SIZE = config.get('CHUNK_SIZE', 65536) # 64KB
28-
MAX_BUFFER_SIZE = config.get('MAX_BUFFER_SIZE', 1024 * 1024 * 100) # 100MB
23+
CHUNK_SIZE = int(config.get('CHUNK_SIZE', 65536)) # 64KB
24+
MAX_BUFFER_SIZE = int(config.get('MAX_BUFFER_SIZE', 1024 * 1024 * 100)) # 100MB
2925

3026
PROVIDER_NAME = config.get('PROVIDER_NAME', 'osf')
3127

32-
CACHE_ENABLED = config.get('CACHE_ENABLED', False)
28+
CACHE_ENABLED = config.get_bool('CACHE_ENABLED', False)
3329
CACHE_PROVIDER_NAME = config.get('CACHE_PROVIDER_NAME', 'filesystem')
3430
CACHE_PROVIDER_SETTINGS = config.get('CACHE_PROVIDER_SETTINGS', {'folder': '/tmp/mfr/'})
3531
CACHE_PROVIDER_CREDENTIALS = config.get('CACHE_PROVIDER_CREDENTIALS', {})
3632

3733
LOCAL_CACHE_PROVIDER_SETTINGS = config.get('LOCAL_CACHE_PROVIDER_SETTINGS', {'folder': '/tmp/mfrlocalcache/'})
3834

39-
ALLOWED_PROVIDER_DOMAINS = config.get('ALLOWED_PROVIDER_DOMAINS', ['http://localhost:5000/', 'http://localhost:7777/'])
35+
ALLOWED_PROVIDER_DOMAINS = config.get('ALLOWED_PROVIDER_DOMAINS', 'http://localhost:5000/ http://localhost:7777/').split(' ')
4036
ALLOWED_PROVIDER_NETLOCS = []
4137
for domain in ALLOWED_PROVIDER_DOMAINS:
4238
ALLOWED_PROVIDER_NETLOCS.append(furl.furl(domain).netloc)
4339

4440

45-
analytics_config = config.get('ANALYTICS', {})
41+
analytics_config = config.child('ANALYTICS')
4642

47-
keen_config = analytics_config.get('KEEN', {})
43+
keen_config = analytics_config.child('KEEN')
4844
KEEN_API_BASE_URL = keen_config.get('API_BASE_URL', 'https://api.keen.io')
4945
KEEN_API_VERSION = keen_config.get('API_VERSION', '3.0')
5046

51-
keen_private_config = keen_config.get('PRIVATE', {})
52-
KEEN_PRIVATE_PROJECT_ID = keen_private_config.get('PROJECT_ID', None)
53-
KEEN_PRIVATE_WRITE_KEY = keen_private_config.get('WRITE_KEY', None)
47+
keen_private_config = keen_config.child('PRIVATE')
48+
KEEN_PRIVATE_PROJECT_ID = keen_private_config.get_nullable('PROJECT_ID', None)
49+
KEEN_PRIVATE_WRITE_KEY = keen_private_config.get_nullable('WRITE_KEY', None)
5450

55-
keen_public_config = keen_config.get('PUBLIC', {})
56-
KEEN_PUBLIC_PROJECT_ID = keen_public_config.get('PROJECT_ID', None)
57-
KEEN_PUBLIC_WRITE_KEY = keen_public_config.get('WRITE_KEY', None)
51+
keen_public_config = keen_config.child('PUBLIC')
52+
KEEN_PUBLIC_PROJECT_ID = keen_public_config.get_nullable('PROJECT_ID', None)
53+
KEEN_PUBLIC_WRITE_KEY = keen_public_config.get_nullable('WRITE_KEY', None)

mfr/settings.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,80 @@
44
import logging.config
55

66

7+
class SettingsDict(dict):
8+
"""Allow overriding on-disk config via environment variables. Normal config is done with a
9+
hierarchical dict::
10+
11+
"SERVER_CONFIG": {
12+
"HOST": "http://localhost:7777"
13+
}
14+
15+
``HOST`` can be retrieved in the python code with::
16+
17+
config = SettingsDict(json.load('local-config.json'))
18+
server_cfg = config.child('SERVER_CONFIG')
19+
host = server_cfg.get('HOST')
20+
21+
To override a value, join all of the parent keys and the child keys with an underscore::
22+
23+
$ SERVER_CONFIG_HOST='http://foo.bar.com' invoke server
24+
25+
Nested dicts can be handled with the ``.child()`` method. Config keys will be all parent keys
26+
joined by underscores::
27+
28+
"SERVER_CONFIG": {
29+
"ANALYTICS": {
30+
"PROJECT_ID": "foo"
31+
}
32+
}
33+
34+
The corresponding envvar for ``PROJECT_ID`` would be ``SERVER_CONFIG_ANALYTICS_PROJECT_ID``.
35+
"""
36+
37+
def __init__(self, *args, parent=None, **kwargs):
38+
self.parent = parent
39+
super().__init__(*args, **kwargs)
40+
41+
def get(self, key, default=None):
42+
"""Fetch a config value for ``key`` from the settings. First checks the env, then the
43+
on-disk config. If neither exists, returns ``default``."""
44+
env = self.full_key(key)
45+
if env in os.environ:
46+
return os.environ.get(env)
47+
return super().get(key, default)
48+
49+
def get_bool(self, key, default=None):
50+
"""Fetch a config value and interpret as a bool. Since envvars are always strings,
51+
interpret '0' and the empty string as False and '1' as True. Anything else is probably
52+
an acceident, so die screaming."""
53+
value = self.get(key, default)
54+
if value in [False, 0, '0', '']:
55+
retval = False
56+
elif value in [True, 1, '1']:
57+
retval = True
58+
else:
59+
raise Exception(
60+
'{} should be a truthy value, but instead we got {}'.format(
61+
self.full_key(key), value
62+
)
63+
)
64+
return retval
65+
66+
def get_nullable(self, key, default=None):
67+
"""Fetch a config value and interpret the empty string as None. Useful for external code
68+
that expects an explicit None."""
69+
value = self.get(key, default)
70+
return None if value == '' else value
71+
72+
def full_key(self, key):
73+
"""The name of the envvar which corresponds to this key."""
74+
return '{}_{}'.format(self.parent, key) if self.parent else key
75+
76+
def child(self, key):
77+
"""Fetch a sub-dict of the current dict."""
78+
return SettingsDict(self.get(key, {}), parent=self.full_key(key))
79+
80+
781
PROJECT_NAME = 'mfr'
882
PROJECT_CONFIG_PATH = '~/.cos'
983

@@ -61,21 +135,21 @@
61135
config_path = '{}/{}-{}.json'.format(PROJECT_CONFIG_PATH, PROJECT_NAME, env)
62136

63137

64-
config = {}
138+
config = SettingsDict()
65139
config_path = os.path.expanduser(config_path)
66140
if not os.path.exists(config_path):
67141
logging.warning('No \'{}\' configuration file found'.format(config_path))
68142
else:
69143
with open(os.path.expanduser(config_path)) as fp:
70-
config = json.load(fp)
144+
config = SettingsDict(json.load(fp))
71145

72146

73-
def get(key, default):
74-
return config.get(key, default)
147+
def child(key):
148+
return config.child(key)
75149

76150

77-
logging_config = get('LOGGING', DEFAULT_LOGGING_CONFIG)
151+
logging_config = config.get('LOGGING', DEFAULT_LOGGING_CONFIG)
78152
logging.config.dictConfig(logging_config)
79153

80154

81-
SENTRY_DSN = get('SENTRY_DSN', None)
155+
SENTRY_DSN = config.get_nullable('SENTRY_DSN', None)

tasks.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,12 @@ def test(verbose=False):
5959
@task
6060
def server():
6161
monkey_patch()
62+
63+
if os.environ.get('REMOTE_DEBUG', None):
64+
import pydevd
65+
# e.g. '127.0.0.1:5678'
66+
remote_parts = os.environ.get('REMOTE_DEBUG').split(':')
67+
pydevd.settrace(remote_parts[0], port=int(remote_parts[1]), suspend=False, stdoutToServer=True, stderrToServer=True)
68+
6269
from mfr.server.app import serve
6370
serve()

0 commit comments

Comments
 (0)