1
# Copyright 2010 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Returns rules defining which features are active"""
7
'DuplicatePriorityError',
9
'NullFeatureRuleSource',
10
'StormFeatureRuleSource',
15
from collections import (
21
from storm.locals import Desc
23
from canonical.launchpad.webapp import adapter
24
from lp.services.features.model import (
29
# A convenient mapping for a feature flag rule in the database.
30
Rule = namedtuple("Rule", "flag scope priority value")
33
class DuplicatePriorityError(Exception):
35
def __init__(self, flag, priority):
37
self.priority = priority
40
return 'duplicate priority for flag "%s": %d' % (
41
self.flag, self.priority)
44
class FeatureRuleSource(object):
45
"""Access feature rule sources from the database or elsewhere."""
47
def getAllRulesAsDict(self):
48
"""Return all rule definitions.
50
:returns: dict from flag name to a list of
51
(scope, priority, value)
52
in descending order by priority.
55
for (flag, scope, priority, value) in self.getAllRulesAsTuples():
56
d.setdefault(str(flag), []).append((str(scope), priority, value))
59
def getAllRulesAsTuples(self):
60
"""Generate list of (flag, scope, priority, value)"""
61
raise NotImplementedError()
63
def getAllRulesAsText(self):
64
"""Return a text for of the rules.
66
This has one line per rule, with tab-separate
67
(flag, scope, prioirity, value), as used in the flag editor web
71
for (flag, scope, priority, value) in self.getAllRulesAsTuples():
72
tr.append('\t'.join((flag, scope, str(priority), value)))
76
def setAllRulesFromText(self, text_form):
77
"""Update all rules from text input.
79
The input is similar in form to that generated by getAllRulesAsText:
80
one line per rule, with whitespace-separated (flag, scope,
81
priority, value). Whitespace is allowed in the flag value.
84
self.setAllRules(self.parseRules(text_form))
86
def parseRules(self, text_form):
87
"""Return a list of tuples for the parsed form of the text input.
89
For each non-blank line gives back a tuple of
90
(flag, scope, priority, value).
92
Returns a list rather than a generator so that you see any syntax
96
seen_priorities = defaultdict(set)
97
for line in text_form.splitlines():
98
if line.strip() == '':
100
flag, scope, priority_str, value = re.split('[ \t]+', line, 3)
101
priority = int(priority_str)
102
r.append((flag, scope, priority, unicode(value)))
103
if priority in seen_priorities[flag]:
104
raise DuplicatePriorityError(flag, priority)
105
seen_priorities[flag].add(priority)
110
class StormFeatureRuleSource(FeatureRuleSource):
111
"""Access feature rules stored in the database via Storm.
114
def getAllRulesAsTuples(self):
116
# This LBYL may look odd but it is needed. Rendering OOPSes and
117
# timeouts also looks up flags, but doing such a lookup can
118
# will cause a doom if the db request is not executed or is
119
# canceled by the DB - and then results in a failure in
120
# zope.app.publications.ZopePublication.handleError when it
121
# calls transaction.commit.
122
# By Looking this up first, we avoid this and also permit
123
# code using flags to work in timed out requests (by appearing to
125
adapter.get_request_remaining_seconds()
126
except adapter.RequestExpired:
128
store = getFeatureStore()
133
Desc(FeatureFlag.priority)))
135
yield Rule(str(r.flag), str(r.scope), r.priority, r.value)
137
def setAllRules(self, new_rules):
138
"""Replace all existing rules with a new set.
140
:param new_rules: List of (name, scope, priority, value) tuples.
142
# XXX: would be slightly better to only update rules as necessary so
143
# we keep timestamps, and to avoid the direct sql etc -- mbp 20100924
144
store = getFeatureStore()
145
store.execute('DELETE FROM FeatureFlag')
146
for (flag, scope, priority, value) in new_rules:
147
store.add(FeatureFlag(
148
scope=unicode(scope),
155
class NullFeatureRuleSource(FeatureRuleSource):
156
"""For use in testing: everything is turned off"""
158
def getAllRulesAsTuples(self):