Skip to content

Commit 7a5ae75

Browse files
lvrfrc87chadellitdependsnetworkspszulczewskiscetron
authored
Ver. 0.0.3 release. (#99)
* Update readme to start with use cases (#84) * Update readme to start with use cases * Apply suggestions from code review Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: Stephen Corry <[email protected]> * Update README.md Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: Stephen Corry <[email protected]> * Doc update (#87) * Fix operator checks to follow other check_type logic. (#85) * Fix operator checks to follow other check_type logic. * Add new release 0.0.2 Co-authored-by: Patryk Szulczewski <[email protected]> * Implement `ge` and `le` operator type (#89) * Release 0.0.2 (#88) * Update readme to start with use cases (#84) * Update readme to start with use cases * Apply suggestions from code review Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: Stephen Corry <[email protected]> * Update README.md Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: Stephen Corry <[email protected]> * Doc update (#87) * Fix operator checks to follow other check_type logic. (#85) * Fix operator checks to follow other check_type logic. * Add new release 0.0.2 Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: Christian Adell <[email protected]> Co-authored-by: Ken Celenza <[email protected]> Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: Stephen Corry <[email protected]> Co-authored-by: Patryk Szulczewski <[email protected]> * add operator ge and le * fix validate tests Co-authored-by: Christian Adell <[email protected]> Co-authored-by: Ken Celenza <[email protected]> Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: Stephen Corry <[email protected]> Co-authored-by: Patryk Szulczewski <[email protected]> * Fix parameter_match check, to support non-normalized data (#90) * Fix parameter_match check, to support non-normalized data * Add test * Update tests. * remove test print Co-authored-by: Patryk Szulczewski <[email protected]> * Update mypy * Bugfix to data normalization * Fix ref_key parsing * Update docs * Issue 92 (#98) * Base-line tests for future refactor. * Refactor to simplify code. * Implement issue #92 * Bump certifi from 2022.9.24 to 2022.12.7 (#95) Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.9.24 to 2022.12.7. - [Release notes](https://github.com/certifi/python-certifi/releases) - [Commits](certifi/python-certifi@2022.09.24...2022.12.07) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: Christian Adell <[email protected]> Co-authored-by: Ken Celenza <[email protected]> Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: Stephen Corry <[email protected]> Co-authored-by: Patryk Szulczewski <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent a539dbe commit 7a5ae75

18 files changed

+751
-473
lines changed

CHANGELOG.md

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
# Changelog
2+
## v0.0.3
3+
- Update documentation
4+
- Fix evaluation logic in operator check
5+
- Fix operator checks to follow other check_type logic #85
6+
- Add support for multiple ref key #92
7+
- Update gt and lt logic in operator check #64
8+
- Fix ref key in dict of dicts data type #91
9+
- Support single value in expression #94
10+
- Minor fixes
211

312
## v0.0.2
4-
513
- Update operator logic for returned result
614
- Update docs
715

816
## v0.0.1
9-
1017
- Initial release
1118

1219
## v0.0.1-beta.1
13-
1420
- First beta release

docs/usage.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ Let's have a look at a couple of examples:
251251
```
252252
We will define a JMESPath expression for the values we want to test and extract from the reference and comparison objects.
253253
```python
254-
>>> my_jmspath = "global.$peers$.*.*.ipv4.[accepted_prefixes,received_prefixes,sent_prefixes]"
254+
>>> my_jmspath = "global.peers.$*$.*.ipv4.[accepted_prefixes,received_prefixes,sent_prefixes]"
255255
>>> reference_value = extract_data_from_json(reference_data, my_jmspath)
256256
>>> reference_value
257257
[{'10.1.0.0': {'accepted_prefixes': 900,
@@ -478,7 +478,12 @@ The `operator` check is a collection of more specific checks divided into catego
478478
2. `is-lt`: Check if the value of a specified element is lesser than a given numeric value.
479479
- `is-lt: 55`: checks if value is lower than 55 or not.
480480

481+
3. `is-ge`: Check if the value of a specified element is greater than or equal to a given numeric value.
482+
- `is-ge: 2`: checks if value should be greater or equal than 2.
481483

484+
4. `is-le`: Check if the value of a specified element is lesser than or equal a given numeric value.
485+
- `is-le: 55`: checks if value is lower or equal than 55 or not.
486+
482487
Examples:
483488

484489
```python

jdiff/check_types.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""CheckType Implementation."""
2-
from typing import Mapping, Tuple, Dict, Any, Union
2+
from typing import List, Tuple, Dict, Any, Union
33
from abc import ABC, abstractmethod
44
from .evaluators import diff_generator, parameter_evaluator, regex_evaluator, operator_evaluator
55

@@ -136,7 +136,7 @@ def _validate(params, mode) -> None: # type: ignore[override]
136136
f"'mode' argument should be one of the following: {', '.join(mode_options)}. You have: {mode}"
137137
)
138138

139-
def evaluate(self, params: Dict, value_to_compare: Mapping, mode: str) -> Tuple[Dict, bool]: # type: ignore[override]
139+
def evaluate(self, params: Dict, value_to_compare: List[Dict], mode: str) -> Tuple[Dict, bool]: # type: ignore[override]
140140
"""Parameter Match evaluator implementation."""
141141
self._validate(params=params, mode=mode)
142142
# TODO: we don't use the mode?
@@ -161,7 +161,7 @@ def _validate(regex, mode) -> None: # type: ignore[override]
161161
if mode not in mode_options:
162162
raise ValueError(f"'mode' argument should be {mode_options}. You have: {mode}")
163163

164-
def evaluate(self, regex: str, value_to_compare: Mapping, mode: str) -> Tuple[Dict, bool]: # type: ignore[override]
164+
def evaluate(self, regex: str, value_to_compare: List[Dict[Any, Dict]], mode: str) -> Tuple[Dict, bool]: # type: ignore[override]
165165
"""Regex Match evaluator implementation."""
166166
self._validate(regex=regex, mode=mode)
167167
evaluation_result = regex_evaluator(value_to_compare, regex, mode)
@@ -176,7 +176,7 @@ def _validate(params) -> None: # type: ignore[override]
176176
"""Validate operator parameters."""
177177
in_operators = ("is-in", "not-in", "in-range", "not-in-range")
178178
bool_operators = ("all-same",)
179-
number_operators = ("is-gt", "is-lt")
179+
number_operators = ("is-gt", "is-lt", "is-ge", "is-le")
180180
string_operators = ("contains", "not-contains")
181181
valid_options = (
182182
in_operators,

jdiff/evaluators.py

+21-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Evaluators."""
22
import re
3-
from typing import Any, Mapping, Dict, Tuple
3+
from typing import Any, Mapping, Dict, Tuple, List
44
from deepdiff import DeepDiff
55
from .utils.diff_helpers import get_diff_iterables_items, fix_deepdiff_key_names
66
from .operator import Operator
@@ -36,7 +36,7 @@ def diff_generator(pre_result: Any, post_result: Any) -> Dict:
3636
return fix_deepdiff_key_names(result)
3737

3838

39-
def parameter_evaluator(values: Mapping, parameters: Mapping, mode: str) -> Dict:
39+
def parameter_evaluator(values: List[Dict], parameters: Mapping, mode: str) -> Dict:
4040
"""Parameter Match evaluator engine.
4141
4242
Args:
@@ -51,41 +51,41 @@ def parameter_evaluator(values: Mapping, parameters: Mapping, mode: str) -> Dict
5151
Dictionary with all the items that have some value not matching the expectations from parameters
5252
"""
5353
if not isinstance(values, list):
54-
raise TypeError("Something went wrong during jmespath parsing. 'values' must be of type List.")
54+
raise TypeError("'values' must be of type List.")
5555

5656
result = {}
57-
for value in values:
57+
for index, value in enumerate(values):
5858
# value: {'7.7.7.7': {'peerAddress': '7.7.7.7', 'localAsn': '65130.1101', 'linkType': 'externals
5959
if not isinstance(value, dict):
60-
raise TypeError(
61-
"Something went wrong during jmespath parsing. ",
62-
f"'value' ({value}) must be of type Dict, and it's {type(value)}",
63-
)
60+
raise TypeError(f"'value' ({value}) must be of type Dict, and it's {type(value)}")
6461

6562
result_item = {}
6663

67-
# TODO: Why the 'value' dict has always ONE single element? we have to explain
68-
# inner_key: '7.7.7.7'
69-
inner_key = list(value.keys())[0]
70-
# inner_value: [{'peerAddress': '7.7.7.7', 'localAsn': '65130.1101', 'linkType': 'externals'}]
71-
inner_value = list(value.values())[0]
64+
# When data has been normalized with $key$, get inner key and value
65+
if len(value) == 1:
66+
# inner_key: '7.7.7.7'
67+
inner_key = list(value.keys())[0]
68+
# inner_value: [{'peerAddress': '7.7.7.7', 'localAsn': '65130.1101', 'linkType': 'externals'}]
69+
value = list(value.values())[0]
70+
else:
71+
inner_key = index
7272

7373
for parameter_key, parameter_value in parameters.items():
74-
if mode == "match" and inner_value[parameter_key] != parameter_value:
75-
result_item[parameter_key] = inner_value[parameter_key]
76-
elif mode == "no-match" and inner_value[parameter_key] == parameter_value:
77-
result_item[parameter_key] = inner_value[parameter_key]
74+
if mode == "match" and value[parameter_key] != parameter_value:
75+
result_item[parameter_key] = value[parameter_key]
76+
elif mode == "no-match" and value[parameter_key] == parameter_value:
77+
result_item[parameter_key] = value[parameter_key]
7878

7979
if result_item:
8080
result[inner_key] = result_item
8181

8282
return result
8383

8484

85-
def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict:
85+
def regex_evaluator(values: List[Dict[Any, Dict]], regex_expression: str, mode: str) -> Dict:
8686
"""Regex Match evaluator engine."""
8787
# values: [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}]
88-
# parameter: {'regex': '.*UNDERLAY.*', 'mode': 'include'}
88+
# parameter: {'regex': '.*UNDERLAY.*', 'mode': 'match'}
8989
result = {}
9090
if not isinstance(values, list):
9191
raise TypeError("Something went wrong during JMSPath parsing. 'values' must be of type List.")
@@ -94,10 +94,10 @@ def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict:
9494
for founded_value in item.values():
9595
for value in founded_value.values():
9696
match_result = re.search(regex_expression, value)
97-
# Fail if there is not regex match
97+
# Fail if there is no regex match for "match" mode
9898
if mode == "match" and not match_result:
9999
result.update(item)
100-
# Fail if there is regex match
100+
# Fail if there is regex match for "no-match" mode.
101101
elif mode == "no-match" and match_result:
102102
result.update(item)
103103

jdiff/extract_data.py

+23-19
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
"""Extract data from JSON. Based on custom JMSPath implementation."""
22
import re
33
import warnings
4-
from typing import Mapping, List, Dict, Any, Union
4+
from typing import Mapping, List, Dict, Any, Union, Optional
55
import jmespath
66
from .utils.data_normalization import exclude_filter, flatten_list
77
from .utils.jmespath_parsers import (
88
jmespath_value_parser,
99
jmespath_refkey_parser,
1010
associate_key_of_my_value,
1111
keys_values_zipper,
12+
multi_reference_keys,
1213
)
1314

1415

15-
def extract_data_from_json(data: Union[Mapping, List], path: str = "*", exclude: List = None) -> Any:
16+
def extract_data_from_json(data: Union[Mapping, List], path: str = "*", exclude: Optional[List] = None) -> Any:
1617
"""Return wanted data from outpdevice data based on the check path. See unit test for complete example.
1718
1819
Get the wanted values to be evaluated if JMESPath expression is defined,
@@ -43,33 +44,36 @@ def extract_data_from_json(data: Union[Mapping, List], path: str = "*", exclude:
4344
# return if path is not specified
4445
return data
4546

47+
# Multi ref_key
48+
if len(re.findall(r"\$.*?\$", path)) > 1:
49+
clean_path = path.replace("$", "")
50+
values = jmespath.search(f"{clean_path}{' | []' * (path.count('*') - 1)}", data)
51+
return keys_values_zipper(multi_reference_keys(path, data), associate_key_of_my_value(clean_path, values))
52+
4653
values = jmespath.search(jmespath_value_parser(path), data)
4754

4855
if values is None:
4956
raise TypeError("JMSPath returned 'None'. Please, verify your JMSPath regex.")
5057

51-
# check for multi-nested lists if not found return here
52-
if not any(isinstance(i, list) for i in values):
53-
return values
54-
55-
# process elements to check if lists should be flattened
56-
for element in values:
57-
for item in element:
58-
# raise if there is a dict, path must be more specific to extract data
59-
if isinstance(item, dict):
60-
raise TypeError(
61-
f'Must be list of lists i.e. [["Idle", 75759616], ["Idle", 75759620]]. You have "{values}".'
62-
)
63-
if isinstance(item, list):
64-
values = flatten_list(values) # flatten list and rewrite values
65-
break # items are the same, need to check only first to see if this is a nested list
66-
67-
paired_key_value = associate_key_of_my_value(jmespath_value_parser(path), values)
58+
# check for multi-nested lists
59+
if any(isinstance(i, list) for i in values):
60+
# process elements to check if lists should be flattened
61+
for element in values:
62+
for item in element:
63+
# raise if there is a dict, path must be more specific to extract data
64+
if isinstance(item, dict):
65+
raise TypeError(
66+
f'Must be list of lists i.e. [["Idle", 75759616], ["Idle", 75759620]]. You have "{values}".'
67+
)
68+
if isinstance(item, list):
69+
values = flatten_list(values) # flatten list and rewrite values
70+
break # items are the same, need to check only first to see if this is a nested list
6871

6972
# We need to get a list of reference keys - list of strings.
7073
# Based on the expression or data we might have different data types
7174
# therefore we need to normalize.
7275
if re.search(r"\$.*\$", path):
76+
paired_key_value = associate_key_of_my_value(jmespath_value_parser(path), values)
7377
wanted_reference_keys = jmespath.search(jmespath_refkey_parser(path), data)
7478

7579
if isinstance(wanted_reference_keys, dict): # when wanted_reference_keys is dict() type

jdiff/operator.py

+10
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ def call_evaluation_logic():
5151

5252
ops = {
5353
">": operator.gt,
54+
">=": operator.ge,
5455
"<": operator.lt,
56+
"<=": operator.le,
5557
"is_in": operator.contains,
5658
"not_in": operator.contains,
5759
"contains": operator.contains,
@@ -99,10 +101,18 @@ def is_gt(self) -> Tuple[List, bool]:
99101
"""Is greather than operator caller."""
100102
return self._loop_through_wrapper(">")
101103

104+
def is_ge(self) -> Tuple[List, bool]:
105+
"""Is greather or equal than operator caller."""
106+
return self._loop_through_wrapper(">=")
107+
102108
def is_lt(self) -> Tuple[List, bool]:
103109
"""Is lower than operator caller."""
104110
return self._loop_through_wrapper("<")
105111

112+
def is_le(self) -> Tuple[List, bool]:
113+
"""Is lower or equal than operator caller."""
114+
return self._loop_through_wrapper("<=")
115+
106116
def is_in(self) -> Tuple[List, bool]:
107117
"""Is in operator caller."""
108118
return self._loop_through_wrapper("is_in")

jdiff/utils/jmespath_parsers.py

+55-10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import re
99
from typing import Mapping, List, Union
1010

11+
import jmespath
12+
1113

1214
def jmespath_value_parser(path: str):
1315
"""
@@ -31,15 +33,15 @@ def jmespath_value_parser(path: str):
3133
path_suffix = path.split(".")[-1]
3234

3335
if regex_match_ref_key:
36+
reference_key = regex_match_ref_key.group()
3437
if regex_ref_key.search(path_suffix):
3538
# [$peerAddress$,prefixesReceived] --> [prefixesReceived]
36-
reference_key = regex_match_ref_key.group()
3739
return path.replace(reference_key, "")
3840

3941
# result[0].$vrfs$.default... --> result[0].vrfs.default....
40-
regex_normalized_value = re.search(r"\$.*\$", regex_match_ref_key.group())
42+
regex_normalized_value = re.search(r"\$.*\$", reference_key)
4143
if regex_normalized_value:
42-
normalized_value = regex_match_ref_key.group().split("$")[1]
44+
normalized_value = reference_key.split("$")[1]
4345
return path.replace(regex_normalized_value.group(), normalized_value)
4446
return path
4547

@@ -65,9 +67,9 @@ def jmespath_refkey_parser(path: str):
6567
splitted_jmespath[number] = regex_match_anchor.group().replace("$", "")
6668

6769
if regex_match_anchor and not element.startswith("[") and not element.endswith("]"):
68-
splitted_jmespath = splitted_jmespath[: number + 1]
70+
splitted_jmespath = splitted_jmespath[:number]
6971

70-
return ".".join(splitted_jmespath)
72+
return ".".join(splitted_jmespath) or "@"
7173

7274

7375
def associate_key_of_my_value(paths: str, wanted_value: List) -> List:
@@ -85,13 +87,19 @@ def associate_key_of_my_value(paths: str, wanted_value: List) -> List:
8587

8688
final_list = []
8789

88-
for items in wanted_value:
89-
if len(items) != len(my_key_value_list):
90-
raise ValueError("Key's value len != from value len")
90+
if not all(isinstance(item, list) for item in wanted_value) and len(my_key_value_list) == 1:
91+
for item in wanted_value:
92+
temp_dict = {my_key_value_list[0]: item}
93+
final_list.append(temp_dict)
94+
95+
else:
96+
for items in wanted_value:
97+
if len(items) != len(my_key_value_list):
98+
raise ValueError("Key's value len != from value len")
9199

92-
temp_dict = {my_key_value_list[my_index]: my_value for my_index, my_value in enumerate(items)}
100+
temp_dict = {my_key_value_list[my_index]: my_value for my_index, my_value in enumerate(items)}
93101

94-
final_list.append(temp_dict)
102+
final_list.append(temp_dict)
95103

96104
return final_list
97105

@@ -120,3 +128,40 @@ def keys_values_zipper(list_of_reference_keys: List, wanted_value_with_key: List
120128
final_result.append({my_key: wanted_value_with_key[my_index]})
121129

122130
return final_result
131+
132+
133+
def multi_reference_keys(jmspath, data):
134+
"""Build a list of concatenated reference keys.
135+
136+
Args:
137+
jmspath: "$*$.peers.$*$.*.ipv4.[accepted_prefixes]"
138+
data: tests/mock/napalm_get_bgp_neighbors/multi_vrf.json
139+
140+
Returns:
141+
["global.10.1.0.0", "global.10.2.0.0", "global.10.64.207.255", "global.7.7.7.7", "vpn.10.1.0.0", "vpn.10.2.0.0"]
142+
"""
143+
ref_key_regex = re.compile(r"\$.*?\$")
144+
mapping = []
145+
split_path = jmspath.split(".")
146+
147+
ref_key_index = -1 # -1 as the starting value, so it will match split path list indexes
148+
for index, element in enumerate(split_path):
149+
if ref_key_regex.search(element):
150+
ref_key_index += 1
151+
key_path = (
152+
".".join(split_path[:index]).replace("$", "") or "@"
153+
) # @ is for top keys, as they are stripped with "*"
154+
flat_path = f"{key_path}{' | []' * key_path.count('*')}" # | [] to flatten the data, nesting level is eq to "*" count
155+
sub_data = jmespath.search(flat_path, data) # extract sub-data with up to the ref key
156+
if isinstance(sub_data, dict):
157+
keys = list(sub_data.keys())
158+
elif isinstance(sub_data, list):
159+
keys = []
160+
for parent, children in zip(
161+
mapping[ref_key_index - 1], sub_data
162+
): # refer to previous keys as they are already present in mapping
163+
keys.extend(f"{parent}.{child}" for child in children.keys()) # concatenate keys
164+
else:
165+
raise ValueError("Ref key anchor must return either a dict or a list.")
166+
mapping.append(keys)
167+
return mapping[-1] # return last element as it has all previous ref_keys concatenated.

0 commit comments

Comments
 (0)