simple_jsonpath 0.1.2__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_jsonpath-0.1.2 → simple_jsonpath-0.2.2}/.github/workflows/CI.yml +4 -7
- {simple_jsonpath-0.1.2 → simple_jsonpath-0.2.2}/.gitignore +3 -0
- {simple_jsonpath-0.1.2 → simple_jsonpath-0.2.2}/Cargo.lock +3 -13
- {simple_jsonpath-0.1.2 → simple_jsonpath-0.2.2}/Cargo.toml +7 -2
- simple_jsonpath-0.2.2/LICENSE +25 -0
- simple_jsonpath-0.2.2/PKG-INFO +173 -0
- simple_jsonpath-0.2.2/README.md +150 -0
- simple_jsonpath-0.2.2/pyproject.toml +36 -0
- simple_jsonpath-0.2.2/python/simple_jsonpath/__init__.py +7 -0
- simple_jsonpath-0.2.2/python/simple_jsonpath/_simple_jsonpath.pyi +16 -0
- simple_jsonpath-0.2.2/python/simple_jsonpath/jsonpath.py +144 -0
- simple_jsonpath-0.2.2/python/simple_jsonpath/py.typed +0 -0
- simple_jsonpath-0.2.2/src/lib.rs +289 -0
- simple_jsonpath-0.1.2/PKG-INFO +0 -7
- simple_jsonpath-0.1.2/README.md +0 -7
- simple_jsonpath-0.1.2/pyproject.toml +0 -13
- simple_jsonpath-0.1.2/simple_jsonpath.pyi +0 -6
- simple_jsonpath-0.1.2/src/lib.rs +0 -74
|
@@ -7,9 +7,6 @@ name: CI
|
|
|
7
7
|
|
|
8
8
|
on:
|
|
9
9
|
push:
|
|
10
|
-
branches:
|
|
11
|
-
- main
|
|
12
|
-
- master
|
|
13
10
|
tags:
|
|
14
11
|
- '*'
|
|
15
12
|
pull_request:
|
|
@@ -40,7 +37,7 @@ jobs:
|
|
|
40
37
|
- uses: actions/checkout@v6
|
|
41
38
|
- uses: actions/setup-python@v6
|
|
42
39
|
with:
|
|
43
|
-
python-version: 3.
|
|
40
|
+
python-version: '>=3.10 <3.15'
|
|
44
41
|
- name: Build wheels
|
|
45
42
|
uses: PyO3/maturin-action@v1
|
|
46
43
|
with:
|
|
@@ -71,7 +68,7 @@ jobs:
|
|
|
71
68
|
- uses: actions/checkout@v6
|
|
72
69
|
- uses: actions/setup-python@v6
|
|
73
70
|
with:
|
|
74
|
-
python-version: 3.
|
|
71
|
+
python-version: '>=3.10 <3.15'
|
|
75
72
|
- name: Build wheels
|
|
76
73
|
uses: PyO3/maturin-action@v1
|
|
77
74
|
with:
|
|
@@ -103,7 +100,7 @@ jobs:
|
|
|
103
100
|
- uses: actions/checkout@v6
|
|
104
101
|
- uses: actions/setup-python@v6
|
|
105
102
|
with:
|
|
106
|
-
python-version: 3.
|
|
103
|
+
python-version: '>=3.10 <3.15'
|
|
107
104
|
architecture: ${{ matrix.platform.python_arch }}
|
|
108
105
|
- name: Build wheels
|
|
109
106
|
uses: PyO3/maturin-action@v1
|
|
@@ -130,7 +127,7 @@ jobs:
|
|
|
130
127
|
- uses: actions/checkout@v6
|
|
131
128
|
- uses: actions/setup-python@v6
|
|
132
129
|
with:
|
|
133
|
-
python-version: 3.
|
|
130
|
+
python-version: '>=3.10 <3.15'
|
|
134
131
|
- name: Build wheels
|
|
135
132
|
uses: PyO3/maturin-action@v1
|
|
136
133
|
with:
|
|
@@ -231,17 +231,6 @@ dependencies = [
|
|
|
231
231
|
"syn",
|
|
232
232
|
]
|
|
233
233
|
|
|
234
|
-
[[package]]
|
|
235
|
-
name = "pythonize"
|
|
236
|
-
version = "0.28.0"
|
|
237
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
238
|
-
checksum = "0b79f670c9626c8b651c0581011b57b6ba6970bb69faf01a7c4c0cfc81c43f95"
|
|
239
|
-
dependencies = [
|
|
240
|
-
"pyo3",
|
|
241
|
-
"serde",
|
|
242
|
-
"serde_json",
|
|
243
|
-
]
|
|
244
|
-
|
|
245
234
|
[[package]]
|
|
246
235
|
name = "quote"
|
|
247
236
|
version = "1.0.45"
|
|
@@ -427,13 +416,14 @@ dependencies = [
|
|
|
427
416
|
|
|
428
417
|
[[package]]
|
|
429
418
|
name = "simple_jsonpath"
|
|
430
|
-
version = "0.
|
|
419
|
+
version = "0.2.2"
|
|
431
420
|
dependencies = [
|
|
432
421
|
"pyo3",
|
|
433
|
-
"pythonize",
|
|
434
422
|
"rstest",
|
|
423
|
+
"serde",
|
|
435
424
|
"serde_json",
|
|
436
425
|
"serde_json_path",
|
|
426
|
+
"serde_json_path_core",
|
|
437
427
|
]
|
|
438
428
|
|
|
439
429
|
[[package]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "simple_jsonpath"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
|
|
@@ -12,9 +12,14 @@ crate-type = ["cdylib"]
|
|
|
12
12
|
[dependencies]
|
|
13
13
|
pyo3 = { version = "0.28.2", features = ["serde"] }
|
|
14
14
|
serde_json_path = { git = "https://github.com/seojumper/serde_json_path.git", branch = "quotes" }
|
|
15
|
+
serde_json_path_core = { git = "https://github.com/seojumper/serde_json_path.git", branch = "quotes" }
|
|
15
16
|
serde_json = "1.0.149"
|
|
16
|
-
|
|
17
|
+
serde = { version = "1.0.228", features = ["derive"] }
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
[dev-dependencies]
|
|
20
21
|
rstest = "0.26.1"
|
|
22
|
+
|
|
23
|
+
[profile.release]
|
|
24
|
+
lto = true
|
|
25
|
+
codegen-units = 1
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Copyright (c) 2026 Jeremy Spencer
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any
|
|
4
|
+
person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the
|
|
6
|
+
Software without restriction, including without
|
|
7
|
+
limitation the rights to use, copy, modify, merge,
|
|
8
|
+
publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software
|
|
10
|
+
is furnished to do so, subject to the following
|
|
11
|
+
conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice
|
|
14
|
+
shall be included in all copies or substantial portions
|
|
15
|
+
of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
18
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
19
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
20
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
21
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
22
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
23
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
24
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
25
|
+
DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_jsonpath
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Classifier: Intended Audience :: Developers
|
|
5
|
+
Classifier: Programming Language :: Rust
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
7
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Summary: A simple - yet quick - JSONPath implementation for querying JSON data.
|
|
16
|
+
Keywords: jsonpath
|
|
17
|
+
Author-email: Jeremy Spencer <seojumper@gmail.com>
|
|
18
|
+
License-Expression: MIT
|
|
19
|
+
Requires-Python: >=3.10, <3.15
|
|
20
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
21
|
+
Project-URL: Homepage, https://github.com/seojumper/simple_jsonpath
|
|
22
|
+
|
|
23
|
+
# simple_jsonpath
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install simple_jsonpath
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## About
|
|
32
|
+
|
|
33
|
+
This module is a JSONPath [RFC9535 - JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/html/rfc9535) utility library.
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
This module exposes a single simple type - a ***JsonPath*** which has two methods after instantiation.
|
|
38
|
+
|
|
39
|
+
- ***set_data()***: sets the data that will be queried against. If multiple queries will be performed against a single piece of JSON data, this helps with the type conversion cost involved. This function can be called with providing new data whenever the inner data held is wished to be changed while retaining already complied paths (useful for querying multiple similarly structured documents).
|
|
40
|
+
- ***find()***: given a path that is wished to be found in the previously set data, this function will perform the query logic. Mulitple calls to **find()** will query against the previously 'set' data.
|
|
41
|
+
- ***find_located()***: given a path that is wished to be found in the previously set data, this function can return a list of ***LocatedNode*** objects. Each ***LocatedNode*** object will have attributes related to the path where the node was located as well as their corresponding data. This method is slower than ***find()***, so should ideally only be used when path information for the found nodes is needed.
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
### 'Find' Example
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from simple_jsonpath import JsonPath
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
json_data = {
|
|
52
|
+
"address": {
|
|
53
|
+
"prefix-list": [
|
|
54
|
+
{
|
|
55
|
+
"prefix": "2001:db8::1/64",
|
|
56
|
+
"eui-64": [
|
|
57
|
+
None
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"link-local-address": [
|
|
62
|
+
{
|
|
63
|
+
"address": "fe80::1",
|
|
64
|
+
"link-local": [
|
|
65
|
+
None
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Instantiates the primary class
|
|
73
|
+
finder = JsonPath()
|
|
74
|
+
|
|
75
|
+
# Sets the data that is desired to be queried against
|
|
76
|
+
finder.set_data(json_data)
|
|
77
|
+
|
|
78
|
+
# A path is provided to query against the 'set' data. The path is internally parsed > used to qeury against the 'set' dataset.
|
|
79
|
+
# Notice that this implementaion allows for escaping of specials characters shorthand path syntax with single or double quotes
|
|
80
|
+
results = finder.find("$.address.'prefix-list'[*].prefix")
|
|
81
|
+
|
|
82
|
+
for data in results:
|
|
83
|
+
# Access the found node.
|
|
84
|
+
print(f"{data}")
|
|
85
|
+
# 2001:db8::1/64
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The inner implementation stores previously parsed 'paths'. This allows repeatedly used paths to bypass the parsing step invovled.
|
|
90
|
+
|
|
91
|
+
This is ideal for situations where multiple similar JSON documents will be searched in succession.
|
|
92
|
+
|
|
93
|
+
The same ***JsonPath*** object can then be reused with new data sets by calling ***set_data()*** on it again, and any previously parsed paths by the object will be retained.
|
|
94
|
+
|
|
95
|
+
Only when moving onto data of differing structure would it be potentially advisable to instantiate a new ***JsonPath*** object.
|
|
96
|
+
|
|
97
|
+
### 'Find Located' Example
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from simple_jsonpath import JsonPath, LocatedNode
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
json_data = {
|
|
104
|
+
"items": [
|
|
105
|
+
{
|
|
106
|
+
"address": {
|
|
107
|
+
"prefix-list": [
|
|
108
|
+
{
|
|
109
|
+
"prefix": "2001:db8::1/64",
|
|
110
|
+
"eui-64": [
|
|
111
|
+
None
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"link-local-address": [
|
|
116
|
+
{
|
|
117
|
+
"address": "fe80::1",
|
|
118
|
+
"link-local": [
|
|
119
|
+
None
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"address": {
|
|
127
|
+
"prefix-list": [
|
|
128
|
+
{
|
|
129
|
+
"prefix": "2001:db8::1/64",
|
|
130
|
+
"eui-64": [
|
|
131
|
+
None
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"link-local-address": [
|
|
136
|
+
{
|
|
137
|
+
"address": "fe80::1",
|
|
138
|
+
"link-local": [
|
|
139
|
+
None
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Instantiates the primary class
|
|
149
|
+
finder = JsonPath()
|
|
150
|
+
|
|
151
|
+
# Sets the data that is desired to be queried against
|
|
152
|
+
finder.set_data(json_data)
|
|
153
|
+
|
|
154
|
+
# Now we are interested in the path information where matches were found as well as the data
|
|
155
|
+
results: list[LocatedNode] = finder.find_located("$.items[*].address.'prefix-list'[*].prefix")
|
|
156
|
+
|
|
157
|
+
# Iterate through each found LocatedNode object
|
|
158
|
+
for data in results:
|
|
159
|
+
|
|
160
|
+
# Print the normalized full path where the node was found
|
|
161
|
+
print(f"{data.full_path}")
|
|
162
|
+
# $['items'][0]['address']['prefix-list'][0]['prefix']
|
|
163
|
+
|
|
164
|
+
# Iterate over the components of the found path
|
|
165
|
+
# Returned elements will either be a 'str' for keys or 'int' for index values
|
|
166
|
+
print(f"{', '.join([str(component) for component in data.path_components])}")
|
|
167
|
+
# $, items, 0, adddress, prefix-list, 0, prefix
|
|
168
|
+
|
|
169
|
+
# Access the found node.
|
|
170
|
+
print(f"{data.node}")
|
|
171
|
+
# 2001:db8::1/64
|
|
172
|
+
```
|
|
173
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# simple_jsonpath
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install simple_jsonpath
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## About
|
|
10
|
+
|
|
11
|
+
This module is a JSONPath [RFC9535 - JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/html/rfc9535) utility library.
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
This module exposes a single simple type - a ***JsonPath*** which has two methods after instantiation.
|
|
16
|
+
|
|
17
|
+
- ***set_data()***: sets the data that will be queried against. If multiple queries will be performed against a single piece of JSON data, this helps with the type conversion cost involved. This function can be called with providing new data whenever the inner data held is wished to be changed while retaining already complied paths (useful for querying multiple similarly structured documents).
|
|
18
|
+
- ***find()***: given a path that is wished to be found in the previously set data, this function will perform the query logic. Mulitple calls to **find()** will query against the previously 'set' data.
|
|
19
|
+
- ***find_located()***: given a path that is wished to be found in the previously set data, this function can return a list of ***LocatedNode*** objects. Each ***LocatedNode*** object will have attributes related to the path where the node was located as well as their corresponding data. This method is slower than ***find()***, so should ideally only be used when path information for the found nodes is needed.
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
### 'Find' Example
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from simple_jsonpath import JsonPath
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
json_data = {
|
|
30
|
+
"address": {
|
|
31
|
+
"prefix-list": [
|
|
32
|
+
{
|
|
33
|
+
"prefix": "2001:db8::1/64",
|
|
34
|
+
"eui-64": [
|
|
35
|
+
None
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"link-local-address": [
|
|
40
|
+
{
|
|
41
|
+
"address": "fe80::1",
|
|
42
|
+
"link-local": [
|
|
43
|
+
None
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Instantiates the primary class
|
|
51
|
+
finder = JsonPath()
|
|
52
|
+
|
|
53
|
+
# Sets the data that is desired to be queried against
|
|
54
|
+
finder.set_data(json_data)
|
|
55
|
+
|
|
56
|
+
# A path is provided to query against the 'set' data. The path is internally parsed > used to qeury against the 'set' dataset.
|
|
57
|
+
# Notice that this implementaion allows for escaping of specials characters shorthand path syntax with single or double quotes
|
|
58
|
+
results = finder.find("$.address.'prefix-list'[*].prefix")
|
|
59
|
+
|
|
60
|
+
for data in results:
|
|
61
|
+
# Access the found node.
|
|
62
|
+
print(f"{data}")
|
|
63
|
+
# 2001:db8::1/64
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The inner implementation stores previously parsed 'paths'. This allows repeatedly used paths to bypass the parsing step invovled.
|
|
68
|
+
|
|
69
|
+
This is ideal for situations where multiple similar JSON documents will be searched in succession.
|
|
70
|
+
|
|
71
|
+
The same ***JsonPath*** object can then be reused with new data sets by calling ***set_data()*** on it again, and any previously parsed paths by the object will be retained.
|
|
72
|
+
|
|
73
|
+
Only when moving onto data of differing structure would it be potentially advisable to instantiate a new ***JsonPath*** object.
|
|
74
|
+
|
|
75
|
+
### 'Find Located' Example
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from simple_jsonpath import JsonPath, LocatedNode
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
json_data = {
|
|
82
|
+
"items": [
|
|
83
|
+
{
|
|
84
|
+
"address": {
|
|
85
|
+
"prefix-list": [
|
|
86
|
+
{
|
|
87
|
+
"prefix": "2001:db8::1/64",
|
|
88
|
+
"eui-64": [
|
|
89
|
+
None
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"link-local-address": [
|
|
94
|
+
{
|
|
95
|
+
"address": "fe80::1",
|
|
96
|
+
"link-local": [
|
|
97
|
+
None
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"address": {
|
|
105
|
+
"prefix-list": [
|
|
106
|
+
{
|
|
107
|
+
"prefix": "2001:db8::1/64",
|
|
108
|
+
"eui-64": [
|
|
109
|
+
None
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"link-local-address": [
|
|
114
|
+
{
|
|
115
|
+
"address": "fe80::1",
|
|
116
|
+
"link-local": [
|
|
117
|
+
None
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Instantiates the primary class
|
|
127
|
+
finder = JsonPath()
|
|
128
|
+
|
|
129
|
+
# Sets the data that is desired to be queried against
|
|
130
|
+
finder.set_data(json_data)
|
|
131
|
+
|
|
132
|
+
# Now we are interested in the path information where matches were found as well as the data
|
|
133
|
+
results: list[LocatedNode] = finder.find_located("$.items[*].address.'prefix-list'[*].prefix")
|
|
134
|
+
|
|
135
|
+
# Iterate through each found LocatedNode object
|
|
136
|
+
for data in results:
|
|
137
|
+
|
|
138
|
+
# Print the normalized full path where the node was found
|
|
139
|
+
print(f"{data.full_path}")
|
|
140
|
+
# $['items'][0]['address']['prefix-list'][0]['prefix']
|
|
141
|
+
|
|
142
|
+
# Iterate over the components of the found path
|
|
143
|
+
# Returned elements will either be a 'str' for keys or 'int' for index values
|
|
144
|
+
print(f"{', '.join([str(component) for component in data.path_components])}")
|
|
145
|
+
# $, items, 0, adddress, prefix-list, 0, prefix
|
|
146
|
+
|
|
147
|
+
# Access the found node.
|
|
148
|
+
print(f"{data.node}")
|
|
149
|
+
# 2001:db8::1/64
|
|
150
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.12,<2.0"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simple_jsonpath"
|
|
7
|
+
requires-python = ">=3.10,<3.15"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Intended Audience :: Developers",
|
|
10
|
+
"Programming Language :: Rust",
|
|
11
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
12
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
]
|
|
20
|
+
authors = [
|
|
21
|
+
{ name = "Jeremy Spencer", email = "seojumper@gmail.com" },
|
|
22
|
+
]
|
|
23
|
+
description = "A simple - yet quick - JSONPath implementation for querying JSON data."
|
|
24
|
+
readme = "README.md"
|
|
25
|
+
license = "MIT"
|
|
26
|
+
license-files = ["LICENSE"]
|
|
27
|
+
keywords = ["jsonpath"]
|
|
28
|
+
|
|
29
|
+
dynamic = ["version"]
|
|
30
|
+
|
|
31
|
+
[tool.maturin]
|
|
32
|
+
python-source = "python"
|
|
33
|
+
module-name = "simple_jsonpath._simple_jsonpath"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/seojumper/simple_jsonpath"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""A Python module for querying JSON data using JSONPath expressions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SimpleJsonPath:
|
|
5
|
+
"""A parser object that can be reused for multiple queries on the same JSON data."""
|
|
6
|
+
def __init__(self) -> None: ...
|
|
7
|
+
def set_data_from_json_str(self, input_data: str) -> None:
|
|
8
|
+
"""Set the JSON data for the parser from a JSON string."""
|
|
9
|
+
...
|
|
10
|
+
def find_from_set_data(self, path: str) -> str:
|
|
11
|
+
"""Find the value(s) in the JSON data that match the given JSONPath expression, using a cache for parsed paths."""
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
def find_location_from_set_data(self, path: str) -> str:
|
|
15
|
+
"""Find the value(s) in the JSON data that match the given JSONPath expression, along with their locations, using a cache for parsed paths."""
|
|
16
|
+
...
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from ._simple_jsonpath import SimpleJsonPath as RustSimpleJsonPath
|
|
2
|
+
from typing import Any, Union
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import builtins
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
class PathComponentsIter:
|
|
8
|
+
def __init__(self, path: str, nodes: list[tuple[int, int]]):
|
|
9
|
+
self._current: int = 0
|
|
10
|
+
self._path: str = path
|
|
11
|
+
self._end: int = len(nodes)
|
|
12
|
+
self._items: list[tuple[int,int]] = nodes
|
|
13
|
+
|
|
14
|
+
def __next__(self) -> Union[str, int]:
|
|
15
|
+
|
|
16
|
+
if self._current >= self._end:
|
|
17
|
+
raise StopIteration
|
|
18
|
+
if self._current == 0:
|
|
19
|
+
self._current +=1
|
|
20
|
+
return "$"
|
|
21
|
+
else:
|
|
22
|
+
item = self._items[self._current]
|
|
23
|
+
if item[0] == 0:
|
|
24
|
+
self._current += 1
|
|
25
|
+
return item[1]
|
|
26
|
+
else:
|
|
27
|
+
self._current += 1
|
|
28
|
+
return self._path[item[0]:item[1]]
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class PathComponents:
|
|
32
|
+
_path: str
|
|
33
|
+
_items: list[tuple[int, int]]
|
|
34
|
+
|
|
35
|
+
def __len__(self) -> int:
|
|
36
|
+
return len(self._items)
|
|
37
|
+
|
|
38
|
+
def __iter__(self) -> PathComponentsIter:
|
|
39
|
+
return PathComponentsIter(self._path, self._items)
|
|
40
|
+
|
|
41
|
+
def __getitem__(self, index: int) -> Union[str, int]:
|
|
42
|
+
if index >= len(self._items):
|
|
43
|
+
raise IndexError("Index out of range")
|
|
44
|
+
elif index == 0:
|
|
45
|
+
return "$"
|
|
46
|
+
elif index < 0:
|
|
47
|
+
raise IndexError("Negative indexing is not supported")
|
|
48
|
+
else:
|
|
49
|
+
item = self._items[index]
|
|
50
|
+
if item[0] == 0:
|
|
51
|
+
return item[1]
|
|
52
|
+
else:
|
|
53
|
+
return self._path[item[0]:item[1]]
|
|
54
|
+
def __contains__(self, item: Union[int, str]) -> bool:
|
|
55
|
+
if isinstance(item, int):
|
|
56
|
+
for i in range(len(self._items)):
|
|
57
|
+
if i == 0:
|
|
58
|
+
continue
|
|
59
|
+
else:
|
|
60
|
+
if self._items[i][0] == 0 and self._items[i][1] == item:
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
else:
|
|
64
|
+
for i in range(len(self._items[1:])):
|
|
65
|
+
if i == 0:
|
|
66
|
+
if item == "$":
|
|
67
|
+
return True
|
|
68
|
+
else:
|
|
69
|
+
if self._path[self._items[i][0]:self._items[i][1]] == item:
|
|
70
|
+
return True
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
class LocatedNode:
|
|
74
|
+
"""A struct to hold the located nodes found from a located JSONPath query."""
|
|
75
|
+
def __init__(self, full_path: str, path_components: list[tuple[int, int]], node: Union[str,int,float,bool,None,dict[str, Any], list[Any]]) -> None:
|
|
76
|
+
self._full_path: str = full_path
|
|
77
|
+
self._path_components: PathComponents = PathComponents(full_path, path_components)
|
|
78
|
+
self._node: Union[str,int,float,bool,None,dict[str, Any], list[Any]] = node
|
|
79
|
+
|
|
80
|
+
@builtins.property
|
|
81
|
+
def path_components(self) -> PathComponents:
|
|
82
|
+
"""An iterator that yields the path components of the last query result."""
|
|
83
|
+
return self._path_components
|
|
84
|
+
@builtins.property
|
|
85
|
+
def full_path(self) -> str:
|
|
86
|
+
"""The full path of the last query result."""
|
|
87
|
+
return self._full_path
|
|
88
|
+
@builtins.property
|
|
89
|
+
def node(self) -> Union[str,int,float,bool,None,dict[str, Any], list[Any]]:
|
|
90
|
+
"""The node value of the last query result."""
|
|
91
|
+
return self._node
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class JsonPath:
|
|
95
|
+
"""A simple JSONPath implementation for querying JSON data.
|
|
96
|
+
|
|
97
|
+
It uses a Rust backend for performance and supports caching of parsed
|
|
98
|
+
JSONPath expressions for repeated queries against the same JSON data.
|
|
99
|
+
"""
|
|
100
|
+
def __init__(self) -> None:
|
|
101
|
+
self._parser = RustSimpleJsonPath()
|
|
102
|
+
|
|
103
|
+
def set_data(self, input_data: Union[dict[str, Any], list[Any]]) -> None:
|
|
104
|
+
"""Set the JSON data for the parser from a Python dictionary or list.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
input_data: The JSON data to set, as a Python dictionary or list.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
None
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ValueError: If the input data is not a valid JSON object or array.
|
|
114
|
+
"""
|
|
115
|
+
self._parser.set_data_from_json_str(json.dumps(input_data))
|
|
116
|
+
|
|
117
|
+
def find(self, path: str) -> list[Any]:
|
|
118
|
+
"""Find the value(s) in the JSON data that match the given JSONPath expression.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
path: The JSONPath expression to evaluate.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
A list of values that match the JSONPath expression.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If the JSONPath expression is invalid.
|
|
128
|
+
"""
|
|
129
|
+
return json.loads(self._parser.find_from_set_data(path))
|
|
130
|
+
|
|
131
|
+
def find_located(self, path: str) -> list[LocatedNode]:
|
|
132
|
+
"""Find the value(s) in the JSON data that match the given JSONPath expression, along with their locations.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
path: The JSONPath expression to evaluate.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
A list of LocatedNode objects that match the JSONPath expression.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If the JSONPath expression is invalid.
|
|
142
|
+
"""
|
|
143
|
+
result = json.loads(self._parser.find_location_from_set_data(path))
|
|
144
|
+
return [LocatedNode(item['full_path'], item['path_components'], item['node']) for item in result]
|
|
File without changes
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
use pyo3::prelude::*;
|
|
4
|
+
use serde_json_path::NormalizedPath;
|
|
5
|
+
use serde_json::Value;
|
|
6
|
+
use serde::{Serialize};
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/// A Python module for querying JSON data using JSONPath expressions.
|
|
10
|
+
#[pymodule]
|
|
11
|
+
#[pyo3(name = "_simple_jsonpath")]
|
|
12
|
+
mod simple_jsonpath {
|
|
13
|
+
use super::*;
|
|
14
|
+
use serde_json_path::JsonPath;
|
|
15
|
+
use serde_json::Value;
|
|
16
|
+
use std::{collections::HashMap, sync::Arc};
|
|
17
|
+
|
|
18
|
+
/// A parser object that can be reused for multiple queries on the same JSON data.
|
|
19
|
+
#[pyclass]
|
|
20
|
+
pub struct SimpleJsonPath {
|
|
21
|
+
inner: HashMap<String, JsonPath>,
|
|
22
|
+
data: Arc<Value>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[pymethods]
|
|
26
|
+
impl SimpleJsonPath {
|
|
27
|
+
/// Create a new SimpleJsonPath object with an empty cache and null data.
|
|
28
|
+
#[new]
|
|
29
|
+
pub fn new() -> PyResult<Self> {
|
|
30
|
+
Ok(Self { inner: HashMap::new(), data: Arc::new(Value::Null) })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Set the JSON data for the parser from a JSON string.
|
|
34
|
+
pub fn set_data_from_json_str(&mut self, input_data: &str) -> PyResult<()> {
|
|
35
|
+
let value: Value = serde_json::from_str(input_data).map_err(|e| {
|
|
36
|
+
pyo3::exceptions::PyValueError::new_err(format!("Invalid JSON string: {}", e))
|
|
37
|
+
})?;
|
|
38
|
+
self.data = Arc::new(value);
|
|
39
|
+
Ok(())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Find the value(s) in the JSON data that match the given JSONPath expression, using a cache for parsed paths.
|
|
43
|
+
pub fn find_from_set_data(&mut self, path: &str) -> PyResult<String> {
|
|
44
|
+
if !self.inner.contains_key(path) {
|
|
45
|
+
let json_path = JsonPath::parse(path).map_err(|e| {
|
|
46
|
+
pyo3::exceptions::PyValueError::new_err(format!(
|
|
47
|
+
"Parse error at position {} for path {}: {}",
|
|
48
|
+
e.position(),
|
|
49
|
+
path,
|
|
50
|
+
e.message()
|
|
51
|
+
))
|
|
52
|
+
})?;
|
|
53
|
+
self.inner.insert(path.to_string(), json_path);
|
|
54
|
+
}
|
|
55
|
+
let json_path = self.inner.get(path).unwrap();
|
|
56
|
+
let result = json_path.query(&self.data).all();
|
|
57
|
+
let pyresult: String = serde_json::to_string(&result).map_err(|e| {
|
|
58
|
+
pyo3::exceptions::PyValueError::new_err(format!(
|
|
59
|
+
"Error converting result to JSON string: {}",
|
|
60
|
+
e
|
|
61
|
+
))
|
|
62
|
+
})?;
|
|
63
|
+
Ok(pyresult)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Find the value(s) in the JSON data that match the given JSONPath expression, along with their locations, using a cache for parsed paths.
|
|
67
|
+
pub fn find_location_from_set_data(
|
|
68
|
+
&mut self,
|
|
69
|
+
path: &str,
|
|
70
|
+
) -> PyResult<String> {
|
|
71
|
+
if !self.inner.contains_key(path) {
|
|
72
|
+
let json_path = JsonPath::parse(path).map_err(|e| {
|
|
73
|
+
pyo3::exceptions::PyValueError::new_err(format!(
|
|
74
|
+
"Parse error at position {} for path {}: {}",
|
|
75
|
+
e.position(),
|
|
76
|
+
path,
|
|
77
|
+
e.message()
|
|
78
|
+
))
|
|
79
|
+
})?;
|
|
80
|
+
self.inner.insert(path.to_string(), json_path);
|
|
81
|
+
}
|
|
82
|
+
let json_path = self.inner.get(path).unwrap();
|
|
83
|
+
let result = json_path.query_located(&self.data).all();
|
|
84
|
+
let mut located_nodes = Vec::new();
|
|
85
|
+
for item in &result {
|
|
86
|
+
located_nodes.push(LocatedNode::new(item.location(), item.node()));
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
let pyresult: String = serde_json::to_string(&located_nodes).map_err(|e| {
|
|
90
|
+
pyo3::exceptions::PyValueError::new_err(format!(
|
|
91
|
+
"Error converting result to JSON string: {}",
|
|
92
|
+
e
|
|
93
|
+
))
|
|
94
|
+
})?;
|
|
95
|
+
Ok(pyresult)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
/// A struct to hold the located nodes for serialization.
|
|
102
|
+
#[derive(Serialize)]
|
|
103
|
+
struct LocatedNode<'a> {
|
|
104
|
+
full_path: String,
|
|
105
|
+
path_components: Vec<(usize, usize)>,
|
|
106
|
+
node: &'a Value,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl<'a> LocatedNode<'a> {
|
|
110
|
+
fn new(full_path: &'a NormalizedPath, node: &'a Value) -> LocatedNode<'a> {
|
|
111
|
+
let (full_path, path_components) = split_normalized_path_component_ranges(full_path);
|
|
112
|
+
LocatedNode { full_path, path_components, node }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Splits a normalized JSONPath into byte ranges `(start, end)` per component.
|
|
117
|
+
///
|
|
118
|
+
/// Example: `$['items'][0]['name']` -> `[(0,1), (1,10), (10,13), (13,21)]`
|
|
119
|
+
fn split_normalized_path_component_ranges(path: &NormalizedPath) -> (String, Vec<(usize, usize)>) {
|
|
120
|
+
let path_str = path.to_string();
|
|
121
|
+
let chars = &path_str.as_str();
|
|
122
|
+
|
|
123
|
+
let mut ranges = Vec::with_capacity(path.len() + 1);
|
|
124
|
+
ranges.push((0, 1)); // Start with the root component '$'
|
|
125
|
+
|
|
126
|
+
#[derive(Debug)]
|
|
127
|
+
enum State {
|
|
128
|
+
Start,
|
|
129
|
+
Root,
|
|
130
|
+
InBracket,
|
|
131
|
+
InQuotedField,
|
|
132
|
+
InEscapedChar,
|
|
133
|
+
InIndex,
|
|
134
|
+
}
|
|
135
|
+
let mut num_start = None;
|
|
136
|
+
let mut start = None;
|
|
137
|
+
let mut state = State::Start;
|
|
138
|
+
for (i, c) in chars.char_indices() {
|
|
139
|
+
match state {
|
|
140
|
+
State::Start => {
|
|
141
|
+
if c == '$' {
|
|
142
|
+
state = State::Root;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
State::Root => {
|
|
146
|
+
if c == '[' {
|
|
147
|
+
state = State::InBracket;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
State::InBracket => {
|
|
151
|
+
if c == ']' {
|
|
152
|
+
state = State::Root;
|
|
153
|
+
} else if c == '\'' {
|
|
154
|
+
state = State::InQuotedField;
|
|
155
|
+
} else {
|
|
156
|
+
state = State::InIndex;
|
|
157
|
+
num_start = Some(i);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
State::InQuotedField => {
|
|
161
|
+
match c {
|
|
162
|
+
'\\' => state = State::InEscapedChar,
|
|
163
|
+
'\'' => {
|
|
164
|
+
match start {
|
|
165
|
+
Some(_) =>
|
|
166
|
+
ranges.push((start.take().unwrap(), i)),
|
|
167
|
+
None => ranges.push((i+1, i-1)),
|
|
168
|
+
}
|
|
169
|
+
state = State::InBracket;
|
|
170
|
+
},
|
|
171
|
+
_ => {
|
|
172
|
+
match start {
|
|
173
|
+
Some(_) => {},
|
|
174
|
+
None => start = Some(i),
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
State::InEscapedChar => {
|
|
180
|
+
state = State::InQuotedField;
|
|
181
|
+
}
|
|
182
|
+
State::InIndex => {
|
|
183
|
+
if c == ']' {
|
|
184
|
+
if num_start.is_some() {
|
|
185
|
+
let num = chars[num_start.take().unwrap()..i].parse::<usize>().unwrap();
|
|
186
|
+
ranges.push((0,num));
|
|
187
|
+
}
|
|
188
|
+
state = State::Root;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
(path_str, ranges)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[cfg(test)]
|
|
198
|
+
mod tests {
|
|
199
|
+
use super::split_normalized_path_component_ranges;
|
|
200
|
+
use super::simple_jsonpath::SimpleJsonPath;
|
|
201
|
+
use rstest::rstest;
|
|
202
|
+
use serde_json::Number;
|
|
203
|
+
use serde_json::Value;
|
|
204
|
+
use serde_json_path::{JsonPath};
|
|
205
|
+
|
|
206
|
+
#[rstest]
|
|
207
|
+
#[case("$.router.bgp[0].bgp.router_id.interface")]
|
|
208
|
+
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.default.'ipv4-unicast'")]
|
|
209
|
+
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'log-neighbor-changes'")]
|
|
210
|
+
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'graceful-restart'")]
|
|
211
|
+
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'update-delay'")]
|
|
212
|
+
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].template.'peer-policy'[*]")]
|
|
213
|
+
#[case(
|
|
214
|
+
"$.router.'Cisco-IOS-XE-bgp:bgp'[0].'address-family'.'no-vrf'.ipv4[0].'ipv4-unicast'.'aggregate-address'[*]"
|
|
215
|
+
)]
|
|
216
|
+
fn path(#[case] input: &str) {
|
|
217
|
+
let result = JsonPath::parse(input);
|
|
218
|
+
match &result {
|
|
219
|
+
Ok(path) => {}
|
|
220
|
+
Err(e) => {
|
|
221
|
+
println!("Parse error at position {}: {}", e.position(), e.message());
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
assert!(result.is_ok());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn split_normalized_path_into_component_ranges() {
|
|
229
|
+
let data: Value = serde_json::from_str(r#"{"items":[{"name":"a"}] }"#).unwrap();
|
|
230
|
+
let path = JsonPath::parse("$.items[0].name").unwrap();
|
|
231
|
+
let located = path.query_located(&data).all();
|
|
232
|
+
let location = located.first().unwrap().location();
|
|
233
|
+
// $['items'][0]['name']
|
|
234
|
+
println!("Getting here");
|
|
235
|
+
let ranges = split_normalized_path_component_ranges(&location);
|
|
236
|
+
println!("Getting here 2");
|
|
237
|
+
let expected = vec![(0, 1), (3, 7), (0, 0), (15, 18)];
|
|
238
|
+
for range in &ranges.1 {
|
|
239
|
+
println!("Component: {}", &location.to_string()[range.0..range.1]);
|
|
240
|
+
}
|
|
241
|
+
println!("Getting here 3");
|
|
242
|
+
|
|
243
|
+
assert_eq!(ranges.1, expected);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
// #[test]
|
|
249
|
+
// fn find_location() {
|
|
250
|
+
// let data = r#"[
|
|
251
|
+
// {"name": "1/0/1", "description": "Test Interface 1/0/1", "media-type": "sfp", "switchport-conf": {"switchport": false}, "arp": {"timeout": 1000}, "bfd": {"Cisco-IOS-XE-bfd:enable": true, "Cisco-IOS-XE-bfd:local-address": "10.100.1.10", "Cisco-IOS-XE-bfd:interval-interface": {"msecs": 300, "min_rx": 300, "multiplier": 3}, "Cisco-IOS-XE-bfd:echo": false}, "bandwidth": {"kilobits": 1000000}, "mpls": {"Cisco-IOS-XE-mpls:ip": [null], "Cisco-IOS-XE-mpls:mtu": 1400}, "vrf": {"forwarding": "PRODUCTION"}, "ip": {"access-group": {"in": {"acl": {"acl-name": "ACL_IN", "in": [null]}}, "out": {"acl": {"acl-name": "ACL_OUT", "out": [null]}}}, "arp": {"inspection": {"limit": {"rate": 1000}, "trust": [null]}}, "address": {"primary": {"address": "10.100.1.10", "mask": "255.255.255.0"}}, "Cisco-IOS-XE-nat:nat": {"inside": [null]}, "helper-address": [{"address": "10.100.1.50", "vrf": "PRODUCTION"}], "pim": {"Cisco-IOS-XE-multicast:border": [null], "Cisco-IOS-XE-multicast:bfd": [null], "Cisco-IOS-XE-multicast:pim-mode-choice-cfg": {"sparse-mode": {}}, "Cisco-IOS-XE-multicast:dr-priority": 1000}, "proxy-arp": false, "redirects": false, "dhcp": {"Cisco-IOS-XE-dhcp:relay": {"information": {"option": {"vpn-id": [null]}}, "source-interface": "Loopback150"}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST", "direction": "input"}]}, "Cisco-IOS-XE-icmp:unreachables": false, "Cisco-IOS-XE-igmp:igmp": {"version": 3}, "Cisco-IOS-XE-nbar:nbar": {"protocol-discovery": {}}, "Cisco-IOS-XE-ospf:router-ospf": {"ospf": {"process-id": [{"id": 10, "area": [{"area-id": 0}]}], "cost": 10, "dead-interval": 60, "hello-interval": 15, "mtu-ignore": true, "message-digest-key": [{"id": 1, "md5": {"auth-type": 0, "auth-key": "cisco"}}], "network": {"point-to-point": [null]}, "multi-area": {"multi-area-id": [{"area-id": 1}, {"area-id": 2}]}, "priority": 10, "ttl-security": {"hops": 5}}}}, "ipv6": {"address": {"prefix-list": [{"prefix": "2001:db8::1/64", "eui-64": [null]}], "link-local-address": [{"address": "fe80::1", "link-local": [null]}]}, "enable": [null], "mtu": 1600, "nd": {"Cisco-IOS-XE-nd:ra": {"suppress": {"all": [null]}}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST_V6", "direction": "input"}]}, "Cisco-IOS-XE-multicast:pim-conf": {"pim": true}, "Cisco-IOS-XE-multicast:pim-container": {"bfd": [null], "dr-priority": 1000}}, "load-interval": 30, "logging": {"event": {"link-status-enable": true}}, "mtu": 1600, "Cisco-IOS-XE-cdp:cdp": {"enable": true, "tlv": {"default-wrp": {"app": false}, "server-location-config": false, "location-config": false}}, "Cisco-IOS-XE-dot1x:dot1x": {"pae": "authenticator", "max-reauth-req": 3, "max-req": 3, "timeout": {"auth-period": 10000, "held-period": 40000, "quiet-period": 10000, "ratelimit-period": 5000, "server-timeout": 5000, "start-period": 2000, "supp-timeout": 2000, "tx-period": 2000}}, "Cisco-IOS-XE-ethernet:port-settings": {"speed": {"speed-value": "100"}, "auto-negotiation": "disable"}, "Cisco-IOS-XE-ethernet:speed": {"nonegotiate": [null]}, "Cisco-IOS-XE-ospfv3:ospfv3": {"cost-config": {"value": 10}, "network-type": {"point-to-point": [null]}}, "Cisco-IOS-XE-policy:service-policy": {"input": "POLICY_IN", "output": "POLICY_OUT"}, "Cisco-IOS-XE-sanet:authentication": {"periodic": [null], "timer": {"reauthenticate": {"server-config": [null]}}}, "Cisco-IOS-XE-sanet:mab": {"eap": [null]}, "Cisco-IOS-XE-snmp:snmp": {"trap": {"link-status": true}}},
|
|
252
|
+
// {"name": "1/0/1", "description": "Test Interface 1/0/1", "media-type": "sfp", "switchport-conf": {"switchport": false}, "arp": {"timeout": 1000}, "bfd": {"Cisco-IOS-XE-bfd:enable": true, "Cisco-IOS-XE-bfd:local-address": "10.100.1.10", "Cisco-IOS-XE-bfd:interval-interface": {"msecs": 300, "min_rx": 300, "multiplier": 3}, "Cisco-IOS-XE-bfd:echo": false}, "bandwidth": {"kilobits": 1000000}, "mpls": {"Cisco-IOS-XE-mpls:ip": [null], "Cisco-IOS-XE-mpls:mtu": 1400}, "vrf": {"forwarding": "PRODUCTION"}, "ip": {"access-group": {"in": {"acl": {"acl-name": "ACL_IN", "in": [null]}}, "out": {"acl": {"acl-name": "ACL_OUT", "out": [null]}}}, "arp": {"inspection": {"limit": {"rate": 1000}, "trust": [null]}}, "address": {"primary": {"address": "10.100.1.10", "mask": "255.255.255.0"}}, "Cisco-IOS-XE-nat:nat": {"inside": [null]}, "helper-address": [{"address": "10.100.1.50", "vrf": "PRODUCTION"}], "pim": {"Cisco-IOS-XE-multicast:border": [null], "Cisco-IOS-XE-multicast:bfd": [null], "Cisco-IOS-XE-multicast:pim-mode-choice-cfg": {"sparse-mode": {}}, "Cisco-IOS-XE-multicast:dr-priority": 1000}, "proxy-arp": false, "redirects": false, "dhcp": {"Cisco-IOS-XE-dhcp:relay": {"information": {"option": {"vpn-id": [null]}}, "source-interface": "Loopback150"}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST", "direction": "input"}]}, "Cisco-IOS-XE-icmp:unreachables": false, "Cisco-IOS-XE-igmp:igmp": {"version": 3}, "Cisco-IOS-XE-nbar:nbar": {"protocol-discovery": {}}, "Cisco-IOS-XE-ospf:router-ospf": {"ospf": {"process-id": [{"id": 10, "area": [{"area-id": 0}]}], "cost": 10, "dead-interval": 60, "hello-interval": 15, "mtu-ignore": true, "message-digest-key": [{"id": 1, "md5": {"auth-type": 0, "auth-key": "cisco"}}], "network": {"point-to-point": [null]}, "multi-area": {"multi-area-id": [{"area-id": 1}, {"area-id": 2}]}, "priority": 10, "ttl-security": {"hops": 5}}}}, "ipv6": {"address": {"prefix-list": [{"prefix": "2001:db8::1/64", "eui-64": [null]}], "link-local-address": [{"address": "fe80::1", "link-local": [null]}]}, "enable": [null], "mtu": 1600, "nd": {"Cisco-IOS-XE-nd:ra": {"suppress": {"all": [null]}}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST_V6", "direction": "input"}]}, "Cisco-IOS-XE-multicast:pim-conf": {"pim": true}, "Cisco-IOS-XE-multicast:pim-container": {"bfd": [null], "dr-priority": 1000}}, "load-interval": 30, "logging": {"event": {"link-status-enable": true}}, "mtu": 1600, "Cisco-IOS-XE-cdp:cdp": {"enable": true, "tlv": {"default-wrp": {"app": false}, "server-location-config": false, "location-config": false}}, "Cisco-IOS-XE-dot1x:dot1x": {"pae": "authenticator", "max-reauth-req": 3, "max-req": 3, "timeout": {"auth-period": 10000, "held-period": 40000, "quiet-period": 10000, "ratelimit-period": 5000, "server-timeout": 5000, "start-period": 2000, "supp-timeout": 2000, "tx-period": 2000}}, "Cisco-IOS-XE-ethernet:port-settings": {"speed": {"speed-value": "100"}, "auto-negotiation": "disable"}, "Cisco-IOS-XE-ethernet:speed": {"nonegotiate": [null]}, "Cisco-IOS-XE-ospfv3:ospfv3": {"cost-config": {"value": 10}, "network-type": {"point-to-point": [null]}}, "Cisco-IOS-XE-policy:service-policy": {"input": "POLICY_IN", "output": "POLICY_OUT"}, "Cisco-IOS-XE-sanet:authentication": {"periodic": [null], "timer": {"reauthenticate": {"server-config": [null]}}}, "Cisco-IOS-XE-sanet:mab": {"eap": [null]}, "Cisco-IOS-XE-snmp:snmp": {"trap": {"link-status": true}}}
|
|
253
|
+
// ]"#;
|
|
254
|
+
// let path = "$[*].name";
|
|
255
|
+
// let data: Value = serde_json::from_str(data).unwrap();
|
|
256
|
+
// let data_str = serde_json::to_string(&data).unwrap();
|
|
257
|
+
// let mut finder = SimpleJsonPath::new().unwrap();
|
|
258
|
+
// finder.set_data_from_json_str(&data_str).unwrap();
|
|
259
|
+
// let result: Value = serde_json::from_str(&finder.find_location_from_set_data(path).unwrap()).unwrap();
|
|
260
|
+
// assert_eq!(result.as_array().unwrap().len(), 2);
|
|
261
|
+
// let array = result.as_array().unwrap();
|
|
262
|
+
// let path_components_1 = Value::Array(vec![Value::String("$".to_string()), Value::Number(Number::from_str("0").unwrap()), Value::String("name".to_string())]);
|
|
263
|
+
// let path_components_2 = Value::Array(vec![Value::String("$".to_string()), Value::Number(Number::from_str("1").unwrap()), Value::String("name".to_string())]);
|
|
264
|
+
// assert_eq!(array.len(), 2);
|
|
265
|
+
|
|
266
|
+
// println!("{:?}", array);
|
|
267
|
+
// let located_node1 = array[0].as_object().unwrap();
|
|
268
|
+
// let full_path = located_node1.get("full_path").unwrap().as_str().unwrap();
|
|
269
|
+
// assert_eq!(full_path, " ");
|
|
270
|
+
// let path_components = located_node1.get("path_components").unwrap().as_array().unwrap();
|
|
271
|
+
// assert_eq!(path_components, path_components_1.as_array().unwrap());
|
|
272
|
+
// let nodes = located_node1.get("nodes").unwrap().as_array().unwrap();
|
|
273
|
+
// assert_eq!(nodes.len(), 1);
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
// let located_node2 = array[1].as_object().unwrap();
|
|
277
|
+
// let full_path = located_node2.get("full_path").unwrap().as_str().unwrap();
|
|
278
|
+
// assert_eq!(full_path, "$[1]['name']");
|
|
279
|
+
// let path_components = located_node2.get("path_components").unwrap().as_array().unwrap();
|
|
280
|
+
// assert_eq!(path_components, path_components_2.as_array().unwrap());
|
|
281
|
+
// let nodes = located_node2.get("nodes").unwrap().as_array().unwrap();
|
|
282
|
+
// assert_eq!(nodes.len(), 1);
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
// // assert_eq!(result.as_array().unwrap()[0].as_str().unwrap(), "1/0/1");
|
|
288
|
+
// }
|
|
289
|
+
}
|
simple_jsonpath-0.1.2/PKG-INFO
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: simple_jsonpath
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Classifier: Programming Language :: Rust
|
|
5
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
-
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
7
|
-
Requires-Python: >=3.8
|
simple_jsonpath-0.1.2/README.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["maturin>=1.12,<2.0"]
|
|
3
|
-
build-backend = "maturin"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "simple_jsonpath"
|
|
7
|
-
requires-python = ">=3.8"
|
|
8
|
-
classifiers = [
|
|
9
|
-
"Programming Language :: Rust",
|
|
10
|
-
"Programming Language :: Python :: Implementation :: CPython",
|
|
11
|
-
"Programming Language :: Python :: Implementation :: PyPy",
|
|
12
|
-
]
|
|
13
|
-
dynamic = ["version"]
|
simple_jsonpath-0.1.2/src/lib.rs
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
use pyo3::{prelude::*};
|
|
2
|
-
use serde_json_path::{JsonPath};
|
|
3
|
-
|
|
4
|
-
/// A Python module for querying JSON data using JSONPath expressions.
|
|
5
|
-
#[pymodule]
|
|
6
|
-
mod simple_jsonpath {
|
|
7
|
-
use super::*;
|
|
8
|
-
|
|
9
|
-
/// Find the value(s) in the JSON data that match the given JSONPath expression.
|
|
10
|
-
#[pyfunction]
|
|
11
|
-
fn find(path: String, data: String) -> PyResult<Option<String>> {
|
|
12
|
-
match serde_json::from_str(&data) {
|
|
13
|
-
Ok(json) => {
|
|
14
|
-
match JsonPath::parse(&path) {
|
|
15
|
-
Ok(json_path) => {
|
|
16
|
-
let result = json_path.query(&json).all();
|
|
17
|
-
if result.is_empty() {
|
|
18
|
-
Ok(None)
|
|
19
|
-
} else {
|
|
20
|
-
// Convert the result to a JSON string
|
|
21
|
-
match serde_json::to_string(&result) {
|
|
22
|
-
Ok(json_result) => Ok(Some(json_result)),
|
|
23
|
-
Err(e) => return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Error converting result to JSON string: {}", e))),
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Parse error at position {} for path {}:\n{}", e.position(), path, e.message()))),
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Invalid JSON data: {}", e))),
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
#[cfg(test)]
|
|
37
|
-
mod tests {
|
|
38
|
-
use super::*;
|
|
39
|
-
use rstest::rstest;
|
|
40
|
-
use serde_json_path::{JsonPath, ParseError};
|
|
41
|
-
use serde_json::Value;
|
|
42
|
-
|
|
43
|
-
#[rstest]
|
|
44
|
-
#[case("$.router.bgp[0].bgp.router_id.interface")]
|
|
45
|
-
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.default.'ipv4-unicast'")]
|
|
46
|
-
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'log-neighbor-changes'")]
|
|
47
|
-
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'graceful-restart'")]
|
|
48
|
-
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'update-delay'")]
|
|
49
|
-
#[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].template.'peer-policy'[*]")]
|
|
50
|
-
#[case(
|
|
51
|
-
"$.router.'Cisco-IOS-XE-bgp:bgp'[0].'address-family'.'no-vrf'.ipv4[0].'ipv4-unicast'.'aggregate-address'[*]"
|
|
52
|
-
)]
|
|
53
|
-
fn path(#[case] input: &str) {
|
|
54
|
-
let result = JsonPath::parse(input);
|
|
55
|
-
match &result {
|
|
56
|
-
Ok(path) => {}
|
|
57
|
-
Err(e) => {
|
|
58
|
-
println!("Parse error at position {}: {}", e.position(), e.message());
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
assert!(result.is_ok());
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
#[test]
|
|
65
|
-
fn find() {
|
|
66
|
-
let data = r#"
|
|
67
|
-
{"name": "1/0/1", "description": "Test Interface 1/0/1", "media-type": "sfp", "switchport-conf": {"switchport": false}, "arp": {"timeout": 1000}, "bfd": {"Cisco-IOS-XE-bfd:enable": true, "Cisco-IOS-XE-bfd:local-address": "10.100.1.10", "Cisco-IOS-XE-bfd:interval-interface": {"msecs": 300, "min_rx": 300, "multiplier": 3}, "Cisco-IOS-XE-bfd:echo": false}, "bandwidth": {"kilobits": 1000000}, "mpls": {"Cisco-IOS-XE-mpls:ip": [null], "Cisco-IOS-XE-mpls:mtu": 1400}, "vrf": {"forwarding": "PRODUCTION"}, "ip": {"access-group": {"in": {"acl": {"acl-name": "ACL_IN", "in": [null]}}, "out": {"acl": {"acl-name": "ACL_OUT", "out": [null]}}}, "arp": {"inspection": {"limit": {"rate": 1000}, "trust": [null]}}, "address": {"primary": {"address": "10.100.1.10", "mask": "255.255.255.0"}}, "Cisco-IOS-XE-nat:nat": {"inside": [null]}, "helper-address": [{"address": "10.100.1.50", "vrf": "PRODUCTION"}], "pim": {"Cisco-IOS-XE-multicast:border": [null], "Cisco-IOS-XE-multicast:bfd": [null], "Cisco-IOS-XE-multicast:pim-mode-choice-cfg": {"sparse-mode": {}}, "Cisco-IOS-XE-multicast:dr-priority": 1000}, "proxy-arp": false, "redirects": false, "dhcp": {"Cisco-IOS-XE-dhcp:relay": {"information": {"option": {"vpn-id": [null]}}, "source-interface": "Loopback150"}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST", "direction": "input"}]}, "Cisco-IOS-XE-icmp:unreachables": false, "Cisco-IOS-XE-igmp:igmp": {"version": 3}, "Cisco-IOS-XE-nbar:nbar": {"protocol-discovery": {}}, "Cisco-IOS-XE-ospf:router-ospf": {"ospf": {"process-id": [{"id": 10, "area": [{"area-id": 0}]}], "cost": 10, "dead-interval": 60, "hello-interval": 15, "mtu-ignore": true, "message-digest-key": [{"id": 1, "md5": {"auth-type": 0, "auth-key": "cisco"}}], "network": {"point-to-point": [null]}, "multi-area": {"multi-area-id": [{"area-id": 1}, {"area-id": 2}]}, "priority": 10, "ttl-security": {"hops": 5}}}}, "ipv6": {"address": {"prefix-list": [{"prefix": "2001:db8::1/64", "eui-64": [null]}], "link-local-address": [{"address": "fe80::1", "link-local": [null]}]}, "enable": [null], "mtu": 1600, "nd": {"Cisco-IOS-XE-nd:ra": {"suppress": {"all": [null]}}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST_V6", "direction": "input"}]}, "Cisco-IOS-XE-multicast:pim-conf": {"pim": true}, "Cisco-IOS-XE-multicast:pim-container": {"bfd": [null], "dr-priority": 1000}}, "load-interval": 30, "logging": {"event": {"link-status-enable": true}}, "mtu": 1600, "Cisco-IOS-XE-cdp:cdp": {"enable": true, "tlv": {"default-wrp": {"app": false}, "server-location-config": false, "location-config": false}}, "Cisco-IOS-XE-dot1x:dot1x": {"pae": "authenticator", "max-reauth-req": 3, "max-req": 3, "timeout": {"auth-period": 10000, "held-period": 40000, "quiet-period": 10000, "ratelimit-period": 5000, "server-timeout": 5000, "start-period": 2000, "supp-timeout": 2000, "tx-period": 2000}}, "Cisco-IOS-XE-ethernet:port-settings": {"speed": {"speed-value": "100"}, "auto-negotiation": "disable"}, "Cisco-IOS-XE-ethernet:speed": {"nonegotiate": [null]}, "Cisco-IOS-XE-ospfv3:ospfv3": {"cost-config": {"value": 10}, "network-type": {"point-to-point": [null]}}, "Cisco-IOS-XE-policy:service-policy": {"input": "POLICY_IN", "output": "POLICY_OUT"}, "Cisco-IOS-XE-sanet:authentication": {"periodic": [null], "timer": {"reauthenticate": {"server-config": [null]}}}, "Cisco-IOS-XE-sanet:mab": {"eap": [null]}, "Cisco-IOS-XE-snmp:snmp": {"trap": {"link-status": true}}}"#;
|
|
68
|
-
let path = "$.name";
|
|
69
|
-
let data: Value = serde_json::from_str(data).unwrap();
|
|
70
|
-
let result = JsonPath::parse(path).unwrap().query(&data).all();
|
|
71
|
-
assert_eq!(result.len(), 1);
|
|
72
|
-
assert_eq!(result[0].as_str().unwrap(), "1/0/1");
|
|
73
|
-
}
|
|
74
|
-
}
|