Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

st2common/st2common/operators.py (3 issues)

1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
from __future__ import absolute_import
17
import re
18
import six
19
import fnmatch
20
21
from st2common.util import date as date_utils
22
from st2common.constants.rules import TRIGGER_ITEM_PAYLOAD_PREFIX
23
from st2common.util.payload import PayloadLookup
24
25
__all__ = [
26
    'SEARCH',
27
    'get_operator',
28
    'get_allowed_operators',
29
    'UnrecognizedConditionError',
30
]
31
32
33
def get_allowed_operators():
34
    return operators
35
36
37
def get_operator(op):
38
    op = op.lower()
39
    if op in operators:
40
        return operators[op]
41
    else:
42
        raise Exception('Invalid operator: ' + op)
43
44
45
class UnrecognizedConditionError(Exception):
46
    pass
47
48
49
# Operation implementations
50
51
52
def search(value, criteria_pattern, criteria_condition, check_function):
53
    """
54
    Search a list of values that match all child criteria. If condition is 'any', return a
55
    successful match if any items match all child criteria. If condition is 'all', return a
56
    successful match if ALL items match all child criteria.
57
58
    value: the payload list to search
59
    condition: one of:
60
      * any - return true if any items of the list match and false if none of them match
61
      * all - return true if all items of the list match and false if any of them do not match
62
    pattern: a dictionary of criteria to apply to each item of the list
63
64
    This operator has O(n) algorithmic complexity in terms of number of child patterns.
65
    This operator has O(n) algorithmic complexity in terms of number of payload fields.
66
67
    However, it has O(n_patterns * n_payloads) algorithmic complexity, where:
68
      n_patterns = number of child patterns
69
      n_payloads = number of fields in payload
70
    It is therefore very easy to write a slow rule when using this operator.
71
72
    This operator should ONLY be used when trying to match a small number of child patterns and/or
73
    a small number of payload list elements.
74
75
    Other conditions (such as 'count', 'count_gt', 'count_gte', etc.) can be added as needed.
76
77
    Data from the trigger:
78
79
    {
80
        "fields": [
81
            {
82
                "field_name": "Status",
83
                "to_value": "Approved"
84
            }
85
        ]
86
    }
87
88
    And an example usage in criteria:
89
90
    ---
91
    criteria:
92
      trigger.fields:
93
        type: search
94
        # Controls whether this criteria has to match any or all items of the list
95
        condition: any  # or all
96
        pattern:
97
          # Here our context is each item of the list
98
          # All of these patterns have to match the item for the item to match
99
          # These are simply other operators applied to each item in the list
100
          item.field_name:
101
            type: "equals"
102
            pattern: "Status"
103
104
          item.to_value:
105
            type: "equals"
106
            pattern: "Approved"
107
    """
108
    if criteria_condition == 'any':
109
        # Any item of the list can match all patterns
110
        rtn = any([
111
            # Any payload item can match
112
            all([
113
                # Match all patterns
114
                check_function(
115
                    child_criterion_k, child_criterion_v,
116
                    PayloadLookup(child_payload, prefix=TRIGGER_ITEM_PAYLOAD_PREFIX))
117
                for child_criterion_k, child_criterion_v in six.iteritems(criteria_pattern)
118
            ])
119
            for child_payload in value
120
        ])
121
    elif criteria_condition == 'all':
122
        # Every item of the list must match all patterns
123
        rtn = all([
124
            # All payload items must match
125
            all([
126
                # Match all patterns
127
                check_function(
128
                    child_criterion_k, child_criterion_v,
129
                    PayloadLookup(child_payload, prefix=TRIGGER_ITEM_PAYLOAD_PREFIX))
130
                for child_criterion_k, child_criterion_v in six.iteritems(criteria_pattern)
131
            ])
132
            for child_payload in value
133
        ])
134
    else:
135
        raise UnrecognizedConditionError("The '%s' search condition is not recognized, only 'any' "
136
                                         "and 'all' are allowed" % criteria_condition)
137
138
    return rtn
139
140
141
def equals(value, criteria_pattern):
142
    if criteria_pattern is None:
143
        return False
144
    return value == criteria_pattern
145
146
147
def nequals(value, criteria_pattern):
148
    return value != criteria_pattern
149
150
151
def iequals(value, criteria_pattern):
152
    if criteria_pattern is None:
153
        return False
154
    return value.lower() == criteria_pattern.lower()
155
156
157
def contains(value, criteria_pattern):
158
    if criteria_pattern is None:
159
        return False
160
    return criteria_pattern in value
161
162
163
def icontains(value, criteria_pattern):
164
    if criteria_pattern is None:
165
        return False
166
    return criteria_pattern.lower() in value.lower()
167
168
169
def ncontains(value, criteria_pattern):
170
    if criteria_pattern is None:
171
        return False
172
    return criteria_pattern not in value
173
174
175
def incontains(value, criteria_pattern):
176
    if criteria_pattern is None:
177
        return False
178
    return criteria_pattern.lower() not in value.lower()
179
180
181
def startswith(value, criteria_pattern):
182
    if criteria_pattern is None:
183
        return False
184
    return value.startswith(criteria_pattern)
185
186
187
def istartswith(value, criteria_pattern):
188
    if criteria_pattern is None:
189
        return False
190
    return value.lower().startswith(criteria_pattern.lower())
191
192
193
def endswith(value, criteria_pattern):
194
    if criteria_pattern is None:
195
        return False
196
    return value.endswith(criteria_pattern)
197
198
199
def iendswith(value, criteria_pattern):
200
    if criteria_pattern is None:
201
        return False
202
    return value.lower().endswith(criteria_pattern.lower())
203
204
205
def less_than(value, criteria_pattern):
206
    if criteria_pattern is None:
207
        return False
208
    return value < criteria_pattern
209
210
211
def greater_than(value, criteria_pattern):
212
    if criteria_pattern is None:
213
        return False
214
    return value > criteria_pattern
215
216
217
def match_wildcard(value, criteria_pattern):
218
    if criteria_pattern is None:
219
        return False
220
221
    return fnmatch.fnmatch(value, criteria_pattern)
222
223
224
def match_regex(value, criteria_pattern):
225
    # match_regex is deprecated, please use 'regex' and 'iregex'
226
    if criteria_pattern is None:
227
        return False
228
    regex = re.compile(criteria_pattern, re.DOTALL)
0 ignored issues
show
Comprehensibility Bug introduced by
regex is re-defining a name which is already available in the outer-scope (previously defined on line 233).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
229
    # check for a match and not for details of the match.
230
    return regex.match(value) is not None
231
232
233
def regex(value, criteria_pattern):
234
    if criteria_pattern is None:
235
        return False
236
    regex = re.compile(criteria_pattern)
0 ignored issues
show
Comprehensibility Bug introduced by
regex is re-defining a name which is already available in the outer-scope (previously defined on line 233).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
237
    # check for a match and not for details of the match.
238
    return regex.search(value) is not None
239
240
241
def iregex(value, criteria_pattern):
242
    if criteria_pattern is None:
243
        return False
244
    regex = re.compile(criteria_pattern, re.IGNORECASE)
0 ignored issues
show
Comprehensibility Bug introduced by
regex is re-defining a name which is already available in the outer-scope (previously defined on line 233).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
245
    # check for a match and not for details of the match.
246
    return regex.search(value) is not None
247
248
249
def _timediff(diff_target, period_seconds, operator):
250
    """
251
    :param diff_target: Date string.
252
    :type diff_target: ``str``
253
254
    :param period_seconds: Seconds.
255
    :type period_seconds: ``int``
256
257
    :rtype: ``bool``
258
    """
259
    # Pickup now in UTC to compare against
260
    utc_now = date_utils.get_datetime_utc_now()
261
262
    # assuming diff_target is UTC and specified in python iso format.
263
    # Note: date_utils.parse uses dateutil.parse which is way more flexible then strptime and
264
    # supports many date formats
265
    diff_target_utc = date_utils.parse(diff_target)
266
    return operator((utc_now - diff_target_utc).total_seconds(), period_seconds)
267
268
269
def timediff_lt(value, criteria_pattern):
270
    if criteria_pattern is None:
271
        return False
272
    return _timediff(diff_target=value, period_seconds=criteria_pattern, operator=less_than)
273
274
275
def timediff_gt(value, criteria_pattern):
276
    if criteria_pattern is None:
277
        return False
278
    return _timediff(diff_target=value, period_seconds=criteria_pattern, operator=greater_than)
279
280
281
def exists(value, criteria_pattern):
282
    return value is not None
283
284
285
def nexists(value, criteria_pattern):
286
    return value is None
287
288
289
def inside(value, criteria_pattern):
290
    if criteria_pattern is None:
291
        return False
292
    return value in criteria_pattern
293
294
295
def ninside(value, criteria_pattern):
296
    if criteria_pattern is None:
297
        return False
298
    return value not in criteria_pattern
299
300
301
# operator match strings
302
MATCH_WILDCARD = 'matchwildcard'
303
MATCH_REGEX = 'matchregex'
304
REGEX = 'regex'
305
IREGEX = 'iregex'
306
EQUALS_SHORT = 'eq'
307
EQUALS_LONG = 'equals'
308
NEQUALS_LONG = 'nequals'
309
NEQUALS_SHORT = 'neq'
310
IEQUALS_SHORT = 'ieq'
311
IEQUALS_LONG = 'iequals'
312
CONTAINS_LONG = 'contains'
313
ICONTAINS_LONG = 'icontains'
314
NCONTAINS_LONG = 'ncontains'
315
INCONTAINS_LONG = 'incontains'
316
STARTSWITH_LONG = 'startswith'
317
ISTARTSWITH_LONG = 'istartswith'
318
ENDSWITH_LONG = 'endswith'
319
IENDSWITH_LONG = 'iendswith'
320
LESS_THAN_SHORT = 'lt'
321
LESS_THAN_LONG = 'lessthan'
322
GREATER_THAN_SHORT = 'gt'
323
GREATER_THAN_LONG = 'greaterthan'
324
TIMEDIFF_LT_SHORT = 'td_lt'
325
TIMEDIFF_LT_LONG = 'timediff_lt'
326
TIMEDIFF_GT_SHORT = 'td_gt'
327
TIMEDIFF_GT_LONG = 'timediff_gt'
328
KEY_EXISTS = 'exists'
329
KEY_NOT_EXISTS = 'nexists'
330
INSIDE_LONG = 'inside'
331
INSIDE_SHORT = 'in'
332
NINSIDE_LONG = 'ninside'
333
NINSIDE_SHORT = 'nin'
334
SEARCH = 'search'
335
336
# operator lookups
337
operators = {
338
    MATCH_WILDCARD: match_wildcard,
339
    MATCH_REGEX: match_regex,
340
    REGEX: regex,
341
    IREGEX: iregex,
342
    EQUALS_SHORT: equals,
343
    EQUALS_LONG: equals,
344
    NEQUALS_SHORT: nequals,
345
    NEQUALS_LONG: nequals,
346
    IEQUALS_SHORT: iequals,
347
    IEQUALS_LONG: iequals,
348
    CONTAINS_LONG: contains,
349
    ICONTAINS_LONG: icontains,
350
    NCONTAINS_LONG: ncontains,
351
    INCONTAINS_LONG: incontains,
352
    STARTSWITH_LONG: startswith,
353
    ISTARTSWITH_LONG: istartswith,
354
    ENDSWITH_LONG: endswith,
355
    IENDSWITH_LONG: iendswith,
356
    LESS_THAN_SHORT: less_than,
357
    LESS_THAN_LONG: less_than,
358
    GREATER_THAN_SHORT: greater_than,
359
    GREATER_THAN_LONG: greater_than,
360
    TIMEDIFF_LT_SHORT: timediff_lt,
361
    TIMEDIFF_LT_LONG: timediff_lt,
362
    TIMEDIFF_GT_SHORT: timediff_gt,
363
    TIMEDIFF_GT_LONG: timediff_gt,
364
    KEY_EXISTS: exists,
365
    KEY_NOT_EXISTS: nexists,
366
    INSIDE_LONG: inside,
367
    INSIDE_SHORT: inside,
368
    NINSIDE_LONG: ninside,
369
    NINSIDE_SHORT: ninside,
370
    SEARCH: search,
371
}
372