This repository has been archived by the owner on Feb 7, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 25
/
__init__.py
189 lines (135 loc) · 5.56 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""
(c) 2013 Rachel Sanders. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from functools import wraps
import logging
from flask import abort, current_app, url_for
from flask import redirect as _redirect
__version__ = u'0.4dev'
log = logging.getLogger(u'flask-featureflags')
RAISE_ERROR_ON_MISSING_FEATURES = u'RAISE_ERROR_ON_MISSING_FEATURES'
FEATURE_FLAGS_CONFIG = u'FEATURE_FLAGS'
EXTENSION_NAME = "FeatureFlags"
class StopCheckingFeatureFlags(Exception):
""" Raise this inside of a feature flag handler to immediately return False and stop any further handers from running """
pass
class NoFeatureFlagFound(Exception):
""" Raise this when the feature flag does not exist. """
pass
def AppConfigFlagHandler(feature=None):
""" This is the default handler. It checks for feature flags in the current app's configuration.
For example, to have 'unfinished_feature' hidden in production but active in development:
config.py
class ProductionConfig(Config):
FEATURE_FLAGS = {
'unfinished_feature' : False,
}
class DevelopmentConfig(Config):
FEATURE_FLAGS = {
'unfinished_feature' : True,
}
"""
if not current_app:
log.warn(u"Got a request to check for {feature} but we're outside the request context. Returning False".format(feature=feature))
return False
try:
return current_app.config[FEATURE_FLAGS_CONFIG][feature]
except (AttributeError, KeyError):
raise NoFeatureFlagFound()
class FeatureFlag(object):
JINJA_TEST_NAME = u'active_feature'
def __init__(self, app=None):
if app is not None:
self.init_app(app)
# The default out-of-the-box handler looks up features in Flask's app config.
self.handlers = [AppConfigFlagHandler]
def init_app(self, app):
""" Add ourselves into the app config and setup, and add a jinja function test """
app.config.setdefault(FEATURE_FLAGS_CONFIG, {})
app.config.setdefault(RAISE_ERROR_ON_MISSING_FEATURES, False)
if hasattr(app, "add_template_test"):
# flask 0.10 and higher has a proper hook
app.add_template_test(self.check, name=self.JINJA_TEST_NAME)
else:
app.jinja_env.tests[self.JINJA_TEST_NAME] = self.check
if not hasattr(app, 'extensions'):
app.extensions = {}
app.extensions[EXTENSION_NAME] = self
def clear_handlers(self):
""" Clear all handlers. This effectively turns every feature off."""
self.handlers = []
def add_handler(self, function):
""" Add a new handler to the end of the chain of handlers. """
self.handlers.append(function)
def remove_handler(self, function):
""" Remove a handler from the chain of handlers. """
try:
self.handlers.remove(function)
except ValueError: # handler wasn't in the list, pretend we don't notice
pass
def check(self, feature):
""" Loop through all our feature flag checkers and return true if any of them are true.
The order of handlers matters - we will immediately return True if any handler returns true.
If you want to a handler to return False and stop the chain, raise the StopCheckingFeatureFlags exception."""
found = False
for handler in self.handlers:
try:
if handler(feature):
return True
except StopCheckingFeatureFlags:
return False
except NoFeatureFlagFound:
pass
else:
found = True
if not found:
if current_app.debug and current_app.config.get(RAISE_ERROR_ON_MISSING_FEATURES, False):
raise KeyError(u"No feature flag defined for {feature}".format(feature=feature))
else:
log.info(u"No feature flag defined for {feature}".format(feature=feature))
return False
def is_active(feature):
""" Check if a feature is active """
if current_app:
feature_flagger = current_app.extensions.get(EXTENSION_NAME)
if feature_flagger:
return feature_flagger.check(feature)
else:
raise AssertionError("Oops. This application doesn't have the Flask-FeatureFlag extention installed.")
else:
log.warn(u"Got a request to check for {feature} but we're running outside the request context. Check your setup. Returning False".format(feature=feature))
return False
def is_active_feature(feature, redirect_to=None, redirect=None):
"""
Decorator for Flask views. If a feature is off, it can either return a 404 or redirect to a URL if you'd rather.
"""
def _is_active_feature(func):
@wraps(func)
def wrapped(*args, **kwargs):
if not is_active(feature):
url = redirect_to
if redirect:
url = url_for(redirect)
if url:
log.debug(u'Feature {feature} is off, redirecting to {url}'.format(feature=feature, url=url))
return _redirect(url, code=302)
else:
log.debug(u'Feature {feature} is off, aborting request'.format(feature=feature))
abort(404)
return func(*args, **kwargs)
return wrapped
return _is_active_feature
# Silence that annoying No handlers could be found for logger "flask-featureflags"
class NullHandler(logging.Handler):
def emit(self, record):
pass
log.addHandler(NullHandler())