Skip to content

Commit 3687c8e

Browse files
committed
Implement issue #92
1 parent a5061c0 commit 3687c8e

File tree

4 files changed

+115
-1
lines changed

4 files changed

+115
-1
lines changed

jdiff/extract_data.py

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
jmespath_refkey_parser,
1010
associate_key_of_my_value,
1111
keys_values_zipper,
12+
multi_reference_keys,
1213
)
1314

1415

@@ -43,6 +44,14 @@ 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+
paired_key_value = associate_key_of_my_value(clean_path, values)
52+
list_of_reference_keys = multi_reference_keys(path, data)
53+
return keys_values_zipper(list_of_reference_keys, paired_key_value)
54+
4655
values = jmespath.search(jmespath_value_parser(path), data)
4756

4857
if values is None:

jdiff/utils/jmespath_parsers.py

+39
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
"""
@@ -120,3 +122,40 @@ def keys_values_zipper(list_of_reference_keys: List, wanted_value_with_key: List
120122
final_result.append({my_key: wanted_value_with_key[my_index]})
121123

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

tests/test_get_value.py

+27
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ def test_jmspath_return_none(jmspath):
4444
"vpn.peers.$*$.*.ipv6.[accepted_prefixes]",
4545
[{"10.1.0.0": {"accepted_prefixes": 1000}}, {"10.2.0.0": {"accepted_prefixes": 1000}}],
4646
),
47+
(
48+
"$*$.peers.$*$.*.ipv4.[accepted_prefixes]",
49+
[
50+
{"global.10.1.0.0": {"accepted_prefixes": 1000}},
51+
{"global.10.2.0.0": {"accepted_prefixes": 1000}},
52+
{"global.10.64.207.255": {"accepted_prefixes": 1000}},
53+
{"global.7.7.7.7": {"accepted_prefixes": 1000}},
54+
{"vpn.10.1.0.0": {"accepted_prefixes": 1000}},
55+
{"vpn.10.2.0.0": {"accepted_prefixes": 1000}},
56+
],
57+
),
58+
(
59+
"$*$.peers.$*$.*.ipv6.[accepted_prefixes]",
60+
[
61+
{"global.10.1.0.0": {"accepted_prefixes": 1000}},
62+
{"global.10.2.0.0": {"accepted_prefixes": 1000}},
63+
{"global.10.64.207.255": {"accepted_prefixes": 1000}},
64+
{"global.7.7.7.7": {"accepted_prefixes": 1000}},
65+
{"vpn.10.1.0.0": {"accepted_prefixes": 1000}},
66+
{"vpn.10.2.0.0": {"accepted_prefixes": 1000}},
67+
],
68+
),
69+
pytest.param(
70+
"$*$.peers.$*$.address_family.$*$.[accepted_prefixes]",
71+
"",
72+
marks=pytest.mark.xfail(reason="Jmespath issue - path returns empty list."),
73+
),
4774
]
4875

4976

tests/test_jmespath_parsers.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
jmespath_refkey_parser,
66
keys_values_zipper,
77
associate_key_of_my_value,
8+
multi_reference_keys,
89
)
9-
from .utility import ASSERT_FAIL_MESSAGE
10+
from .utility import load_json_file, ASSERT_FAIL_MESSAGE
1011

1112

1213
value_parser_case_1 = (
@@ -112,3 +113,41 @@ def test_keys_zipper(ref_keys, wanted_values, expected_output):
112113
def test_keys_association(path, wanted_values, expected_output):
113114
output = associate_key_of_my_value(path, wanted_values)
114115
assert expected_output == output, ASSERT_FAIL_MESSAGE.format(output=output, expected_output=expected_output)
116+
117+
118+
multi_ref_key_case_1 = (
119+
"$*$.peers.$*$.*.ipv4.[accepted_prefixes]",
120+
["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"],
121+
)
122+
123+
124+
multi_ref_key_case_2 = (
125+
"$*$.peers.$*$.address_family.$*$.[accepted_prefixes]",
126+
[
127+
"global.10.1.0.0.ipv4",
128+
"global.10.1.0.0.ipv6",
129+
"global.10.2.0.0.ipv4",
130+
"global.10.2.0.0.ipv6",
131+
"global.10.64.207.255.ipv4",
132+
"global.10.64.207.255.ipv6",
133+
"global.7.7.7.7.ipv4",
134+
"global.7.7.7.7.ipv6",
135+
"vpn.10.1.0.0.ipv4",
136+
"vpn.10.1.0.0.ipv6",
137+
"vpn.10.2.0.0.ipv4",
138+
"vpn.10.2.0.0.ipv6",
139+
],
140+
)
141+
142+
143+
multi_ref_key_test_cases = [
144+
multi_ref_key_case_1,
145+
multi_ref_key_case_2,
146+
]
147+
148+
149+
@pytest.mark.parametrize("path, expected_output", multi_ref_key_test_cases)
150+
def test_multi_ref_key(path, expected_output):
151+
data = load_json_file("napalm_get_bgp_neighbors", "multi_vrf.json")
152+
output = multi_reference_keys(path, data)
153+
assert expected_output == output, ASSERT_FAIL_MESSAGE.format(output=output, expected_output=expected_output)

0 commit comments

Comments
 (0)