Skip to content

Commit f70f683

Browse files
authored
Merge pull request #68 from networktocode-llc/docs-review
OSRB documentation review
2 parents 96b3c91 + 97817f3 commit f70f683

23 files changed

+1244
-951
lines changed

README.md

Lines changed: 9 additions & 600 deletions
Large diffs are not rendered by default.

docs/architecture.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Library Architecture
2+
3+
| ![jdiff HLD](./images/hld-architecture.png) |
4+
|:---:|
5+
| **`jdiff` architecture** |
6+
7+
We use the `CheckType` factory class method `create` along with the specified check type to instantiate a concrete object of the specified `CheckType` class.
8+
9+
```python
10+
from jdiff import CheckType, extract_data_from_json
11+
check = CheckType.create("exact_match")
12+
>>> <jdiff.check_types.ExactMatchType at 0x10a618e80>
13+
```
14+
15+
- `exact_match`
16+
- `tolerance`
17+
- `parameter_match`
18+
- `regex`
19+
- `operator`
20+
21+
22+
Next, load a JSON object as reference data. as well as a JMESPath expression to extract the values wanted and pass them to `extract_data_from_json` method. Be aware! `jdiff` works with a customized version of JMESPath. More on that [below](#customized-jmespath).
23+
24+
> We'll use data from the tests folder. We assume the current working directory is the root of the repo if you've cloned it.
25+
26+
```python
27+
>>> bgp_reference_state = json.load(open("tests/mock/napalm_get_bgp_neighbors/pre.json"))
28+
>>> bgp_jmspath_exp = "global.$peers$.*.*.ipv4.[accepted_prefixes,received_prefixes,sent_prefixes]"
29+
>>> bgp_reference_value = extract_data_from_json(bgp_reference_state, bgp_jmspath_exp)
30+
```
31+
32+
Once the pre-change values are extracted, we would need to evaluate them against our post-change value. In the case of check-type `exact_match`, our post value would be another JSON object:
33+
34+
```python
35+
>>> bgp_comparison_state = json.load(open("tests/mock/napalm_get_bgp_neighbors/post.json"))
36+
>>> bgp_comparison_value = extract_data_from_json(bgp_comparison_state, bgp_jmspath_exp)
37+
```
38+
39+
Each check type expects different types of arguments based on how and what they are checking. For example: check type `tolerance` needs a `tolerance` argument, whereas `parameter_match` expects a dictionary.
40+
41+
Now that we have pre and post data, we use the `evaluate` method to compare them, which will return our evaluation result.
42+
43+
```python
44+
>>> check = CheckType.create(check_type="exact_match")
45+
>>> results = check.evaluate(bgp_reference_value, bgp_comparison_value)
46+
>>> results
47+
({'10.1.0.0': {'accepted_prefixes': {'new_value': 900, 'old_value': 1000},
48+
'received_prefixes': {'new_value': 999, 'old_value': 1000},
49+
'sent_prefixes': {'new_value': 1011, 'old_value': 1000}}},
50+
False)
51+
```
52+
53+
54+
## Arguments
55+
56+
Generally, for all of the `CheckTypes`, the arguments will be in the order `intended state`, `actual state`, `options`. For instance, the reference state would be the first argument and comparison state the second argument in the `exact_match` check type.
57+
58+
For regex or parameter matching, your provided regex or dictionary would be the first argument and the collected data would be the second argument.
59+
60+
# Customized JMESPath
61+
62+
Since `jdiff` works with JSON objects as data inputs, JMESPath was the obvious choice for traversing the data and extracting the value(s) to compare. However, JMESPath has a limitation where context is lost for the values it collects, in other words, for each given value that JMESPath returns, we cannot be sure what key it was part of.
63+
64+
Below is the output of `show bgp`.
65+
66+
```python
67+
>>> data = {
68+
"result": [
69+
{
70+
"vrfs": {
71+
"default": {
72+
"peerList": [
73+
{
74+
"linkType": "external",
75+
"localAsn": "65130.1100",
76+
"prefixesSent": 50,
77+
"receivedUpdates": 0,
78+
"peerAddress": "7.7.7.7",
79+
"state": "Idle",
80+
"updownTime": 1394,
81+
"asn": "1.2354",
82+
"routerId": "0.0.0.0"
83+
},
84+
{
85+
"linkType": "external",
86+
"localAsn": "65130.1100",
87+
"receivedUpdates": 0,
88+
"peerAddress": "10.1.0.0",
89+
"state": "Connected",
90+
"updownTime": 1394,
91+
"asn": "1.2354",
92+
"routerId": "0.0.0.0"
93+
}
94+
]
95+
}
96+
}
97+
}
98+
]
99+
}
100+
```
101+
A JMESPath expression to extract `state` is shown below.
102+
103+
```python
104+
>>> path = result[0].vrfs.default.peerList[*].state
105+
```
106+
107+
...which will return
108+
109+
```python
110+
>>> extract_data_from_json(data, path)
111+
["Idle", "Connected"]
112+
```
113+
114+
How can we understand that `Idle` is relative to peer 7.7.7.7 and `Connected` to peer `10.1.0.0`?
115+
We could index the output, but that would require some post-processing of the data. For that reason, `jdiff` uses a customized version of JMESPath where it is possible to define a reference key for the value(s) wanted. The reference key must be within `$` sign anchors and defined in a list, together with the value(s):
116+
117+
```python
118+
>>> path = "result[0].vrfs.default.peerList[*].[$peerAddress$,state]"
119+
```
120+
121+
That would give us...
122+
123+
```python
124+
>>> extract_data_from_json(pre,path)
125+
[{'7.7.7.7': {'state': 'Idle'}}, {'10.1.0.0': {'state': 'Connected'}}]
126+
```

docs/development.md

Whitespace-only changes.

docs/example.md

Whitespace-only changes.

docs/extra.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* Images */
2+
img {
3+
display: block;
4+
margin-left: auto;
5+
margin-right: auto;
6+
}
7+
8+
/* Tables */
9+
table {
10+
margin-bottom: 24px;
11+
width: 100%;
12+
}
13+
th {
14+
background-color: #f0f0f0;
15+
padding: 6px;
16+
}
17+
td {
18+
padding: 6px;
19+
}

docs/images/hld-architecture.png

68.1 KB
Loading

docs/images/jdiff-workflow.png

42.6 KB
Loading

docs/images/workflow.png

47.5 KB
Loading

docs/index.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# jdiff
2+
3+
`jdiff` is a lightweight Python library allowing you to examine structured data. `jdiff` provides an interface to intelligently compare JSON data objects and test for the presence (or absence) of keys. You can also examine and compare corresponding key-values.
4+
5+
The library heavily relies on [JMESPath](https://jmespath.org/) for traversing the JSON object and finding the values to be evaluated. More on that [here](architecture.md#customized-jmespath).
6+
7+
## Getting Started
8+
9+
10+
First, you import the `CheckType` class.
11+
12+
```python
13+
from jdiff import CheckType
14+
```
15+
16+
Get (or fabricate) some data. (This data may also be loaded from a file or from a string, more examples later.)
17+
18+
```python
19+
a = {"foo": "bar"}
20+
b = {"foo": "bar baz"}
21+
```
22+
23+
Call the `create` method of the `CheckType` class to get an instance of the check type you want to perform.
24+
25+
```python
26+
match = CheckType.create("exact_match")
27+
```
28+
29+
Evaluate the check type and the diff.
30+
```python
31+
match.evaluate(a, b)
32+
>>> ({'foo': {'new_value': 'bar baz', 'old_value': 'bar'}}, False)
33+
```
34+
35+
This results in a tuple:
36+
- The first value is the diff between the two data structures.
37+
- The second value is a boolean with the result of the check.
38+
39+
This diff can also show whether any keys were added or deleted.
40+
The second value returned will be the boolean result of the check. In this case, the two data structures were not an exact match.
41+
42+
43+
## Checking Data Structures
44+
45+
As shown in the example, the check evaluation both performs a diff and tests the objects. All of the concrete `CheckTypes` both perform the diff and their specified check.
46+
47+
More on the **check** part: The check provides a way to test some keys or values in our collected data. The check portion is focused on providing a boolean result of the test. There are a few different ways to check our data.
48+
49+
Below are the names of checks provided by the library. These both describe the type of check performed against the data and are used as an argument to instantiate that type of check with the `create` method, e.g. `CheckType.create("check_type")`.
50+
51+
- `exact_match`: the keys and values must match exactly between the two objects
52+
- `tolerance`: the keys must match and the values can differ according to the 'tolerance' value provided
53+
- `parameter_match`: a reference key and value is provided and its presence (or absence) is checked in the provided object
54+
- `regex`: a reference regex pattern is provided and is used to find a match in the provided object
55+
- `operator`: similar to parameter match, but the reference includes several different possible operators: 'in', 'bool', 'string', and numerical comparison with 'int' and 'float' to check against
56+
57+
`CheckTypes` are explained in more detail in the [architecture](architecture.md).
58+
59+
60+
## Workflow
61+
62+
| ![jdiff Workflow](./images/jdiff-workflow.png) |
63+
|:---:|
64+
| **`jdiff` Workflow** |
65+
66+
67+
1. The reference state object is retrieved or assembled. The structured data may be from:
68+
69+
- an API
70+
- another Python module/library
71+
- retrieved from a saved file
72+
- constructed programmatically
73+
74+
2. Some time passes where some change to the data may occur; then the comparison state is retrieved or assembled, often using a similar process used to get the reference state.
75+
3. The reference state is then compared to the current state via the jdiff library using one of the `CheckTypes`.
76+
4. The evaluate method is called on the `check` object, and the result is returned.
77+
78+
Please see [usage](usage.md) for commands and more information.

0 commit comments

Comments
 (0)