Skip to content

Commit d5929c7

Browse files
chadelllvrfrc87
andauthored
Yet another refactor (#33)
* draft * proposal * fix tests * fix tolerance tests * fix build Co-authored-by: Network to Code <federico.olivieri@networktocode.com>
1 parent a69a78b commit d5929c7

File tree

5 files changed

+318
-287
lines changed

5 files changed

+318
-287
lines changed

netcompare/check_types.py

Lines changed: 86 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Mapping, Tuple, List, Dict, Any, Union
44
import jmespath
55

6+
67
from .utils.jmespath_parsers import (
78
jmespath_value_parser,
89
jmespath_refkey_parser,
@@ -13,29 +14,27 @@
1314
from .utils.data_normalization import exclude_filter, flatten_list
1415
from .evaluators import diff_generator, parameter_evaluator, regex_evaluator
1516

17+
# pylint: disable=arguments-differ
18+
1619

1720
class CheckType:
1821
"""Check Type Class."""
1922

20-
def __init__(self, *args):
21-
"""Check Type init method."""
22-
2323
@staticmethod
24-
def init(*args):
24+
def init(check_type):
2525
"""Factory pattern to get the appropriate CheckType implementation.
2626
2727
Args:
28-
*args: Variable length argument list.
28+
check_type: String to define the type of check.
2929
"""
30-
check_type = args[0]
3130
if check_type == "exact_match":
32-
return ExactMatchType(*args)
31+
return ExactMatchType()
3332
if check_type == "tolerance":
34-
return ToleranceType(*args)
33+
return ToleranceType()
3534
if check_type == "parameter_match":
36-
return ParameterMatchType(*args)
35+
return ParameterMatchType()
3736
if check_type == "regex":
38-
return RegexType(*args)
37+
return RegexType()
3938

4039
raise NotImplementedError
4140

@@ -89,102 +88,133 @@ def get_value(output: Union[Mapping, List], path: str, exclude: List = None) ->
8988

9089
return values
9190

92-
def evaluate(self, reference_value: Any, value_to_compare: Any) -> Tuple[Dict, bool]:
91+
def evaluate(self, value_to_compare: Any, **kwargs) -> Tuple[Dict, bool]:
9392
"""Return the result of the evaluation and a boolean True if it passes it or False otherwise.
9493
9594
This method is the one that each CheckType has to implement.
9695
9796
Args:
98-
reference_value: Can be any structured data or just a simple value.
9997
value_to_compare: Similar value as above to perform comparison.
10098
10199
Returns:
102100
tuple: Dictionary representing check result, bool indicating if differences are found.
103101
"""
102+
# This method should call before any other logic the validation of the arguments
103+
# self.validate(**kwargs)
104+
raise NotImplementedError
105+
106+
@staticmethod
107+
def validate(**kwargs):
108+
"""Method to validate arguments that raises proper exceptions."""
104109
raise NotImplementedError
105110

106111

107112
class ExactMatchType(CheckType):
108113
"""Exact Match class docstring."""
109114

110-
def evaluate(self, reference_value: Any, value_to_compare: Any) -> Tuple[Dict, bool]:
115+
@staticmethod
116+
def validate(**kwargs):
117+
"""Method to validate arguments."""
118+
# reference_data = getattr(kwargs, "reference_data")
119+
120+
def evaluate(self, value_to_compare: Any, reference_data: Any) -> Tuple[Dict, bool]:
111121
"""Returns the difference between values and the boolean."""
112-
evaluation_result = diff_generator(reference_value, value_to_compare)
122+
self.validate(reference_data=reference_data)
123+
evaluation_result = diff_generator(reference_data, value_to_compare)
113124
return evaluation_result, not evaluation_result
114125

115126

116127
class ToleranceType(CheckType):
117128
"""Tolerance class docstring."""
118129

119-
def __init__(self, *args):
120-
"""Tolerance init method."""
121-
super().__init__()
122-
123-
try:
124-
tolerance = float(args[1])
125-
except IndexError as error:
126-
raise IndexError(f"Tolerance parameter must be defined as float at index 1. You have: {args}") from error
127-
except ValueError as error:
128-
raise ValueError(f"Argument must be convertible to float. You have: {args[1]}") from error
129-
130-
self.tolerance_factor = tolerance / 100
131-
132-
def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Dict, bool]:
130+
@staticmethod
131+
def validate(**kwargs):
132+
"""Method to validate arguments."""
133+
# reference_data = getattr(kwargs, "reference_data")
134+
tolerance = kwargs.get("tolerance")
135+
if not tolerance:
136+
raise ValueError("Tolerance argument is mandatory for Tolerance Check Type.")
137+
if not isinstance(tolerance, int):
138+
raise ValueError(f"Tolerance argument must be an integer, and it's {type(tolerance)}.")
139+
140+
def evaluate(self, value_to_compare: Any, reference_data: Any, tolerance: int) -> Tuple[Dict, bool]:
133141
"""Returns the difference between values and the boolean. Overwrites method in base class."""
134-
diff = diff_generator(reference_value, value_to_compare)
135-
self._remove_within_tolerance(diff)
142+
self.validate(reference_data=reference_data, tolerance=tolerance)
143+
diff = diff_generator(reference_data, value_to_compare)
144+
self._remove_within_tolerance(diff, tolerance)
136145
return diff, not diff
137146

138-
def _remove_within_tolerance(self, diff: Dict) -> None:
147+
def _remove_within_tolerance(self, diff: Dict, tolerance: int) -> None:
139148
"""Recursively look into diff and apply tolerance check, remove reported difference when within tolerance."""
140149

141150
def _within_tolerance(*, old_value: float, new_value: float) -> bool:
142151
"""Return True if new value is within the tolerance range of the previous value."""
143-
max_diff = old_value * self.tolerance_factor
152+
tolerance_factor = tolerance / 100
153+
max_diff = old_value * tolerance_factor
144154
return (old_value - max_diff) < new_value < (old_value + max_diff)
145155

146156
for key, value in list(diff.items()): # casting list makes copy, so we don't modify object being iterated.
147157
if isinstance(value, dict):
148158
if "new_value" in value.keys() and "old_value" in value.keys() and _within_tolerance(**value):
149159
diff.pop(key)
150160
else:
151-
self._remove_within_tolerance(diff[key])
161+
self._remove_within_tolerance(diff[key], tolerance)
152162
if not value:
153163
diff.pop(key)
154164

155165

156166
class ParameterMatchType(CheckType):
157167
"""Parameter Match class implementation."""
158168

159-
def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Dict, bool]:
169+
@staticmethod
170+
def validate(**kwargs):
171+
"""Method to validate arguments."""
172+
mode_options = ["match", "no-match"]
173+
params = kwargs.get("params")
174+
if not params:
175+
raise ValueError("Params argument is mandatory for ParameterMatch Check Type.")
176+
if not isinstance(params, dict):
177+
raise ValueError(f"Params argument must be a dict, and it's {type(params)}.")
178+
179+
mode = kwargs.get("mode")
180+
if not mode:
181+
raise ValueError("Mode argument is mandatory for ParameterMatch Check Type.")
182+
if not isinstance(mode, str):
183+
raise ValueError(f"Mode argument must be a string, and it's {type(mode)}.")
184+
if mode not in mode_options:
185+
raise ValueError(f"Mode argument should be {mode_options}, and it's {mode}")
186+
187+
def evaluate(self, value_to_compare: Mapping, params: Dict, mode: str) -> Tuple[Dict, bool]:
160188
"""Parameter Match evaluator implementation."""
161-
if not isinstance(value_to_compare, dict):
162-
raise TypeError("check_option must be of type dict()")
163-
164-
evaluation_result = parameter_evaluator(reference_value, value_to_compare)
189+
self.validate(params=params, mode=mode)
190+
# TODO: we don't use the mode?
191+
evaluation_result = parameter_evaluator(value_to_compare, params)
165192
return evaluation_result, not evaluation_result
166193

167194

168195
class RegexType(CheckType):
169196
"""Regex Match class implementation."""
170197

171-
def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]:
198+
@staticmethod
199+
def validate(**kwargs):
200+
"""Method to validate arguments."""
201+
mode_options = ["match", "no-match"]
202+
regex = kwargs.get("regex")
203+
if not regex:
204+
raise ValueError("Params argument is mandatory for Regex Match Check Type.")
205+
if not isinstance(regex, str):
206+
raise ValueError(f"Params argument must be a string, and it's {type(regex)}.")
207+
208+
mode = kwargs.get("mode")
209+
if not mode:
210+
raise ValueError("Mode argument is mandatory for Regex Match Check Type.")
211+
if not isinstance(mode, str):
212+
raise ValueError(f"Mode argument must be a string, and it's {type(mode)}.")
213+
if mode not in mode_options:
214+
raise ValueError(f"Mode argument should be {mode_options}, and it's {mode}")
215+
216+
def evaluate(self, value_to_compare: Mapping, regex: str, mode: str) -> Tuple[Mapping, bool]:
172217
"""Regex Match evaluator implementation."""
173-
# Check that check value_to_compare is dict.
174-
if not isinstance(value_to_compare, dict):
175-
raise TypeError("check_option must be of type dict().")
176-
177-
# Check that value_to_compare has 'regex' and 'mode' dict keys.
178-
if any(key not in value_to_compare.keys() for key in ("regex", "mode")):
179-
raise KeyError(
180-
"Regex check-type requires check-option. Example: dict(regex='.*UNDERLAY.*', mode='no-match')."
181-
)
182-
183-
# Assert that check option has 'regex' and 'mode' dict keys.\
184-
if value_to_compare["mode"] not in ["match", "no-match"]:
185-
raise ValueError(
186-
"Regex check-type requires check-option. Example: dict(regex='.*UNDERLAY.*', mode='no-match')."
187-
)
188-
189-
diff = regex_evaluator(reference_value, value_to_compare)
218+
self.validate(regex=regex, mode=mode)
219+
diff = regex_evaluator(value_to_compare, regex, mode)
190220
return diff, not diff

netcompare/evaluators.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,14 @@ def parameter_evaluator(values: Mapping, parameters: Mapping) -> Dict:
7979
return result
8080

8181

82-
def regex_evaluator(values: Mapping, parameter: Mapping) -> Dict:
82+
def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict:
8383
"""Regex Match evaluator engine."""
8484
# values: [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}]
8585
# parameter: {'regex': '.*UNDERLAY.*', 'mode': 'include'}
8686
result = {}
8787
if not isinstance(values, list):
8888
raise TypeError("Something went wrong during JMSPath parsing. 'values' must be of type List.")
8989

90-
regex_expression = parameter["regex"]
91-
mode = parameter["mode"]
92-
9390
for item in values:
9491
for founded_value in item.values():
9592
for value in founded_value.values():

0 commit comments

Comments
 (0)