Skip to content

Commit ce2ecba

Browse files
operator check implementation (#39)
* draft regex base function * fix file import * draft regex implementation * fix logic into regex result * remove assertion * draft test for range check type * add match/no-match logic * commit save * add assertion for check option * add assertions for range type * add assertion for strings * commit save * draft operator check type * complete validate method for check_type operator * work on oeprator_evaluator method * drat operator all-same * add all-same operator * add contains operator * add not-contains. Update contains logic * add is-gt and is-lt operators * add range operators * add operator lib for DRY code * fix some lint errors * Update netcompare/operator.py Not sure about this...if you look at the if statements, your suggestion would brake the logic...or am I missing something? Co-authored-by: Patryk Szulczewski <[email protected]> * work on comments * working on operator args * fix tests * improve validator switch logic * fix pytests Co-authored-by: Patryk Szulczewski <[email protected]>
1 parent fa28727 commit ce2ecba

File tree

6 files changed

+415
-20
lines changed

6 files changed

+415
-20
lines changed

netcompare/check_types.py

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
from typing import Mapping, Tuple, List, Dict, Any, Union
44
from abc import ABC, abstractmethod
55
import jmespath
6-
7-
86
from .utils.jmespath_parsers import (
97
jmespath_value_parser,
108
jmespath_refkey_parser,
@@ -13,11 +11,10 @@
1311
keys_values_zipper,
1412
)
1513
from .utils.data_normalization import exclude_filter, flatten_list
16-
from .evaluators import diff_generator, parameter_evaluator, regex_evaluator
17-
18-
# pylint: disable=arguments-differ
14+
from .evaluators import diff_generator, parameter_evaluator, regex_evaluator, operator_evaluator
1915

2016

17+
# pylint: disable=arguments-differ
2118
class CheckType(ABC):
2219
"""Check Type Base Abstract Class."""
2320

@@ -36,6 +33,8 @@ def init(check_type: str):
3633
return ParameterMatchType()
3734
if check_type == "regex":
3835
return RegexType()
36+
if check_type == "operator":
37+
return OperatorType()
3938

4039
raise NotImplementedError
4140

@@ -115,7 +114,7 @@ class ExactMatchType(CheckType):
115114
"""Exact Match class docstring."""
116115

117116
@staticmethod
118-
def validate(**kwargs):
117+
def validate(**kwargs) -> None:
119118
"""Method to validate arguments."""
120119
# reference_data = getattr(kwargs, "reference_data")
121120

@@ -130,7 +129,7 @@ class ToleranceType(CheckType):
130129
"""Tolerance class docstring."""
131130

132131
@staticmethod
133-
def validate(**kwargs):
132+
def validate(**kwargs) -> None:
134133
"""Method to validate arguments."""
135134
# reference_data = getattr(kwargs, "reference_data")
136135
tolerance = kwargs.get("tolerance")
@@ -177,7 +176,7 @@ class ParameterMatchType(CheckType):
177176
"""Parameter Match class implementation."""
178177

179178
@staticmethod
180-
def validate(**kwargs):
179+
def validate(**kwargs) -> None:
181180
"""Method to validate arguments."""
182181
mode_options = ["match", "no-match"]
183182
params = kwargs.get("params")
@@ -206,7 +205,7 @@ class RegexType(CheckType):
206205
"""Regex Match class implementation."""
207206

208207
@staticmethod
209-
def validate(**kwargs):
208+
def validate(**kwargs) -> None:
210209
"""Method to validate arguments."""
211210
mode_options = ["match", "no-match"]
212211
regex = kwargs.get("regex")
@@ -228,3 +227,80 @@ def evaluate(self, value_to_compare: Mapping, regex: str, mode: str) -> Tuple[Ma
228227
self.validate(regex=regex, mode=mode)
229228
diff = regex_evaluator(value_to_compare, regex, mode)
230229
return diff, not diff
230+
231+
232+
class OperatorType(CheckType):
233+
"""Operator class implementation."""
234+
235+
@staticmethod
236+
def validate(**kwargs) -> None:
237+
"""Validate operator parameters."""
238+
in_operators = ("is-in", "not-in", "in-range", "not-range")
239+
bool_operators = ("all-same",)
240+
number_operators = ("is-gt", "is-lt")
241+
# "equals" is redundant with check type "exact_match" an "parameter_match"
242+
# equal_operators = ("is-equal", "not-equal")
243+
string_operators = ("contains", "not-contains")
244+
valid_options = (
245+
in_operators,
246+
bool_operators,
247+
number_operators,
248+
string_operators,
249+
# equal_operators,
250+
)
251+
252+
# Validate "params" argument is not None.
253+
if not kwargs:
254+
raise KeyError(f"'params' argument must be provided. You have {kwargs}. Read the docs for more info.")
255+
256+
params_key = kwargs["mode"]
257+
params_value = kwargs["operator_data"]
258+
# Validate "params" value is legal.
259+
if all(params_key in operator for operator in valid_options):
260+
raise ValueError(
261+
f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}. You have: {params_key}"
262+
)
263+
264+
if params_key in in_operators:
265+
# "is-in", "not-in", "in-range", "not-range" requires an iterable
266+
if not isinstance(params_value, (list, tuple)):
267+
raise ValueError(
268+
f"Range check-option {in_operators} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}"
269+
)
270+
271+
# "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1
272+
if params_key in ("in-range", "not-range"):
273+
if not isinstance(params_value[0], (int, float)) and not isinstance(params_value[1], float, int):
274+
raise ValueError(
275+
f"Range check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}"
276+
)
277+
if not params_value[0] < params_value[1]:
278+
raise ValueError(
279+
f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}"
280+
)
281+
282+
# "is-gt","is-lt" require either int() or float()
283+
elif params_key in number_operators and not isinstance(params_value, (float, int)):
284+
raise ValueError(
285+
f"Check-option {number_operators} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}"
286+
)
287+
288+
# "contains", "not-contains" require string.
289+
elif params_key in string_operators and not isinstance(params_value, str):
290+
raise ValueError(
291+
f"Range check-option {string_operators} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}"
292+
)
293+
294+
# "all-same" requires boolean True or False
295+
elif params_key in bool_operators and not isinstance(params_value, bool):
296+
raise ValueError(
297+
f"Range check-option {bool_operators} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}"
298+
)
299+
300+
def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]:
301+
"""Operator evaluator implementation."""
302+
self.validate(**params)
303+
# For naming consistency
304+
reference_data = params
305+
evaluation_result = operator_evaluator(reference_data, value_to_compare)
306+
return evaluation_result, not evaluation_result

netcompare/evaluators.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any, Mapping, Dict
44
from deepdiff import DeepDiff
55
from .utils.diff_helpers import get_diff_iterables_items, fix_deepdiff_key_names
6+
from .operator import Operator
67

78

89
def diff_generator(pre_result: Any, post_result: Any) -> Dict:
@@ -99,3 +100,14 @@ def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict:
99100
result.update(item)
100101

101102
return result
103+
104+
105+
def operator_evaluator(referance_data: Mapping, value_to_compare: Mapping) -> Dict:
106+
"""Operator evaluator call."""
107+
# referance_data
108+
# {'mode': 'all-same', 'operator_data': True}
109+
operator_mode = referance_data["mode"].replace("-", "_")
110+
operator = Operator(referance_data["operator_data"], value_to_compare)
111+
112+
result = getattr(operator, operator_mode)()
113+
return result

netcompare/operator.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Operator diff."""
2+
import operator
3+
4+
5+
class Operator:
6+
"""Operator class implementation."""
7+
8+
def __init__(self, referance_data, value_to_compare) -> None:
9+
"""__init__ method."""
10+
# [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}},
11+
# {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}},
12+
# {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}},
13+
# {'10.64.207.255': {'peerGroup': 'IPv4-UNDERLAY-MLAG-PEER', 'vrf': 'default', 'state': 'Idle'}}]
14+
self.referance_data = referance_data
15+
self.value_to_compare = value_to_compare
16+
17+
def _loop_through_wrapper(self, call_ops):
18+
"""Wrappoer method for operator evaluation."""
19+
ops = {
20+
">": operator.gt,
21+
"<": operator.lt,
22+
"is_in": operator.contains,
23+
"not_in": operator.contains,
24+
"contains": operator.contains,
25+
"not_contains": operator.contains,
26+
}
27+
28+
result = []
29+
for item in self.value_to_compare:
30+
for value in item.values():
31+
for evaluated_value in value.values():
32+
# reverse operands (??? WHY ???) https://docs.python.org/3.8/library/operator.html#operator.contains
33+
if call_ops == "is_in":
34+
if ops[call_ops](self.referance_data, evaluated_value):
35+
result.append(item)
36+
elif call_ops == "not_contains":
37+
if not ops[call_ops](evaluated_value, self.referance_data):
38+
result.append(item)
39+
elif call_ops == "not_in":
40+
if not ops[call_ops](self.referance_data, evaluated_value):
41+
result.append(item)
42+
elif call_ops == "in_range":
43+
if self.referance_data[0] < evaluated_value < self.referance_data[1]:
44+
result.append(item)
45+
elif call_ops == "not_range":
46+
if not self.referance_data[0] < evaluated_value < self.referance_data[1]:
47+
result.append(item)
48+
# "<", ">", "contains"
49+
elif ops[call_ops](evaluated_value, self.referance_data):
50+
result.append(item)
51+
if result:
52+
return (True, result)
53+
return (False, result)
54+
55+
def all_same(self):
56+
"""All same operator implementation."""
57+
list_of_values = []
58+
result = []
59+
60+
for item in self.value_to_compare:
61+
for value in item.values():
62+
# Create a list for compare valiues.
63+
list_of_values.append(value)
64+
65+
for element in list_of_values:
66+
if element != list_of_values[0]:
67+
result.append(False)
68+
else:
69+
result.append(True)
70+
71+
if self.referance_data and not all(result):
72+
return (False, self.value_to_compare)
73+
if self.referance_data:
74+
return (True, self.value_to_compare)
75+
if not all(result):
76+
return (True, self.value_to_compare)
77+
return (False, self.value_to_compare)
78+
79+
def contains(self):
80+
"""Contains operator implementation."""
81+
return self._loop_through_wrapper("contains")
82+
83+
def not_contains(self):
84+
"""Not contains operator implementation."""
85+
return self._loop_through_wrapper("not_contains")
86+
87+
def is_gt(self):
88+
"""Is greather than operator implementation."""
89+
return self._loop_through_wrapper(">")
90+
91+
def is_lt(self):
92+
"""Is lower than operator implementation."""
93+
return self._loop_through_wrapper("<")
94+
95+
def is_in(self):
96+
"""Is in operator implementation."""
97+
return self._loop_through_wrapper("is_in")
98+
99+
def not_in(self):
100+
"""Is not in operator implementation."""
101+
return self._loop_through_wrapper("not_in")
102+
103+
def in_range(self):
104+
"""Is in range operator implementation."""
105+
return self._loop_through_wrapper("in_range")
106+
107+
def not_range(self):
108+
"""Is not in range operator implementation."""
109+
return self._loop_through_wrapper("not_range")

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ no-docstring-rgx="^(_|test_|Meta$)"
5757
disable = """,
5858
line-too-long,
5959
bad-continuation,
60-
E5110
60+
E5110,
61+
R0912,
62+
R0801
6163
"""
6264

6365
[tool.pylint.miscellaneous]

0 commit comments

Comments
 (0)