python-jsonpath 0.4.0__py3-none-any.whl → 0.5.0.post1__py3-none-any.whl
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.
- jsonpath/__about__.py +1 -1
- jsonpath/__init__.py +2 -0
- jsonpath/env.py +33 -38
- jsonpath/exceptions.py +4 -0
- jsonpath/filter.py +33 -7
- jsonpath/function_extensions/__init__.py +8 -0
- jsonpath/function_extensions/arguments.py +56 -0
- jsonpath/function_extensions/match.py +56 -0
- jsonpath/function_extensions/search.py +56 -0
- jsonpath/function_extensions/value.py +15 -0
- jsonpath/lex.py +2 -7
- jsonpath/parse.py +81 -11
- jsonpath/path.py +1 -3
- jsonpath/py.typed +0 -0
- jsonpath/selectors.py +17 -1
- python_jsonpath-0.5.0.post1.dist-info/METADATA +113 -0
- python_jsonpath-0.5.0.post1.dist-info/RECORD +24 -0
- python_jsonpath-0.5.0.post1.dist-info/licenses/LICENSE.txt +21 -0
- python_jsonpath-0.4.0.dist-info/METADATA +0 -326
- python_jsonpath-0.4.0.dist-info/RECORD +0 -19
- python_jsonpath-0.4.0.dist-info/licenses/LICENSE.txt +0 -9
- {python_jsonpath-0.4.0.dist-info → python_jsonpath-0.5.0.post1.dist-info}/WHEEL +0 -0
jsonpath/__about__.py
CHANGED
jsonpath/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ from .exceptions import JSONPathError
|
|
|
7
7
|
from .exceptions import JSONPathNameError
|
|
8
8
|
from .exceptions import JSONPathSyntaxError
|
|
9
9
|
from .exceptions import JSONPathTypeError
|
|
10
|
+
from .filter import UNDEFINED
|
|
10
11
|
from .lex import Lexer
|
|
11
12
|
from .match import JSONPathMatch
|
|
12
13
|
from .parse import Parser
|
|
@@ -29,6 +30,7 @@ __all__ = (
|
|
|
29
30
|
"JSONPathTypeError",
|
|
30
31
|
"Lexer",
|
|
31
32
|
"Parser",
|
|
33
|
+
"UNDEFINED",
|
|
32
34
|
)
|
|
33
35
|
|
|
34
36
|
|
jsonpath/env.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Core JSONPath configuration object."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import inspect
|
|
5
4
|
import re
|
|
6
5
|
from collections.abc import Collection
|
|
7
6
|
from operator import getitem
|
|
@@ -22,8 +21,8 @@ from typing import Union
|
|
|
22
21
|
from . import function_extensions
|
|
23
22
|
from .exceptions import JSONPathNameError
|
|
24
23
|
from .exceptions import JSONPathSyntaxError
|
|
25
|
-
from .exceptions import JSONPathTypeError
|
|
26
24
|
from .filter import UNDEFINED
|
|
25
|
+
from .function_extensions import validate
|
|
27
26
|
from .lex import Lexer
|
|
28
27
|
from .parse import Parser
|
|
29
28
|
from .path import CompoundJSONPath
|
|
@@ -69,13 +68,21 @@ class JSONPathEnvironment:
|
|
|
69
68
|
## Class attributes
|
|
70
69
|
|
|
71
70
|
Attributes:
|
|
72
|
-
intersection_token: The pattern used as the intersection operator.
|
|
73
|
-
root_token: The pattern used to select the root node in a JSON document.
|
|
74
|
-
self_token: The pattern used to select the current node in a JSON document.
|
|
75
|
-
union_token: The pattern used as the union operator.
|
|
76
71
|
filter_context_token: The pattern used to select extra filter context data.
|
|
72
|
+
Defaults to `"#"`.
|
|
73
|
+
intersection_token: The pattern used as the intersection operator. Defaults
|
|
74
|
+
to `"$"`.
|
|
77
75
|
lexer_class: The lexer to use when tokenizing path strings.
|
|
76
|
+
max_int_index: The maximum integer allowed when selecting array items by index.
|
|
77
|
+
Defaults to `(2**53) - 1`.
|
|
78
|
+
min_int_index: The minimum integer allowed when selecting array items by index.
|
|
79
|
+
Defaults to `-(2**53) + 1`.
|
|
78
80
|
parser_class: The parser to use when parsing tokens from the lexer.
|
|
81
|
+
root_token: The pattern used to select the root node in a JSON document.
|
|
82
|
+
Defaults to `"$"`.
|
|
83
|
+
self_token: The pattern used to select the current node in a JSON document.
|
|
84
|
+
Defaults to `"@"`
|
|
85
|
+
union_token: The pattern used as the union operator. Defaults to `"|"`.
|
|
79
86
|
"""
|
|
80
87
|
|
|
81
88
|
# These should be unescaped strings. `re.escape` will be called
|
|
@@ -86,6 +93,9 @@ class JSONPathEnvironment:
|
|
|
86
93
|
union_token: str = "|"
|
|
87
94
|
filter_context_token: str = "#"
|
|
88
95
|
|
|
96
|
+
max_int_index: int = (2**53) - 1
|
|
97
|
+
min_int_index: int = -(2**53) + 1
|
|
98
|
+
|
|
89
99
|
# Override these to customize path tokenization and parsing.
|
|
90
100
|
lexer_class: Type[Lexer] = Lexer
|
|
91
101
|
parser_class: Type[Parser] = Parser
|
|
@@ -122,6 +132,13 @@ class JSONPathEnvironment:
|
|
|
122
132
|
if stream.current.kind != TOKEN_EOF:
|
|
123
133
|
_path = CompoundJSONPath(env=self, path=_path)
|
|
124
134
|
while stream.current.kind != TOKEN_EOF:
|
|
135
|
+
if stream.peek.kind == TOKEN_EOF:
|
|
136
|
+
# trailing union or intersection
|
|
137
|
+
raise JSONPathSyntaxError(
|
|
138
|
+
f"expected a path after {stream.current.value!r}",
|
|
139
|
+
token=stream.current,
|
|
140
|
+
)
|
|
141
|
+
|
|
125
142
|
if stream.current.kind == TOKEN_UNION:
|
|
126
143
|
stream.next_token()
|
|
127
144
|
_path.union(
|
|
@@ -230,10 +247,13 @@ class JSONPathEnvironment:
|
|
|
230
247
|
self.function_extensions["keys"] = function_extensions.keys
|
|
231
248
|
self.function_extensions["length"] = function_extensions.length
|
|
232
249
|
self.function_extensions["count"] = function_extensions.length
|
|
250
|
+
self.function_extensions["match"] = function_extensions.Match()
|
|
251
|
+
self.function_extensions["search"] = function_extensions.Search()
|
|
252
|
+
self.function_extensions["value"] = function_extensions.value
|
|
233
253
|
|
|
234
254
|
def validate_function_extension_signature(
|
|
235
255
|
self, token: Token, args: List[Any]
|
|
236
|
-
) ->
|
|
256
|
+
) -> List[Any]:
|
|
237
257
|
"""Compile-time validation of function extension arguments.
|
|
238
258
|
|
|
239
259
|
The IETF JSONPath draft requires us to reject paths that use filter
|
|
@@ -246,36 +266,11 @@ class JSONPathEnvironment:
|
|
|
246
266
|
f"function {token.value!r} is not defined", token=token
|
|
247
267
|
) from err
|
|
248
268
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
f"function {token.value!r} requires keyword arguments",
|
|
255
|
-
token=token,
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
# Too few args?
|
|
259
|
-
positional_args = [
|
|
260
|
-
p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
|
|
261
|
-
]
|
|
262
|
-
if len(args) < len(positional_args):
|
|
263
|
-
raise JSONPathTypeError(
|
|
264
|
-
f"{token.value!r}() requires {len(positional_args)} arguments",
|
|
265
|
-
token=token,
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
# Does the signature have var args?
|
|
269
|
-
if len([p for p in params if p.kind == p.VAR_POSITIONAL]):
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
# Too many args?
|
|
273
|
-
if len(args) > len(positional_args):
|
|
274
|
-
raise JSONPathTypeError(
|
|
275
|
-
f"{token.value!r}() requires at most "
|
|
276
|
-
f"{len(positional_args) + len(positional_args)} arguments",
|
|
277
|
-
token=token,
|
|
278
|
-
)
|
|
269
|
+
if hasattr(func, "validate"):
|
|
270
|
+
args = func.validate(self, args, token)
|
|
271
|
+
assert isinstance(args, list)
|
|
272
|
+
return args
|
|
273
|
+
return validate(self, func, args, token)
|
|
279
274
|
|
|
280
275
|
def getitem(self, obj: Any, key: Any) -> Any:
|
|
281
276
|
"""Sequence and mapping item getter used throughout JSONPath resolution.
|
|
@@ -353,7 +348,7 @@ class JSONPathEnvironment:
|
|
|
353
348
|
return operator == "<="
|
|
354
349
|
|
|
355
350
|
if operator == "=~" and isinstance(right, re.Pattern) and isinstance(left, str):
|
|
356
|
-
return bool(right.
|
|
351
|
+
return bool(right.fullmatch(left))
|
|
357
352
|
|
|
358
353
|
if isinstance(left, str) and isinstance(right, str):
|
|
359
354
|
if operator == "<=":
|
jsonpath/exceptions.py
CHANGED
|
@@ -45,6 +45,10 @@ class JSONPathTypeError(JSONPathError):
|
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
class JSONPathIndexError(JSONPathError):
|
|
49
|
+
"""An exception raised when an array index is out of range."""
|
|
50
|
+
|
|
51
|
+
|
|
48
52
|
class JSONPathNameError(JSONPathError):
|
|
49
53
|
"""An exception raised when an unknown function extension is called."""
|
|
50
54
|
|
jsonpath/filter.py
CHANGED
|
@@ -181,6 +181,24 @@ class RegexLiteral(Literal[Pattern[str]]):
|
|
|
181
181
|
return f"/{pattern}/{''.join(flags)}"
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
class RegexArgument(FilterExpression):
|
|
185
|
+
"""A compiled regex."""
|
|
186
|
+
|
|
187
|
+
__slots__ = ("pattern",)
|
|
188
|
+
|
|
189
|
+
def __init__(self, pattern: Pattern[str]) -> None:
|
|
190
|
+
self.pattern = pattern
|
|
191
|
+
|
|
192
|
+
def __str__(self) -> str:
|
|
193
|
+
return repr(self.pattern.pattern)
|
|
194
|
+
|
|
195
|
+
def evaluate(self, _: FilterContext) -> object:
|
|
196
|
+
return self.pattern
|
|
197
|
+
|
|
198
|
+
async def evaluate_async(self, _: FilterContext) -> object:
|
|
199
|
+
return self.pattern
|
|
200
|
+
|
|
201
|
+
|
|
184
202
|
class ListLiteral(FilterExpression):
|
|
185
203
|
"""A list literal."""
|
|
186
204
|
|
|
@@ -313,15 +331,19 @@ class SelfPath(Path):
|
|
|
313
331
|
def __str__(self) -> str:
|
|
314
332
|
return "@" + str(self.path)[1:]
|
|
315
333
|
|
|
316
|
-
def evaluate(self, context: FilterContext) -> object:
|
|
334
|
+
def evaluate(self, context: FilterContext) -> object: # noqa: PLR0911
|
|
335
|
+
if isinstance(context.current, str):
|
|
336
|
+
if self.path.empty():
|
|
337
|
+
return context.current
|
|
338
|
+
return UNDEFINED
|
|
317
339
|
if not isinstance(context.current, (Sequence, Mapping)):
|
|
318
340
|
if self.path.empty():
|
|
319
|
-
return
|
|
320
|
-
return
|
|
341
|
+
return context.current
|
|
342
|
+
return UNDEFINED
|
|
321
343
|
|
|
322
344
|
try:
|
|
323
345
|
matches = self.path.findall(context.current)
|
|
324
|
-
except json.JSONDecodeError:
|
|
346
|
+
except json.JSONDecodeError: # this should never happen
|
|
325
347
|
return UNDEFINED
|
|
326
348
|
|
|
327
349
|
if not matches:
|
|
@@ -330,11 +352,15 @@ class SelfPath(Path):
|
|
|
330
352
|
return matches[0]
|
|
331
353
|
return matches
|
|
332
354
|
|
|
333
|
-
async def evaluate_async(self, context: FilterContext) -> object:
|
|
355
|
+
async def evaluate_async(self, context: FilterContext) -> object: # noqa: PLR0911
|
|
356
|
+
if isinstance(context.current, str):
|
|
357
|
+
if self.path.empty():
|
|
358
|
+
return context.current
|
|
359
|
+
return UNDEFINED
|
|
334
360
|
if not isinstance(context.current, (Sequence, Mapping)):
|
|
335
361
|
if self.path.empty():
|
|
336
|
-
return
|
|
337
|
-
return
|
|
362
|
+
return context.current
|
|
363
|
+
return UNDEFINED
|
|
338
364
|
|
|
339
365
|
try:
|
|
340
366
|
matches = await self.path.findall_async(context.current)
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# noqa: D104
|
|
2
|
+
from .arguments import validate
|
|
2
3
|
from .keys import keys
|
|
3
4
|
from .length import length
|
|
5
|
+
from .match import Match
|
|
6
|
+
from .search import Search
|
|
7
|
+
from .value import value
|
|
4
8
|
|
|
5
9
|
__all__ = (
|
|
10
|
+
"Match",
|
|
11
|
+
"Search",
|
|
12
|
+
"value",
|
|
6
13
|
"keys",
|
|
7
14
|
"length",
|
|
15
|
+
"validate",
|
|
8
16
|
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Class-based function extension base."""
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..env import JSONPathEnvironment
|
|
10
|
+
from ..token import Token
|
|
11
|
+
|
|
12
|
+
from ..exceptions import JSONPathTypeError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate(
|
|
16
|
+
_: "JSONPathEnvironment",
|
|
17
|
+
func: Callable[..., Any],
|
|
18
|
+
args: List[Any],
|
|
19
|
+
token: "Token",
|
|
20
|
+
) -> List[Any]:
|
|
21
|
+
"""Generic validation of function extension arguments using introspection.
|
|
22
|
+
|
|
23
|
+
The IETF JSONPath draft requires us to reject paths that use filter
|
|
24
|
+
functions with too many or too few arguments.
|
|
25
|
+
"""
|
|
26
|
+
params = list(inspect.signature(func).parameters.values())
|
|
27
|
+
|
|
28
|
+
# Keyword only params are not supported
|
|
29
|
+
if len([p for p in params if p.kind in (p.KEYWORD_ONLY, p.VAR_KEYWORD)]):
|
|
30
|
+
raise JSONPathTypeError(
|
|
31
|
+
f"function {token.value!r} requires keyword arguments",
|
|
32
|
+
token=token,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Too few args?
|
|
36
|
+
positional_args = [
|
|
37
|
+
p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
|
|
38
|
+
]
|
|
39
|
+
if len(args) < len(positional_args):
|
|
40
|
+
raise JSONPathTypeError(
|
|
41
|
+
f"{token.value!r}() requires {len(positional_args)} arguments",
|
|
42
|
+
token=token,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Does the signature have var args?
|
|
46
|
+
has_var_args = bool([p for p in params if p.kind == p.VAR_POSITIONAL])
|
|
47
|
+
|
|
48
|
+
# Too many args?
|
|
49
|
+
if not has_var_args and len(args) > len(positional_args):
|
|
50
|
+
raise JSONPathTypeError(
|
|
51
|
+
f"{token.value!r}() requires at most "
|
|
52
|
+
f"{len(positional_args) + len(positional_args)} arguments",
|
|
53
|
+
token=token,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return args
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The standard `match` function extension."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Pattern
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
from ..exceptions import JSONPathTypeError
|
|
10
|
+
from ..filter import RegexArgument
|
|
11
|
+
from ..filter import StringLiteral
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..env import JSONPathEnvironment
|
|
15
|
+
from ..token import Token
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Match:
|
|
19
|
+
"""The built-in `match` function.
|
|
20
|
+
|
|
21
|
+
This implementation uses the standard _re_ module, without attempting to map
|
|
22
|
+
I-Regexps to Python regex.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __call__(self, string: str, pattern: Union[str, Pattern[str], None]) -> bool:
|
|
26
|
+
"""Return `True` if _pattern_ matches the given string, `False` otherwise."""
|
|
27
|
+
# The IETF JSONPath draft requires us to return `False` if the pattern was
|
|
28
|
+
# invalid. We use `None` to indicate the pattern could not be compiled.
|
|
29
|
+
if string is None or pattern is None:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
return bool(re.fullmatch(pattern, string))
|
|
34
|
+
except (TypeError, re.error):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def validate(
|
|
38
|
+
self,
|
|
39
|
+
_: "JSONPathEnvironment",
|
|
40
|
+
args: List[object],
|
|
41
|
+
token: "Token",
|
|
42
|
+
) -> List[object]:
|
|
43
|
+
"""Function argument validation."""
|
|
44
|
+
if len(args) != 2: # noqa: PLR2004
|
|
45
|
+
raise JSONPathTypeError(
|
|
46
|
+
f"{token.value!r} requires 2 arguments, found {len(args)}",
|
|
47
|
+
token=token,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if isinstance(args[1], StringLiteral):
|
|
51
|
+
try:
|
|
52
|
+
return [args[0], RegexArgument(re.compile(args[1].value))]
|
|
53
|
+
except re.error:
|
|
54
|
+
return [None, None]
|
|
55
|
+
|
|
56
|
+
return args
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The standard `search` function extension."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Pattern
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
from ..exceptions import JSONPathTypeError
|
|
10
|
+
from ..filter import RegexArgument
|
|
11
|
+
from ..filter import StringLiteral
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..env import JSONPathEnvironment
|
|
15
|
+
from ..token import Token
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Search:
|
|
19
|
+
"""The built-in `search` function.
|
|
20
|
+
|
|
21
|
+
This implementation uses the standard _re_ module, without attempting to map
|
|
22
|
+
I-Regexps to Python regex.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __call__(self, string: str, pattern: Union[str, Pattern[str], None]) -> bool:
|
|
26
|
+
"""Return `True` if the given string contains _pattern_, `False` otherwise."""
|
|
27
|
+
# The IETF JSONPath draft requires us to return `False` if the pattern was
|
|
28
|
+
# invalid. We use `None` to indicate the pattern could not be compiled.
|
|
29
|
+
if string is None or pattern is None:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
return bool(re.search(pattern, string))
|
|
34
|
+
except (TypeError, re.error):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def validate(
|
|
38
|
+
self,
|
|
39
|
+
_: "JSONPathEnvironment",
|
|
40
|
+
args: List[object],
|
|
41
|
+
token: "Token",
|
|
42
|
+
) -> List[object]:
|
|
43
|
+
"""Function argument validation."""
|
|
44
|
+
if len(args) != 2: # noqa: PLR2004
|
|
45
|
+
raise JSONPathTypeError(
|
|
46
|
+
f"{token.value!r} requires 2 arguments, found {len(args)}",
|
|
47
|
+
token=token,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if isinstance(args[1], StringLiteral):
|
|
51
|
+
try:
|
|
52
|
+
return [args[0], RegexArgument(re.compile(args[1].value))]
|
|
53
|
+
except re.error:
|
|
54
|
+
return [None, None]
|
|
55
|
+
|
|
56
|
+
return args
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""The standard `value` function extension."""
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
|
|
4
|
+
from ..filter import UNDEFINED
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def value(obj: object) -> object:
|
|
8
|
+
"""Return the first object in the sequence if the sequence has only one item."""
|
|
9
|
+
if isinstance(obj, str):
|
|
10
|
+
return obj
|
|
11
|
+
if isinstance(obj, Sequence):
|
|
12
|
+
if len(obj) == 1:
|
|
13
|
+
return obj[0]
|
|
14
|
+
return UNDEFINED
|
|
15
|
+
return obj
|
jsonpath/lex.py
CHANGED
|
@@ -72,11 +72,11 @@ if TYPE_CHECKING:
|
|
|
72
72
|
class Lexer:
|
|
73
73
|
"""Tokenize a JSONPath string."""
|
|
74
74
|
|
|
75
|
+
key_pattern = r"[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*"
|
|
76
|
+
|
|
75
77
|
def __init__(self, *, env: JSONPathEnvironment) -> None:
|
|
76
78
|
self.env = env
|
|
77
79
|
|
|
78
|
-
self.key_pattern = r"[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*"
|
|
79
|
-
|
|
80
80
|
self.double_quote_pattern = r'"(?P<G_DQUOTE>(?:(?!(?<!\\)").)*)"'
|
|
81
81
|
self.single_quote_pattern = r"'(?P<G_SQUOTE>(?:(?!(?<!\\)').)*)'"
|
|
82
82
|
|
|
@@ -86,10 +86,6 @@ class Lexer:
|
|
|
86
86
|
# [thing]
|
|
87
87
|
self.bracketed_property_pattern = rf"\[\s*(?P<G_BPROP>{self.key_pattern})\s*]"
|
|
88
88
|
|
|
89
|
-
# .1
|
|
90
|
-
# NOTE: `.1` can be a dot property where the key is "1".
|
|
91
|
-
self.dot_index_pattern = r"\.\s*(?P<G_DINDEX>\d+)\b"
|
|
92
|
-
|
|
93
89
|
# [1] or [-1]
|
|
94
90
|
self.index_pattern = r"\[\s*(?P<G_INDEX>\-?\s*\d+)\s*]"
|
|
95
91
|
|
|
@@ -132,7 +128,6 @@ class Lexer:
|
|
|
132
128
|
(TOKEN_DOUBLE_QUOTE_STRING, self.double_quote_pattern),
|
|
133
129
|
(TOKEN_SINGLE_QUOTE_STRING, self.single_quote_pattern),
|
|
134
130
|
(TOKEN_RE_PATTERN, self.re_pattern),
|
|
135
|
-
(TOKEN_DOT_INDEX, self.dot_index_pattern),
|
|
136
131
|
(TOKEN_INDEX, self.index_pattern),
|
|
137
132
|
(TOKEN_SLICE, self.slice_pattern),
|
|
138
133
|
(TOKEN_WILD, self.wild_pattern),
|
jsonpath/parse.py
CHANGED
|
@@ -92,6 +92,41 @@ if TYPE_CHECKING:
|
|
|
92
92
|
|
|
93
93
|
# ruff: noqa: D102
|
|
94
94
|
|
|
95
|
+
INVALID_NAME_SELECTOR_CHARS = [
|
|
96
|
+
"\x00",
|
|
97
|
+
"\x01",
|
|
98
|
+
"\x02",
|
|
99
|
+
"\x03",
|
|
100
|
+
"\x04",
|
|
101
|
+
"\x05",
|
|
102
|
+
"\x06",
|
|
103
|
+
"\x07",
|
|
104
|
+
"\x08",
|
|
105
|
+
"\t",
|
|
106
|
+
"\n",
|
|
107
|
+
"\x0b",
|
|
108
|
+
"\x0c",
|
|
109
|
+
"\r",
|
|
110
|
+
"\x0e",
|
|
111
|
+
"\x0f",
|
|
112
|
+
"\x10",
|
|
113
|
+
"\x11",
|
|
114
|
+
"\x12",
|
|
115
|
+
"\x13",
|
|
116
|
+
"\x14",
|
|
117
|
+
"\x15",
|
|
118
|
+
"\x16",
|
|
119
|
+
"\x17",
|
|
120
|
+
"\x18",
|
|
121
|
+
"\x19",
|
|
122
|
+
"\x1a",
|
|
123
|
+
"\x1b",
|
|
124
|
+
"\x1c",
|
|
125
|
+
"\x1d",
|
|
126
|
+
"\x1e",
|
|
127
|
+
"\x1f",
|
|
128
|
+
]
|
|
129
|
+
|
|
95
130
|
|
|
96
131
|
class Parser:
|
|
97
132
|
"""A JSONPath parser bound to a JSONPathEnvironment."""
|
|
@@ -149,6 +184,11 @@ class Parser:
|
|
|
149
184
|
"s": re.S,
|
|
150
185
|
}
|
|
151
186
|
|
|
187
|
+
_INVALID_NAME_SELECTOR_CHARS = f"[{''.join(INVALID_NAME_SELECTOR_CHARS)}]"
|
|
188
|
+
RE_INVALID_NAME_SELECTOR = re.compile(
|
|
189
|
+
rf'(?:(?!(?<!\\)"){_INVALID_NAME_SELECTOR_CHARS})'
|
|
190
|
+
)
|
|
191
|
+
|
|
152
192
|
def __init__(self, *, env: JSONPathEnvironment) -> None:
|
|
153
193
|
self.env = env
|
|
154
194
|
|
|
@@ -245,7 +285,7 @@ class Parser:
|
|
|
245
285
|
)
|
|
246
286
|
elif stream.current.kind == TOKEN_LIST_START:
|
|
247
287
|
yield self.parse_selector_list(stream)
|
|
248
|
-
elif stream.current.kind == TOKEN_FILTER_START
|
|
288
|
+
elif stream.current.kind == TOKEN_FILTER_START:
|
|
249
289
|
yield self.parse_filter(stream)
|
|
250
290
|
else:
|
|
251
291
|
if in_filter:
|
|
@@ -288,7 +328,9 @@ class Parser:
|
|
|
288
328
|
def parse_selector_list(self, stream: TokenStream) -> ListSelector:
|
|
289
329
|
"""Parse a comma separated list JSONPath selectors from a stream of tokens."""
|
|
290
330
|
tok = stream.next_token()
|
|
291
|
-
list_items: List[
|
|
331
|
+
list_items: List[
|
|
332
|
+
Union[IndexSelector, PropertySelector, SliceSelector, WildSelector]
|
|
333
|
+
] = []
|
|
292
334
|
|
|
293
335
|
while stream.current.kind != TOKEN_RBRACKET:
|
|
294
336
|
if stream.current.kind == TOKEN_INT:
|
|
@@ -299,32 +341,56 @@ class Parser:
|
|
|
299
341
|
index=int(stream.current.value),
|
|
300
342
|
)
|
|
301
343
|
)
|
|
302
|
-
elif stream.current.kind
|
|
344
|
+
elif stream.current.kind == TOKEN_BARE_PROPERTY:
|
|
345
|
+
list_items.append(
|
|
346
|
+
PropertySelector(
|
|
347
|
+
env=self.env,
|
|
348
|
+
token=stream.current,
|
|
349
|
+
name=stream.current.value,
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
elif stream.current.kind == TOKEN_STRING:
|
|
353
|
+
if self.RE_INVALID_NAME_SELECTOR.search(stream.current.value):
|
|
354
|
+
raise JSONPathSyntaxError(
|
|
355
|
+
f"invalid name selector {stream.current.value!r}",
|
|
356
|
+
token=stream.current,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
name = (
|
|
360
|
+
codecs.decode(
|
|
361
|
+
stream.current.value.replace("\\/", "/"), "unicode-escape"
|
|
362
|
+
)
|
|
363
|
+
.encode("utf-16", "surrogatepass")
|
|
364
|
+
.decode("utf-16")
|
|
365
|
+
)
|
|
366
|
+
|
|
303
367
|
list_items.append(
|
|
304
368
|
PropertySelector(
|
|
305
369
|
env=self.env,
|
|
306
370
|
token=stream.current,
|
|
307
|
-
name=
|
|
308
|
-
stream.current.value.replace("\\/", "/"), "unicode-escape"
|
|
309
|
-
)
|
|
310
|
-
.encode("utf-16", "surrogatepass")
|
|
311
|
-
.decode("utf-16"),
|
|
371
|
+
name=name,
|
|
312
372
|
),
|
|
313
373
|
)
|
|
314
374
|
elif stream.current.kind == TOKEN_SLICE_START:
|
|
315
375
|
list_items.append(self.parse_slice(stream))
|
|
316
|
-
elif stream.current.kind ==
|
|
376
|
+
elif stream.current.kind == TOKEN_WILD:
|
|
377
|
+
list_items.append(WildSelector(env=self.env, token=stream.current))
|
|
378
|
+
|
|
379
|
+
if stream.peek.kind == TOKEN_EOF:
|
|
317
380
|
raise JSONPathSyntaxError(
|
|
318
381
|
"unexpected end of selector list",
|
|
319
382
|
token=stream.current,
|
|
320
383
|
)
|
|
321
384
|
|
|
322
385
|
if stream.peek.kind != TOKEN_RBRACKET:
|
|
386
|
+
stream.expect_peek(TOKEN_COMMA)
|
|
323
387
|
stream.next_token()
|
|
324
388
|
|
|
325
389
|
stream.next_token()
|
|
326
390
|
|
|
327
|
-
|
|
391
|
+
if not list_items:
|
|
392
|
+
raise JSONPathSyntaxError("empty segment", token=tok)
|
|
393
|
+
|
|
328
394
|
return ListSelector(env=self.env, token=tok, items=list_items)
|
|
329
395
|
|
|
330
396
|
def parse_filter(self, stream: TokenStream) -> Filter:
|
|
@@ -463,12 +529,16 @@ class Parser:
|
|
|
463
529
|
) from err
|
|
464
530
|
|
|
465
531
|
if stream.peek.kind != TOKEN_RPAREN:
|
|
532
|
+
if stream.peek.kind == TOKEN_FILTER_END:
|
|
533
|
+
break
|
|
466
534
|
stream.expect_peek(TOKEN_COMMA)
|
|
467
535
|
stream.next_token()
|
|
468
536
|
|
|
469
537
|
stream.next_token()
|
|
470
538
|
|
|
471
|
-
self.env.validate_function_extension_signature(
|
|
539
|
+
function_arguments = self.env.validate_function_extension_signature(
|
|
540
|
+
tok, function_arguments
|
|
541
|
+
)
|
|
472
542
|
return FunctionExtension(tok.value, function_arguments)
|
|
473
543
|
|
|
474
544
|
def parse_filter_selector(
|
jsonpath/path.py
CHANGED
|
@@ -76,7 +76,6 @@ class JSONPath:
|
|
|
76
76
|
an incompatible way.
|
|
77
77
|
"""
|
|
78
78
|
if isinstance(data, str):
|
|
79
|
-
# TODO: catch JSONDecodeError?
|
|
80
79
|
_data = json.loads(data)
|
|
81
80
|
elif isinstance(data, TextIO):
|
|
82
81
|
_data = json.loads(data.read())
|
|
@@ -141,7 +140,6 @@ class JSONPath:
|
|
|
141
140
|
) -> List[object]:
|
|
142
141
|
"""An async version of `findall()`."""
|
|
143
142
|
if isinstance(data, str):
|
|
144
|
-
# TODO: catch JSONDecodeError
|
|
145
143
|
_data = json.loads(data)
|
|
146
144
|
elif isinstance(data, TextIO):
|
|
147
145
|
_data = json.loads(data.read())
|
|
@@ -187,7 +185,7 @@ class JSONPath:
|
|
|
187
185
|
|
|
188
186
|
def empty(self) -> bool:
|
|
189
187
|
"""Return `True` if this path has no selectors."""
|
|
190
|
-
return bool(self._selectors)
|
|
188
|
+
return not bool(self._selectors)
|
|
191
189
|
|
|
192
190
|
|
|
193
191
|
class CompoundJSONPath:
|
jsonpath/py.typed
ADDED
|
File without changes
|
jsonpath/selectors.py
CHANGED
|
@@ -15,6 +15,7 @@ from typing import Optional
|
|
|
15
15
|
from typing import TypeVar
|
|
16
16
|
from typing import Union
|
|
17
17
|
|
|
18
|
+
from .exceptions import JSONPathIndexError
|
|
18
19
|
from .exceptions import JSONPathTypeError
|
|
19
20
|
from .match import JSONPathMatch
|
|
20
21
|
|
|
@@ -113,6 +114,9 @@ class IndexSelector(JSONPathSelector):
|
|
|
113
114
|
token: Token,
|
|
114
115
|
index: int,
|
|
115
116
|
) -> None:
|
|
117
|
+
if index < env.min_int_index or index > env.max_int_index:
|
|
118
|
+
raise JSONPathIndexError("index out of range", token=token)
|
|
119
|
+
|
|
116
120
|
super().__init__(env=env, token=token)
|
|
117
121
|
self.index = index
|
|
118
122
|
self._as_key = str(self.index)
|
|
@@ -201,6 +205,7 @@ class SliceSelector(JSONPathSelector):
|
|
|
201
205
|
step: Optional[int] = None,
|
|
202
206
|
) -> None:
|
|
203
207
|
super().__init__(env=env, token=token)
|
|
208
|
+
self._check_range(start, stop, step)
|
|
204
209
|
self.slice = slice(start, stop, step)
|
|
205
210
|
|
|
206
211
|
def __str__(self) -> str:
|
|
@@ -209,6 +214,13 @@ class SliceSelector(JSONPathSelector):
|
|
|
209
214
|
step = self.slice.step if self.slice.step is not None else "1"
|
|
210
215
|
return f"[{start}:{stop}:{step}]"
|
|
211
216
|
|
|
217
|
+
def _check_range(self, *indices: Optional[int]) -> None:
|
|
218
|
+
for i in indices:
|
|
219
|
+
if i is not None and (
|
|
220
|
+
i < self.env.min_int_index or i > self.env.max_int_index
|
|
221
|
+
):
|
|
222
|
+
raise JSONPathIndexError("index out of range")
|
|
223
|
+
|
|
212
224
|
def _normalized_index(self, obj: Sequence[object], index: int) -> int:
|
|
213
225
|
if index < 0 and len(obj) >= abs(index):
|
|
214
226
|
return len(obj) + index
|
|
@@ -396,7 +408,9 @@ class ListSelector(JSONPathSelector):
|
|
|
396
408
|
*,
|
|
397
409
|
env: JSONPathEnvironment,
|
|
398
410
|
token: Token,
|
|
399
|
-
items: List[
|
|
411
|
+
items: List[
|
|
412
|
+
Union[SliceSelector, IndexSelector, PropertySelector, WildSelector]
|
|
413
|
+
],
|
|
400
414
|
) -> None:
|
|
401
415
|
super().__init__(env=env, token=token)
|
|
402
416
|
self.items = items
|
|
@@ -411,6 +425,8 @@ class ListSelector(JSONPathSelector):
|
|
|
411
425
|
buf.append(f"{start}:{stop}:{step}")
|
|
412
426
|
elif isinstance(item, PropertySelector):
|
|
413
427
|
buf.append(f"'{item.name}'")
|
|
428
|
+
elif isinstance(item, WildSelector):
|
|
429
|
+
buf.append("*")
|
|
414
430
|
else:
|
|
415
431
|
buf.append(str(item.index))
|
|
416
432
|
return f"[{', '.join(buf)}]"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-jsonpath
|
|
3
|
+
Version: 0.5.0.post1
|
|
4
|
+
Summary: Another JSONPath implementation for Python.
|
|
5
|
+
Project-URL: Documentation, https://jg-rp.github.io/python-jsonpath/
|
|
6
|
+
Project-URL: Issues, https://github.com/jg-rp/python-jsonpath/issues
|
|
7
|
+
Project-URL: Source, https://github.com/jg-rp/python-jsonpath
|
|
8
|
+
Author-email: James Prior <jamesgr.prior@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
|
+
Requires-Python: >=3.7
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Python JSONPath
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/python-jsonpath)
|
|
28
|
+
[](https://github.com/jg-rp/python-jsonpath/actions)
|
|
29
|
+
[](https://github.com/jg-rp/python-jsonpath/blob/main/LICENSE.txt)
|
|
30
|
+
[](https://pypi.org/project/python-jsonpath)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
A flexible JSONPath engine for Python.
|
|
35
|
+
|
|
36
|
+
**Table of Contents**
|
|
37
|
+
|
|
38
|
+
- [Install](#install)
|
|
39
|
+
- [Links](#links)
|
|
40
|
+
- [Example](#example)
|
|
41
|
+
- [License](#license)
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
Install Python JSONPath using [Pipenv](https://pipenv.pypa.io/en/latest/):
|
|
46
|
+
|
|
47
|
+
```console
|
|
48
|
+
pipenv install -u python-jsonpath
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
or [pip](https://pip.pypa.io/en/stable/getting-started/):
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
pip install python-jsonpath
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Links
|
|
58
|
+
|
|
59
|
+
- Documentation: https://jg-rp.github.io/python-jsonpath/.
|
|
60
|
+
- JSONPath Syntax: https://jg-rp.github.io/python-jsonpath/syntax/
|
|
61
|
+
- Change log: https://github.com/jg-rp/python-jsonpath/blob/main/CHANGELOG.md
|
|
62
|
+
- PyPi: https://pypi.org/project/python-jsonpath
|
|
63
|
+
- Source code: https://github.com/jg-rp/python-jsonpath
|
|
64
|
+
- Issue tracker: https://github.com/jg-rp/python-jsonpath/issues
|
|
65
|
+
|
|
66
|
+
## Example
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import jsonpath
|
|
70
|
+
|
|
71
|
+
data = {
|
|
72
|
+
"categories": [
|
|
73
|
+
{
|
|
74
|
+
"name": "footwear",
|
|
75
|
+
"products": [
|
|
76
|
+
{
|
|
77
|
+
"title": "Trainers",
|
|
78
|
+
"description": "Fashionable trainers.",
|
|
79
|
+
"price": 89.99,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"title": "Barefoot Trainers",
|
|
83
|
+
"description": "Running trainers.",
|
|
84
|
+
"price": 130.00,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "headwear",
|
|
90
|
+
"products": [
|
|
91
|
+
{
|
|
92
|
+
"title": "Cap",
|
|
93
|
+
"description": "Baseball cap",
|
|
94
|
+
"price": 15.00,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"title": "Beanie",
|
|
98
|
+
"description": "Winter running hat.",
|
|
99
|
+
"price": 9.00,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
"price_cap": 10,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
products = jsonpath.findall("$..products.*", data)
|
|
108
|
+
print(products)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
`python-jsonpath` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
jsonpath/__about__.py,sha256=hZldckvbIhXFrYcYa_nKfZAxjOcYFuOGy9lZRk4iF8I,139
|
|
2
|
+
jsonpath/__init__.py,sha256=gUgI3mFEPMOAtgh_Pfx6iq-Vq1SIsybOsC_Lixp7qZo,1070
|
|
3
|
+
jsonpath/env.py,sha256=E3LDUjdj-XXFw5koMkaVBhgwWNFRFDjxuUwUL08U6mM,14434
|
|
4
|
+
jsonpath/exceptions.py,sha256=A6lCV7Mtm5ZorJX2cr8qXXWXBqLyX4hRzs6y5rzc4zs,1906
|
|
5
|
+
jsonpath/filter.py,sha256=cVWygNCUGNVmDUDrVJCb8qCz4su_tjD45r0qmziW1hY,12325
|
|
6
|
+
jsonpath/lex.py,sha256=OXz8Az0gMoWQ7XRK1wzGlZ5tbWydfplfQnOI35_4VyY,10881
|
|
7
|
+
jsonpath/match.py,sha256=lJn27VWgTMUeAEO4FtmAmUPvf9FK7hTF8f2yq38KiOk,2513
|
|
8
|
+
jsonpath/parse.py,sha256=MXNQ-TyDLceBP5dWZkQolXpmRDbu8OG2SCGCdJuMmxM,19148
|
|
9
|
+
jsonpath/path.py,sha256=Jp-NQp7bnBWBDqkCrunwWOk8Ujiu2atb9Tc6wTznDjY,11290
|
|
10
|
+
jsonpath/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
jsonpath/selectors.py,sha256=6TIrYpMSvH8NDqfSIo1-4KezS29OFmYMdBdoGdvkpfM,22016
|
|
12
|
+
jsonpath/stream.py,sha256=V9OtMAYl6jdsj2G8e3u-vH7L4iFhA-vm4Z2Aom10Dxk,2567
|
|
13
|
+
jsonpath/token.py,sha256=BS01OCzriOGn85NkyGoC93h_96BQn-GXeeeM2k_sDPc,3650
|
|
14
|
+
jsonpath/function_extensions/__init__.py,sha256=HFtDFqurbIly31Z5R5p5CmpP_su5MOHBo9jGBjrNwB4,269
|
|
15
|
+
jsonpath/function_extensions/arguments.py,sha256=buFAlfRZtH-yPT_ENJetZlehHMiGkZF0Ew_xw_4WRyc,1694
|
|
16
|
+
jsonpath/function_extensions/keys.py,sha256=vNdIV8xqr1LMG6RLJwb6iA5-VXgYSZu6CqprXEs6rYM,364
|
|
17
|
+
jsonpath/function_extensions/length.py,sha256=QdwkLLNTbgNKWIx1M8umAHnc5vyc9cjM_vODc6hXDK0,313
|
|
18
|
+
jsonpath/function_extensions/match.py,sha256=yyv9LMItJBs_20MTPRq7SKn1RrUrXcJtT9oX-WAqS4g,1697
|
|
19
|
+
jsonpath/function_extensions/search.py,sha256=WaX2PDa7Zp6deh5H88r1oSoWbNHnnFMwGd0wygIOyms,1698
|
|
20
|
+
jsonpath/function_extensions/value.py,sha256=7qBf02eqwj6HJp_Gaaw_8UPoH_cAouBfzBEmmb6joRk,402
|
|
21
|
+
python_jsonpath-0.5.0.post1.dist-info/METADATA,sha256=gxF3x6alLfirs4vi1kfZ_RB2Znaqmawq4fKhaDLdWFs,3653
|
|
22
|
+
python_jsonpath-0.5.0.post1.dist-info/WHEEL,sha256=Fd6mP6ydyRguakwUJ05oBE7fh2IPxgtDN9IwHJ9OqJQ,87
|
|
23
|
+
python_jsonpath-0.5.0.post1.dist-info/licenses/LICENSE.txt,sha256=u7PksAQGI1QYWcERHeauMseZ4XAzDKUrKW8Z4wbeU1k,1101
|
|
24
|
+
python_jsonpath-0.5.0.post1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-present James Prior <jamesgr.prior@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: python-jsonpath
|
|
3
|
-
Version: 0.4.0
|
|
4
|
-
Summary: Another JSONPath implementation for Python.
|
|
5
|
-
Project-URL: Documentation, https://jg-rp.github.io/python-jsonpath/
|
|
6
|
-
Project-URL: Issues, https://github.com/jg-rp/python-jsonpath/issues
|
|
7
|
-
Project-URL: Source, https://github.com/jg-rp/python-jsonpath
|
|
8
|
-
Author-email: James Prior <jamesgr.prior@gmail.com>
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE.txt
|
|
11
|
-
Classifier: Development Status :: 4 - Beta
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Classifier: Programming Language :: Python
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
-
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
|
-
Requires-Python: >=3.7
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
|
|
25
|
-
# Python JSONPath
|
|
26
|
-
|
|
27
|
-
[](https://pypi.org/project/python-jsonpath)
|
|
28
|
-
[](https://github.com/jg-rp/python-jsonpath/actions)
|
|
29
|
-
[](https://github.com/jg-rp/python-jsonpath/blob/main/LICENSE.txt)
|
|
30
|
-
[](https://pypi.org/project/python-jsonpath)
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
**Table of Contents**
|
|
35
|
-
|
|
36
|
-
- [Install](#install)
|
|
37
|
-
- [API](#api)
|
|
38
|
-
- [Syntax](#syntax)
|
|
39
|
-
- [License](#license)
|
|
40
|
-
|
|
41
|
-
A flexible JSONPath engine for Python.
|
|
42
|
-
|
|
43
|
-
JSONPath is a mini language for extracting objects from data formatted in JavaScript Object Notation, or equivalent Python objects, like dictionaries and lists.
|
|
44
|
-
|
|
45
|
-
```python
|
|
46
|
-
import jsonpath
|
|
47
|
-
|
|
48
|
-
data = {
|
|
49
|
-
"categories": [
|
|
50
|
-
{
|
|
51
|
-
"name": "footwear",
|
|
52
|
-
"products": [
|
|
53
|
-
{
|
|
54
|
-
"title": "Trainers",
|
|
55
|
-
"description": "Fashionable trainers.",
|
|
56
|
-
"price": 89.99,
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"title": "Barefoot Trainers",
|
|
60
|
-
"description": "Running trainers.",
|
|
61
|
-
"price": 130.00,
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"name": "headwear",
|
|
67
|
-
"products": [
|
|
68
|
-
{
|
|
69
|
-
"title": "Cap",
|
|
70
|
-
"description": "Baseball cap",
|
|
71
|
-
"price": 15.00,
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
"title": "Beanie",
|
|
75
|
-
"description": "Winter running hat.",
|
|
76
|
-
"price": 9.00,
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
"price_cap": 10,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
products = jsonpath.findall("$..products.*", data)
|
|
85
|
-
print(products)
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Install
|
|
89
|
-
|
|
90
|
-
Install Python JSONPath using [Pipenv](https://pipenv.pypa.io/en/latest/):
|
|
91
|
-
|
|
92
|
-
```console
|
|
93
|
-
pipenv install -u python-jsonpath
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
or [pip](https://pip.pypa.io/en/stable/getting-started/):
|
|
97
|
-
|
|
98
|
-
```console
|
|
99
|
-
pip install python-jsonpath
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
or [pipx](https://pypa.github.io/pipx/)
|
|
103
|
-
|
|
104
|
-
```console
|
|
105
|
-
pipx install python-jsonpath
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## API
|
|
109
|
-
|
|
110
|
-
### jsonpath.findall
|
|
111
|
-
|
|
112
|
-
`findall(path: str, data: Sequence | Mapping) -> list[object]`
|
|
113
|
-
|
|
114
|
-
Find all objects in `data` matching the given JSONPath `path`. If data is a string, it will be loaded using `json.loads()` and the default `JSONDecoder`.
|
|
115
|
-
|
|
116
|
-
Returns a list of matched objects, or an empty list if there were no matches.
|
|
117
|
-
|
|
118
|
-
### jsonpath.finditer
|
|
119
|
-
|
|
120
|
-
`finditer(path: str, data: Sequence | Mapping) -> iterable[JSONPathMatch]`
|
|
121
|
-
|
|
122
|
-
Return an iterator yielding a `JSONPathMatch` instance for each match of the `path` in the given `data`. If data is a string, it will be loaded using `json.loads()` and the default `JSONDecoder`.
|
|
123
|
-
|
|
124
|
-
### jsonpath.compile
|
|
125
|
-
|
|
126
|
-
`compile(path: str) -> JSONPath | CompoundJSONPath`
|
|
127
|
-
|
|
128
|
-
Prepare a path for repeated matching against different data. `jsonpath.findall()` and `jsonpath.finditer()` are convenience functions that call `compile()` for you.
|
|
129
|
-
|
|
130
|
-
`JSONPath` and `CompoundJSONPath` both have `findall()` and `finditer()` methods that behave the same as `jsonpath.findall()` and `jsonpath.finditer()`, just without the path argument.
|
|
131
|
-
|
|
132
|
-
### async
|
|
133
|
-
|
|
134
|
-
`findall_async()` and `finditer_async()` are async equivalents of `findall()` and `finditer()`. They are used when integrating Python JSONPath with [Python Liquid](https://github.com/jg-rp/liquid) and use Python Liquid's [async protocol](https://jg-rp.github.io/liquid/introduction/async-support).
|
|
135
|
-
|
|
136
|
-
### Extra filter context
|
|
137
|
-
|
|
138
|
-
`findall()` and `finditer()` take an optional `filter_context` argument, being a mapping of strings to arbitrary data that can be referenced from a [filter expression](#filters-expression).
|
|
139
|
-
|
|
140
|
-
Use `#` to query extra filter data, similar to how one might use `@` or `$`.
|
|
141
|
-
|
|
142
|
-
## Syntax
|
|
143
|
-
|
|
144
|
-
Python JSONPath's default syntax is an opinionated combination of JSONPath features from existing, popular implementations, and much of the [IETF JSONPath draft](https://datatracker.ietf.org/doc/html/draft-ietf-jsonpath-base-11). If you're already familiar with JSONPath syntax, skip to [notable differences](#notable-differences).
|
|
145
|
-
|
|
146
|
-
Imagine a JSON document as a tree structure, where each object (mapping) and array can contain more objects (mappings), arrays and scalar values. Every object (mapping), array and scalar value is a node in the tree, and the outermost object (mapping) or array is the "root" node.
|
|
147
|
-
|
|
148
|
-
For our purposes, a JSON "document" could be a file containing valid JSON data, a Python string containing valid JSON data, or a Python `Object` made up of dictionaries (or any [Mapping](https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes)), lists (or any [Sequence](https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes)), strings, etc.
|
|
149
|
-
|
|
150
|
-
We chain _selectors_ together to retrieve nodes from the target document. Each selector operates on the nodes matched by preceding selectors.
|
|
151
|
-
|
|
152
|
-
### Root (`$`)
|
|
153
|
-
|
|
154
|
-
`$` refers to the first node in the target document, be it an object or an array. Unless referencing the root node from inside a filter expression, `$` is optional. The following two examples are equivalent.
|
|
155
|
-
|
|
156
|
-
```text
|
|
157
|
-
$.categories.*.name
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
```text
|
|
161
|
-
categories.*.name
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
An empty path or a path containing just the root (`$`) selector returns the input data in its entirety.
|
|
165
|
-
|
|
166
|
-
### Properties (`.thing`, `[thing]` or `['thing']`)
|
|
167
|
-
|
|
168
|
-
Select nodes by property/key name using dot notation (`.something`) or bracket notation (`[something]`). If a target property/key contains reserved characters, it must use bracket notation and be enclosed in quotes (`['thing']`).
|
|
169
|
-
|
|
170
|
-
A dot in front of bracket notation is OK, but unnecessary. The following examples are equivalent.
|
|
171
|
-
|
|
172
|
-
```text
|
|
173
|
-
$.categories[0].name
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
```text
|
|
177
|
-
$.categories[0][name]
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
```text
|
|
181
|
-
$.categories[0]['name']
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Array indices (`.0`, `[0]` or `[-1]`)
|
|
185
|
-
|
|
186
|
-
Select an item from an array by its index. Indices are zero-based and enclosed in brackets. If the index is negative, items are selected from the end of the array. Considering example data from the top of this page, the following examples are equivalent.
|
|
187
|
-
|
|
188
|
-
```text
|
|
189
|
-
$.categories[0]
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
```text
|
|
193
|
-
$.categories.0
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
```text
|
|
197
|
-
$.categories[-1]
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Wildcard (`.*` or `[*]`)
|
|
201
|
-
|
|
202
|
-
Select all elements from an array or all values from an object using `*`. These two examples are equivalent.
|
|
203
|
-
|
|
204
|
-
```text
|
|
205
|
-
$.categories[0].products.*
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
```text
|
|
209
|
-
$.categories[0].products[*]
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### Slices (`[0:-1]` or `[-1:0:-1]`)
|
|
213
|
-
|
|
214
|
-
Select a range of elements from an array using slice notation. The start index, stop index and step are all optional. These examples are equivalent.
|
|
215
|
-
|
|
216
|
-
```text
|
|
217
|
-
$.categories[0:]
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
```text
|
|
221
|
-
$.categories[0:-1:]
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
```text
|
|
225
|
-
$.categories[0:-1:1]
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
```text
|
|
229
|
-
$.categories[::]
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### Lists (`[1, 2, 10:20]`)
|
|
233
|
-
|
|
234
|
-
Select multiple indices, slices or properties using list notation (sometimes known as a "union" or "segment", we use "union" to mean something else).
|
|
235
|
-
|
|
236
|
-
```text
|
|
237
|
-
$..products.*.[title, price]
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Recursive descent (`..`)
|
|
241
|
-
|
|
242
|
-
The `..` selector visits every node beneath the current selection. If a property selector, using dot notation, follows `..`, the dot is optional. These two examples are equivalent.
|
|
243
|
-
|
|
244
|
-
```text
|
|
245
|
-
$..title
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
```text
|
|
249
|
-
$...title
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### Filters (`[?(EXPRESSION)]`)
|
|
253
|
-
|
|
254
|
-
Filters allow you to remove nodes from a selection using a Boolean expression. Within a filter, `@` refers to the current node and `$` refers to the root node in the target document. `@` and `$` can be used to select nodes as part of the expression. Since version 0.3.0, the parentheses are optional, as per the IETF JSONPath draft. These two examples are equivalent.
|
|
255
|
-
|
|
256
|
-
```text
|
|
257
|
-
$..products[?(@.price < $.price_cap)]
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
```text
|
|
261
|
-
$..products[?@.price < $.price_cap]
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Comparison operators include `==`, `!=`, `<`, `>`, `<=` and `>=`. Plus `<>` as an alias for `!=`.
|
|
265
|
-
|
|
266
|
-
`in` and `contains` are membership operators. `left in right` is equivalent to `right contains left`.
|
|
267
|
-
|
|
268
|
-
`&&` and `||` are logical operators, `and` and `or` work too.
|
|
269
|
-
|
|
270
|
-
`=~` matches the left value with a regular expression literal. Regular expressions use a syntax similar to that found in JavaScript, where the pattern to match is surrounded by slashes, optionally followed by flags.
|
|
271
|
-
|
|
272
|
-
```text
|
|
273
|
-
$..products[?(@.description =~ /.*trainers/i)]
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
Filters can use [function extensions](#function-extensions) too.
|
|
277
|
-
|
|
278
|
-
### Union (`|`) and intersection (`&`)
|
|
279
|
-
|
|
280
|
-
Union (`|`) and intersection (`&`) are similar to Python's set operations, but we don't dedupe the matches (matches will often contain unhashable objects).
|
|
281
|
-
|
|
282
|
-
The `|` operator combines matches from two or more paths. This example selects a single list of all prices, plus the price cap as the last element.
|
|
283
|
-
|
|
284
|
-
```text
|
|
285
|
-
$..products.*.price | $.price_cap
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
The `&` operator produces matches that are common to both left and right paths. This example would select the list of products that are common to both the "footwear" and "headwear" categories.
|
|
289
|
-
|
|
290
|
-
```text
|
|
291
|
-
$.categories[?(@.name == 'footwear')].products.* & $.categories[?(@.name == 'headwear')].products.*
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Note that `|` and `&` are not allowed inside filter expressions.
|
|
295
|
-
|
|
296
|
-
## Function extensions
|
|
297
|
-
|
|
298
|
-
TODO:
|
|
299
|
-
|
|
300
|
-
## Notable differences
|
|
301
|
-
|
|
302
|
-
This is a list of things that you might find in other JSONPath implementation that we don't support (yet).
|
|
303
|
-
|
|
304
|
-
- We don't support extension functions of the form `selector.func()`.
|
|
305
|
-
- We always return a list of matches from `jsonpath.findall()`, never a scalar value.
|
|
306
|
-
- We do not support arithmetic in filter expression.
|
|
307
|
-
- Python JSONPath is strictly read only. There are no update "selectors".
|
|
308
|
-
|
|
309
|
-
And this is a list of areas where we deviate from the [IETF JSONPath draft](https://datatracker.ietf.org/doc/html/draft-ietf-jsonpath-base-11).
|
|
310
|
-
|
|
311
|
-
- For now, the only built-in function extension is `length()`.
|
|
312
|
-
- We don't require filters that use a function extension to include a comparison operator.
|
|
313
|
-
- Whitespace is mostly insignificant unless inside quotes.
|
|
314
|
-
- The root token (default `$`) is optional.
|
|
315
|
-
- Paths starting with a dot (`.`) are OK. `.thing` is the same as `$.thing`, as is `thing`, `$[thing]` and `$["thing"]`.
|
|
316
|
-
- Nested filters are not supported.
|
|
317
|
-
|
|
318
|
-
And this is a list of features that are uncommon or unique to Python JSONPath.
|
|
319
|
-
|
|
320
|
-
- `|` is a union operator, where matches from two or more JSONPaths are combined. This is not part of the Python API, but built-in to the JSONPath syntax.
|
|
321
|
-
- `&` is an intersection operator, where we exclude matches that don't exist in both left and right paths. This is not part of the Python API, but built-in to the JSONPath syntax.
|
|
322
|
-
- `#` is a filter context selector. With usage similar to `$` and `@`, `#` exposes arbitrary data from the `filter_context` argument to `findall()` and `finditer()`.
|
|
323
|
-
|
|
324
|
-
## License
|
|
325
|
-
|
|
326
|
-
`python-jsonpath` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
jsonpath/__about__.py,sha256=KQpPS9HnR8qrZ7FopeZE_aEgURV3rMaSR0Flb7lvXK4,132
|
|
2
|
-
jsonpath/__init__.py,sha256=rqknIKK4kCu_mlKsCe9c9CoH4W4exuOgmG7Wp0qg5Ao,1023
|
|
3
|
-
jsonpath/env.py,sha256=7Rn43jEd9BTa-Xo0yxwIczV2QaIKOHKkKvZyfoNdKZw,14371
|
|
4
|
-
jsonpath/exceptions.py,sha256=PGHORFjnuln5ot4m9ixIi2BsAADIaaOAbZCXgaX5od4,1796
|
|
5
|
-
jsonpath/filter.py,sha256=lrh6EjOv542a3NoKAdw8bRWQuxxhqmUUF9Ye2Dz9iyg,11536
|
|
6
|
-
jsonpath/lex.py,sha256=v0gqUOvWSODWiv10IPo_80Yr1a8c6fEjpAizY2YxTeg,11085
|
|
7
|
-
jsonpath/match.py,sha256=lJn27VWgTMUeAEO4FtmAmUPvf9FK7hTF8f2yq38KiOk,2513
|
|
8
|
-
jsonpath/parse.py,sha256=v5V5t3TcNaPJaq5YRbWHe1vDYGa8gjIRzEgmRcfzfzw,17562
|
|
9
|
-
jsonpath/path.py,sha256=wIlZDLUscZdAwBshoKR6b0Hb7kCTvYgi1tSlZ2ANb6A,11371
|
|
10
|
-
jsonpath/selectors.py,sha256=O6tGtGLdaChi9NNarsYNaYPSBEmiCUpdn1pyjfh-LKA,21397
|
|
11
|
-
jsonpath/stream.py,sha256=V9OtMAYl6jdsj2G8e3u-vH7L4iFhA-vm4Z2Aom10Dxk,2567
|
|
12
|
-
jsonpath/token.py,sha256=BS01OCzriOGn85NkyGoC93h_96BQn-GXeeeM2k_sDPc,3650
|
|
13
|
-
jsonpath/function_extensions/__init__.py,sha256=w4vQh5zViiLhJ767bG6T27IZrwjvp4r47N3ZVSqaYnA,104
|
|
14
|
-
jsonpath/function_extensions/keys.py,sha256=vNdIV8xqr1LMG6RLJwb6iA5-VXgYSZu6CqprXEs6rYM,364
|
|
15
|
-
jsonpath/function_extensions/length.py,sha256=QdwkLLNTbgNKWIx1M8umAHnc5vyc9cjM_vODc6hXDK0,313
|
|
16
|
-
python_jsonpath-0.4.0.dist-info/METADATA,sha256=LTS_flB6aXxUIBkPlykKK06mYbkE-c7WqIbHRGarecA,12076
|
|
17
|
-
python_jsonpath-0.4.0.dist-info/WHEEL,sha256=Fd6mP6ydyRguakwUJ05oBE7fh2IPxgtDN9IwHJ9OqJQ,87
|
|
18
|
-
python_jsonpath-0.4.0.dist-info/licenses/LICENSE.txt,sha256=0tA1mPzOZoK7XLCxN8Lqnp52hZYHgGqtdbC2L-V6sAs,1102
|
|
19
|
-
python_jsonpath-0.4.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023-present James Prior <jamesgr.prior@gmail.com>
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
-
|
|
7
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
-
|
|
9
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
File without changes
|