Added all common modules in conductor directory
[optf/has.git] / conductor / conductor / common / threshold.py
1 #
2 # -------------------------------------------------------------------------
3 #   Copyright (c) 2015-2017 AT&T Intellectual Property
4 #
5 #   Licensed under the Apache License, Version 2.0 (the "License");
6 #   you may not use this file except in compliance with the License.
7 #   You may obtain a copy of the License at
8 #
9 #       http://www.apache.org/licenses/LICENSE-2.0
10 #
11 #   Unless required by applicable law or agreed to in writing, software
12 #   distributed under the License is distributed on an "AS IS" BASIS,
13 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 #   See the License for the specific language governing permissions and
15 #   limitations under the License.
16 #
17 # -------------------------------------------------------------------------
18 #
19
20 import itertools
21
22 import six
23
24
25 class ThresholdException(Exception):
26     pass
27
28
29 def is_number(input):
30     """Returns True if the value is a number"""
31     try:
32         if type(input) is int or type(input) is float:
33             return True
34         elif isinstance(input, six.string_types) and float(input):
35             return True
36     except ValueError:
37         pass
38     return False
39
40
41 class Threshold(object):
42     OPERATORS = ['=', '<', '>', '<=', '>=']
43     UNITS = {
44         'currency': {
45             'USD': 1.0,
46         },
47         'time': {
48             'ms': 1.0,
49             'sec': 1000.0,
50         },
51         'distance': {
52             'km': 1.0,
53             'mi': 1.609344,
54         },
55         'throughput': {
56             'Kbps': 0.001,
57             'Mbps': 1.0,
58             'Gbps': 1000.0,
59         },
60     }
61
62     def __init__(self, expression, base_unit):
63         if not isinstance(expression, six.string_types):
64             raise ThresholdException("Expression must be a string")
65         if not isinstance(base_unit, six.string_types):
66             raise ThresholdException("Base unit must be a string")
67         if base_unit not in self.UNITS:
68             raise ThresholdException(
69                 "Base unit {} unsupported, must be one of: {}".format(
70                     base_unit, ', '.join(self.UNITS.keys())))
71
72         self._expression = expression
73         self._base_unit = base_unit
74         self._parse()
75
76     def __repr__(self):
77         """Object representation"""
78         return "<Threshold expression: '{}', base_unit: '{}', " \
79                "parts: {}>".format(self.expression, self.base_unit, self.parts)
80
81     def _all_units(self):
82         """Returns a single list of all supported units"""
83         unit_lists = [self.UNITS[k].keys() for k in self.UNITS.keys()]
84         return list(itertools.chain.from_iterable(unit_lists))
85
86     def _default_for_base_unit(self, base_unit):
87         """Returns the default unit (1.0 multiplier) for a given base unit
88
89         Returns None if not found.
90         """
91         units = self.UNITS.get(base_unit)
92         if units:
93             for name, multiplier in units.items():
94                 if multiplier == 1.0:
95                     return name
96         return None
97
98     def _multiplier_for_unit(self, unit):
99         """Returns the multiplier for a given unit
100
101         Returns None if not found.
102         """
103         return self.UNITS.get(self.base_unit).get(unit)
104
105     def _reset(self):
106         """Resets parsed components"""
107         self._operator = None
108         self._value = None
109         self._min_value = None
110         self._max_value = None
111         self._unit = None
112         self._parsed = False
113
114     def _parse(self):
115         """Parses the expression into parts"""
116         self._reset()
117         parts = self.expression.split()
118         for part in parts:
119             # Is it an operator?
120             if not self.operator and part in self.OPERATORS:
121                 if self.value:
122                     raise ThresholdException(
123                         "Value {} encountered before operator {} "
124                         "in expression '{}'".format(
125                             self.value, part, self.expression))
126                 if self.has_range:
127                     raise ThresholdException(
128                         "Range {}-{} encountered before operator {} "
129                         "in expression '{}'".format(
130                             self.min_value, self.max_value,
131                             part, self.expression))
132                 if self.unit:
133                     raise ThresholdException(
134                         "Unit '{}' encountered before operator {} "
135                         "in expression '{}'".format(
136                             self.unit, part, self.expression))
137
138                 self._operator = part
139
140             # Is it a lone value?
141             elif not self.value and is_number(part):
142                 if self.has_range:
143                     raise ThresholdException(
144                         "Range {}-{} encountered before value {} "
145                         "in expression '{}'".format(
146                             self.min_value, self.max_value,
147                             part, self.expression))
148                 if self.unit:
149                     raise ThresholdException(
150                         "Unit '{}' encountered before value {} "
151                         "in expression '{}'".format(
152                             self.unit, part, self.expression))
153                 self._value = float(part)
154                 if not self.operator:
155                     self._operator = '='
156
157             # Is it a value range?
158             elif not self.has_range and part.count('-') == 1:
159                 part1, part2 = part.split('-')
160                 if is_number(part1) and is_number(part2):
161                     if self.operator and self.operator != '=':
162                         raise ThresholdException(
163                             "Operator {} not supported with range {} "
164                             "in expression '{}'".format(
165                                 self.operator, part, self.expression))
166                     if self.value:
167                         raise ThresholdException(
168                             "Value {} encountered before range {} "
169                             "in expression '{}'".format(
170                                 self.value, part, self.expression))
171                     if self.unit:
172                         raise ThresholdException(
173                             "Unit '{}' encountered before range {} "
174                             "in expression '{}'".format(
175                                 self.unit, part, self.expression))
176                     self._min_value = min(float(part1), float(part2))
177                     self._max_value = max(float(part1), float(part2))
178                     if not self.operator:
179                         self._operator = '='
180
181             # Is it a unit?
182             elif part in self._all_units():
183                 if not self.value and not self.has_range:
184                     if not self.value:
185                         raise ThresholdException(
186                             "Value {} encountered before unit {} "
187                             "in expression '{}'".format(
188                                 self.value, part, self.expression))
189                     else:
190                         raise ThresholdException(
191                             "Range {}-{} encountered before unit {} "
192                             "in expression '{}'".format(
193                                 self.min_value, self.max_value,
194                                 part, self.expression))
195                 self._unit = part
196
197             # Well then, we don't know.
198             else:
199                 raise ThresholdException(
200                     "Unknown part '{}' in expression '{}'".format(
201                         part, self._expression))
202
203         if not self.has_range and not self._value:
204             raise ThresholdException(
205                 "Value/range missing in expression '{}'".format(
206                     self._expression))
207
208         if self._unit:
209             # Convert from stated units to default.
210             multiplier = self._multiplier_for_unit(self._unit)
211             if self.value:
212                 self._value = self._value * multiplier
213             if self.has_range:
214                 self._min_value = self._min_value * multiplier
215                 self._max_value = self._max_value * multiplier
216
217         # Always use the default unit.
218         self._unit = self._default_for_base_unit(self._base_unit)
219
220         self._parsed = True
221
222     @property
223     def base_unit(self):
224         """Returns the original base unit"""
225         return self._base_unit
226
227     @property
228     def expression(self):
229         """Returns the original expression"""
230         return self._expression
231
232     @property
233     def has_range(self):
234         """Returns True if a minimum/maximum value range exists"""
235         return self.min_value and self.max_value
236
237     @property
238     def max_value(self):
239         """Returns the detected maximum value, if any"""
240         return self._max_value
241
242     @property
243     def min_value(self):
244         """Returns the detected minimum value, if any"""
245         return self._min_value
246
247     @property
248     def operator(self):
249         """Returns the operator"""
250         return self._operator
251
252     @property
253     def parsed(self):
254         """Returns True if the expression was successfully parsed"""
255         return self._parsed
256
257     @property
258     def parts(self):
259         """Returns the expression as a dictionary of parts"""
260         result = {}
261         if self.parsed:
262             result['operator'] = self.operator
263             if self.has_range:
264                 result['value'] = {
265                     'min': self.min_value,
266                     'max': self.max_value,
267                 }
268             else:
269                 result['value'] = self.value
270             result['units'] = self.unit
271         return result
272
273     @property
274     def unit(self):
275         """Returns the units"""
276         return self._unit
277
278     @property
279     def value(self):
280         """Returns the detected value, if any"""
281         return self._value