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
|
|||
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
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
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 |
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: