~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/services/features/rulesource.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-12-22 04:55:30 UTC
  • mfrom: (14577.1.1 testfix)
  • Revision ID: launchpad@pqm.canonical.com-20111222045530-wki9iu6c0ysqqwkx
[r=wgrant][no-qa] Fix test_publisherconfig lpstorm import. Probably a
        silent conflict between megalint and apocalypse.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2010 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
"""Returns rules defining which features are active"""
 
5
 
 
6
__all__ = [
 
7
    'DuplicatePriorityError',
 
8
    'FeatureRuleSource',
 
9
    'NullFeatureRuleSource',
 
10
    'StormFeatureRuleSource',
 
11
    ]
 
12
 
 
13
__metaclass__ = type
 
14
 
 
15
from collections import (
 
16
    defaultdict,
 
17
    namedtuple,
 
18
    )
 
19
import re
 
20
 
 
21
from storm.locals import Desc
 
22
 
 
23
from canonical.launchpad.webapp import adapter
 
24
from lp.services.features.model import (
 
25
    FeatureFlag,
 
26
    getFeatureStore,
 
27
    )
 
28
 
 
29
# A convenient mapping for a feature flag rule in the database.
 
30
Rule = namedtuple("Rule", "flag scope priority value")
 
31
 
 
32
 
 
33
class DuplicatePriorityError(Exception):
 
34
 
 
35
    def __init__(self, flag, priority):
 
36
        self.flag = flag
 
37
        self.priority = priority
 
38
 
 
39
    def __str__(self):
 
40
        return 'duplicate priority for flag "%s": %d' % (
 
41
            self.flag, self.priority)
 
42
 
 
43
 
 
44
class FeatureRuleSource(object):
 
45
    """Access feature rule sources from the database or elsewhere."""
 
46
 
 
47
    def getAllRulesAsDict(self):
 
48
        """Return all rule definitions.
 
49
 
 
50
        :returns: dict from flag name to a list of
 
51
            (scope, priority, value)
 
52
            in descending order by priority.
 
53
        """
 
54
        d = {}
 
55
        for (flag, scope, priority, value) in self.getAllRulesAsTuples():
 
56
            d.setdefault(str(flag), []).append((str(scope), priority, value))
 
57
        return d
 
58
 
 
59
    def getAllRulesAsTuples(self):
 
60
        """Generate list of (flag, scope, priority, value)"""
 
61
        raise NotImplementedError()
 
62
 
 
63
    def getAllRulesAsText(self):
 
64
        """Return a text for of the rules.
 
65
 
 
66
        This has one line per rule, with tab-separate
 
67
        (flag, scope, prioirity, value), as used in the flag editor web
 
68
        interface.
 
69
        """
 
70
        tr = []
 
71
        for (flag, scope, priority, value) in self.getAllRulesAsTuples():
 
72
            tr.append('\t'.join((flag, scope, str(priority), value)))
 
73
        tr.append('')
 
74
        return '\n'.join(tr)
 
75
 
 
76
    def setAllRulesFromText(self, text_form):
 
77
        """Update all rules from text input.
 
78
 
 
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.
 
82
 
 
83
        """
 
84
        self.setAllRules(self.parseRules(text_form))
 
85
 
 
86
    def parseRules(self, text_form):
 
87
        """Return a list of tuples for the parsed form of the text input.
 
88
 
 
89
        For each non-blank line gives back a tuple of
 
90
        (flag, scope, priority, value).
 
91
 
 
92
        Returns a list rather than a generator so that you see any syntax
 
93
        errors immediately.
 
94
        """
 
95
        r = []
 
96
        seen_priorities = defaultdict(set)
 
97
        for line in text_form.splitlines():
 
98
            if line.strip() == '':
 
99
                continue
 
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)
 
106
 
 
107
        return r
 
108
 
 
109
 
 
110
class StormFeatureRuleSource(FeatureRuleSource):
 
111
    """Access feature rules stored in the database via Storm.
 
112
    """
 
113
 
 
114
    def getAllRulesAsTuples(self):
 
115
        try:
 
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
 
124
            # have no rules).
 
125
            adapter.get_request_remaining_seconds()
 
126
        except adapter.RequestExpired:
 
127
            return
 
128
        store = getFeatureStore()
 
129
        rs = (store
 
130
                .find(FeatureFlag)
 
131
                .order_by(
 
132
                    FeatureFlag.flag,
 
133
                    Desc(FeatureFlag.priority)))
 
134
        for r in rs:
 
135
            yield Rule(str(r.flag), str(r.scope), r.priority, r.value)
 
136
 
 
137
    def setAllRules(self, new_rules):
 
138
        """Replace all existing rules with a new set.
 
139
 
 
140
        :param new_rules: List of (name, scope, priority, value) tuples.
 
141
        """
 
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),
 
149
                flag=unicode(flag),
 
150
                value=value,
 
151
                priority=priority))
 
152
        store.flush()
 
153
 
 
154
 
 
155
class NullFeatureRuleSource(FeatureRuleSource):
 
156
    """For use in testing: everything is turned off"""
 
157
 
 
158
    def getAllRulesAsTuples(self):
 
159
        return []