python-jsonpath 1.1.1__py3-none-any.whl → 1.2.0__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 +8 -0
- jsonpath/env.py +20 -4
- jsonpath/filter.py +3 -0
- jsonpath/fluent_api.py +130 -5
- jsonpath/lex.py +3 -2
- jsonpath/parse.py +53 -1
- jsonpath/patch.py +116 -0
- jsonpath/path.py +49 -0
- jsonpath/pointer.py +4 -0
- {python_jsonpath-1.1.1.dist-info → python_jsonpath-1.2.0.dist-info}/METADATA +12 -3
- {python_jsonpath-1.1.1.dist-info → python_jsonpath-1.2.0.dist-info}/RECORD +15 -15
- {python_jsonpath-1.1.1.dist-info → python_jsonpath-1.2.0.dist-info}/WHEEL +0 -0
- {python_jsonpath-1.1.1.dist-info → python_jsonpath-1.2.0.dist-info}/entry_points.txt +0 -0
- {python_jsonpath-1.1.1.dist-info → python_jsonpath-1.2.0.dist-info}/licenses/LICENSE.txt +0 -0
jsonpath/__about__.py
CHANGED
jsonpath/__init__.py
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
4
|
|
|
5
5
|
from .env import JSONPathEnvironment
|
|
6
|
+
from .exceptions import JSONPatchError
|
|
7
|
+
from .exceptions import JSONPatchTestFailure
|
|
6
8
|
from .exceptions import JSONPathError
|
|
7
9
|
from .exceptions import JSONPathIndexError
|
|
8
10
|
from .exceptions import JSONPathNameError
|
|
9
11
|
from .exceptions import JSONPathSyntaxError
|
|
10
12
|
from .exceptions import JSONPathTypeError
|
|
13
|
+
from .exceptions import JSONPointerEncodeError
|
|
11
14
|
from .exceptions import JSONPointerError
|
|
12
15
|
from .exceptions import JSONPointerIndexError
|
|
13
16
|
from .exceptions import JSONPointerKeyError
|
|
@@ -17,6 +20,7 @@ from .exceptions import RelativeJSONPointerError
|
|
|
17
20
|
from .exceptions import RelativeJSONPointerIndexError
|
|
18
21
|
from .exceptions import RelativeJSONPointerSyntaxError
|
|
19
22
|
from .filter import UNDEFINED
|
|
23
|
+
from .fluent_api import Projection
|
|
20
24
|
from .fluent_api import Query
|
|
21
25
|
from .lex import Lexer
|
|
22
26
|
from .match import JSONPathMatch
|
|
@@ -36,6 +40,8 @@ __all__ = (
|
|
|
36
40
|
"finditer_async",
|
|
37
41
|
"finditer",
|
|
38
42
|
"JSONPatch",
|
|
43
|
+
"JSONPatchError",
|
|
44
|
+
"JSONPatchTestFailure",
|
|
39
45
|
"JSONPath",
|
|
40
46
|
"JSONPathEnvironment",
|
|
41
47
|
"JSONPathError",
|
|
@@ -45,6 +51,7 @@ __all__ = (
|
|
|
45
51
|
"JSONPathSyntaxError",
|
|
46
52
|
"JSONPathTypeError",
|
|
47
53
|
"JSONPointer",
|
|
54
|
+
"JSONPointerEncodeError",
|
|
48
55
|
"JSONPointerError",
|
|
49
56
|
"JSONPointerIndexError",
|
|
50
57
|
"JSONPointerKeyError",
|
|
@@ -53,6 +60,7 @@ __all__ = (
|
|
|
53
60
|
"Lexer",
|
|
54
61
|
"match",
|
|
55
62
|
"Parser",
|
|
63
|
+
"Projection",
|
|
56
64
|
"query",
|
|
57
65
|
"Query",
|
|
58
66
|
"RelativeJSONPointer",
|
jsonpath/env.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Core JSONPath configuration object."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import re
|
|
@@ -317,7 +318,7 @@ class JSONPathEnvironment:
|
|
|
317
318
|
data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
|
|
318
319
|
filter_context: Optional[FilterContextVars] = None,
|
|
319
320
|
) -> Query:
|
|
320
|
-
"""Return a `Query`
|
|
321
|
+
"""Return a `Query` iterator over matches found by applying _path_ to _data_.
|
|
321
322
|
|
|
322
323
|
`Query` objects are iterable.
|
|
323
324
|
|
|
@@ -352,8 +353,23 @@ class JSONPathEnvironment:
|
|
|
352
353
|
for obj in jsonpath.query("$.foo..bar", data).limit(5).values():
|
|
353
354
|
...
|
|
354
355
|
```
|
|
356
|
+
|
|
357
|
+
Arguments:
|
|
358
|
+
path: The JSONPath as a string.
|
|
359
|
+
data: A JSON document or Python object implementing the `Sequence`
|
|
360
|
+
or `Mapping` interfaces.
|
|
361
|
+
filter_context: Arbitrary data made available to filters using
|
|
362
|
+
the _filter context_ selector.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
A query iterator.
|
|
366
|
+
|
|
367
|
+
Raises:
|
|
368
|
+
JSONPathSyntaxError: If the path is invalid.
|
|
369
|
+
JSONPathTypeError: If a filter expression attempts to use types in
|
|
370
|
+
an incompatible way.
|
|
355
371
|
"""
|
|
356
|
-
return Query(self.finditer(path, data, filter_context=filter_context))
|
|
372
|
+
return Query(self.finditer(path, data, filter_context=filter_context), self)
|
|
357
373
|
|
|
358
374
|
async def findall_async(
|
|
359
375
|
self,
|
|
@@ -548,9 +564,9 @@ class JSONPathEnvironment:
|
|
|
548
564
|
return self._lt(right, left) or self._eq(left, right)
|
|
549
565
|
if operator == "<=":
|
|
550
566
|
return self._lt(left, right) or self._eq(left, right)
|
|
551
|
-
if operator == "in" and isinstance(right, Sequence):
|
|
567
|
+
if operator == "in" and isinstance(right, (Mapping, Sequence)):
|
|
552
568
|
return left in right
|
|
553
|
-
if operator == "contains" and isinstance(left, Sequence):
|
|
569
|
+
if operator == "contains" and isinstance(left, (Mapping, Sequence)):
|
|
554
570
|
return right in left
|
|
555
571
|
if operator == "=~" and isinstance(right, re.Pattern) and isinstance(left, str):
|
|
556
572
|
return bool(right.fullmatch(left))
|
jsonpath/filter.py
CHANGED
jsonpath/fluent_api.py
CHANGED
|
@@ -1,19 +1,46 @@
|
|
|
1
|
-
"""A fluent API for
|
|
1
|
+
"""A fluent API for working with `JSONPathMatch` iterators."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import collections
|
|
5
6
|
import itertools
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from enum import auto
|
|
6
9
|
from typing import TYPE_CHECKING
|
|
10
|
+
from typing import Any
|
|
11
|
+
from typing import Dict
|
|
7
12
|
from typing import Iterable
|
|
8
13
|
from typing import Iterator
|
|
14
|
+
from typing import List
|
|
15
|
+
from typing import Mapping
|
|
9
16
|
from typing import Optional
|
|
17
|
+
from typing import Sequence
|
|
10
18
|
from typing import Tuple
|
|
19
|
+
from typing import Union
|
|
11
20
|
|
|
12
21
|
if TYPE_CHECKING:
|
|
22
|
+
from jsonpath import CompoundJSONPath
|
|
23
|
+
from jsonpath import JSONPath
|
|
24
|
+
from jsonpath import JSONPathEnvironment
|
|
13
25
|
from jsonpath import JSONPathMatch
|
|
14
26
|
from jsonpath import JSONPointer
|
|
15
27
|
|
|
16
28
|
|
|
29
|
+
class Projection(Enum):
|
|
30
|
+
"""Projection style used by `Query.select()`."""
|
|
31
|
+
|
|
32
|
+
RELATIVE = auto()
|
|
33
|
+
"""The default projection. Selections include parent arrays and objects relative
|
|
34
|
+
to the JSONPathMatch."""
|
|
35
|
+
|
|
36
|
+
ROOT = auto()
|
|
37
|
+
"""Selections include parent arrays and objects relative to the root JSON value."""
|
|
38
|
+
|
|
39
|
+
FLAT = auto()
|
|
40
|
+
"""All selections are appended to a new array/list, without arrays and objects
|
|
41
|
+
on the path to the selected value."""
|
|
42
|
+
|
|
43
|
+
|
|
17
44
|
class Query:
|
|
18
45
|
"""A fluent API for managing `JSONPathMatch` iterators.
|
|
19
46
|
|
|
@@ -27,8 +54,9 @@ class Query:
|
|
|
27
54
|
**New in version 1.1.0**
|
|
28
55
|
"""
|
|
29
56
|
|
|
30
|
-
def __init__(self, it: Iterable[JSONPathMatch]) -> None:
|
|
57
|
+
def __init__(self, it: Iterable[JSONPathMatch], env: JSONPathEnvironment) -> None:
|
|
31
58
|
self._it = iter(it)
|
|
59
|
+
self._env = env
|
|
32
60
|
|
|
33
61
|
def __iter__(self) -> Iterator[JSONPathMatch]:
|
|
34
62
|
return self._it
|
|
@@ -118,7 +146,7 @@ class Query:
|
|
|
118
146
|
return (m.path for m in self._it)
|
|
119
147
|
|
|
120
148
|
def items(self) -> Iterable[Tuple[str, object]]:
|
|
121
|
-
"""Return an iterable of (
|
|
149
|
+
"""Return an iterable of (path, object) tuples, one for each match."""
|
|
122
150
|
return ((m.path, m.obj) for m in self._it)
|
|
123
151
|
|
|
124
152
|
def pointers(self) -> Iterable[JSONPointer]:
|
|
@@ -151,11 +179,108 @@ class Query:
|
|
|
151
179
|
|
|
152
180
|
It is not safe to use a `Query` instance after calling `tee()`.
|
|
153
181
|
"""
|
|
154
|
-
return tuple(Query(it) for it in itertools.tee(self._it, n))
|
|
182
|
+
return tuple(Query(it, self._env) for it in itertools.tee(self._it, n))
|
|
155
183
|
|
|
156
184
|
def take(self, n: int) -> Query:
|
|
157
185
|
"""Return a new query iterating over the next _n_ matches.
|
|
158
186
|
|
|
159
187
|
It is safe to continue using this query after calling take.
|
|
160
188
|
"""
|
|
161
|
-
return Query(list(itertools.islice(self._it, n)))
|
|
189
|
+
return Query(list(itertools.islice(self._it, n)), self._env)
|
|
190
|
+
|
|
191
|
+
def select(
|
|
192
|
+
self,
|
|
193
|
+
*expressions: Union[str, JSONPath, CompoundJSONPath],
|
|
194
|
+
projection: Projection = Projection.RELATIVE,
|
|
195
|
+
) -> Iterable[object]:
|
|
196
|
+
"""Query projection using relative JSONPaths.
|
|
197
|
+
|
|
198
|
+
Arguments:
|
|
199
|
+
expressions: One or more JSONPath query expressions to select relative
|
|
200
|
+
to each match in this query iterator.
|
|
201
|
+
projection: The style of projection used when selecting values. Can be
|
|
202
|
+
one of `Projection.RELATIVE`, `Projection.ROOT` or `Projection.FLAT`.
|
|
203
|
+
Defaults to `Projection.RELATIVE`.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
An iterable of objects built from selecting _expressions_ relative to
|
|
207
|
+
each match from the current query.
|
|
208
|
+
|
|
209
|
+
**New in version 1.2.0**
|
|
210
|
+
"""
|
|
211
|
+
return filter(
|
|
212
|
+
bool,
|
|
213
|
+
(self._select(m, expressions, projection) for m in self._it),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _select(
|
|
217
|
+
self,
|
|
218
|
+
match: JSONPathMatch,
|
|
219
|
+
expressions: Tuple[Union[str, JSONPath, CompoundJSONPath], ...],
|
|
220
|
+
projection: Projection,
|
|
221
|
+
) -> object:
|
|
222
|
+
if not isinstance(match.obj, (Mapping, Sequence)) or isinstance(match.obj, str):
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
if projection == Projection.RELATIVE:
|
|
226
|
+
obj: Dict[Union[int, str], Any] = {}
|
|
227
|
+
for expr in expressions:
|
|
228
|
+
path = self._env.compile(expr) if isinstance(expr, str) else expr
|
|
229
|
+
for rel_match in path.finditer(match.obj): # type: ignore
|
|
230
|
+
_patch_obj(rel_match.parts, obj, rel_match.obj)
|
|
231
|
+
|
|
232
|
+
return _fix_sparse_arrays(obj)
|
|
233
|
+
|
|
234
|
+
if projection == Projection.FLAT:
|
|
235
|
+
arr: List[object] = []
|
|
236
|
+
for expr in expressions:
|
|
237
|
+
path = self._env.compile(expr) if isinstance(expr, str) else expr
|
|
238
|
+
for rel_match in path.finditer(match.obj): # type: ignore
|
|
239
|
+
arr.append(rel_match.obj)
|
|
240
|
+
return arr
|
|
241
|
+
|
|
242
|
+
# Project from the root document
|
|
243
|
+
obj = {}
|
|
244
|
+
for expr in expressions:
|
|
245
|
+
path = self._env.compile(expr) if isinstance(expr, str) else expr
|
|
246
|
+
for rel_match in path.finditer(match.obj): # type: ignore
|
|
247
|
+
_patch_obj(match.parts + rel_match.parts, obj, rel_match.obj)
|
|
248
|
+
|
|
249
|
+
return _fix_sparse_arrays(obj)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _patch_obj(
|
|
253
|
+
parts: Tuple[Union[int, str], ...],
|
|
254
|
+
obj: Mapping[Union[str, int], Any],
|
|
255
|
+
value: object,
|
|
256
|
+
) -> None:
|
|
257
|
+
_obj = obj
|
|
258
|
+
|
|
259
|
+
# For lack of a better idea, we're patching arrays to dictionaries with
|
|
260
|
+
# integer keys. This is to handle sparse array selections without having
|
|
261
|
+
# to keep track of indexes and how they map from the root JSON value to
|
|
262
|
+
# the selected JSON value.
|
|
263
|
+
#
|
|
264
|
+
# We'll fix these "sparse arrays" after the patch has been applied.
|
|
265
|
+
for part in parts[:-1]:
|
|
266
|
+
if part not in _obj:
|
|
267
|
+
_obj[part] = {} # type: ignore
|
|
268
|
+
_obj = _obj[part]
|
|
269
|
+
|
|
270
|
+
_obj[parts[-1]] = value # type: ignore
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _fix_sparse_arrays(obj: Any) -> object:
|
|
274
|
+
"""Fix sparse arrays (dictionaries with integer keys)."""
|
|
275
|
+
if isinstance(obj, str) or not obj:
|
|
276
|
+
return obj
|
|
277
|
+
|
|
278
|
+
if isinstance(obj, Sequence):
|
|
279
|
+
return [_fix_sparse_arrays(e) for e in obj]
|
|
280
|
+
|
|
281
|
+
if isinstance(obj, Mapping):
|
|
282
|
+
if isinstance(next(iter(obj)), int):
|
|
283
|
+
return [_fix_sparse_arrays(v) for v in obj.values()]
|
|
284
|
+
return {k: _fix_sparse_arrays(v) for k, v in obj.items()}
|
|
285
|
+
|
|
286
|
+
return obj
|
jsonpath/lex.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""JSONPath tokenization."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import re
|
|
@@ -138,8 +139,8 @@ class Lexer:
|
|
|
138
139
|
(TOKEN_LIST_SLICE, self.slice_list_pattern),
|
|
139
140
|
(TOKEN_FUNCTION, self.function_pattern),
|
|
140
141
|
(TOKEN_DOT_PROPERTY, self.dot_property_pattern),
|
|
141
|
-
(TOKEN_FLOAT, r"-?\d+\.\d*(?:
|
|
142
|
-
(TOKEN_INT, r"-?\d+(?P<G_EXP>
|
|
142
|
+
(TOKEN_FLOAT, r"-?\d+\.\d*(?:[eE][+-]?\d+)?"),
|
|
143
|
+
(TOKEN_INT, r"-?\d+(?P<G_EXP>[eE][+\-]?\d+)?\b"),
|
|
143
144
|
(TOKEN_DDOT, r"\.\."),
|
|
144
145
|
(TOKEN_AND, self.logical_and_pattern),
|
|
145
146
|
(TOKEN_OR, self.logical_or_pattern),
|
jsonpath/parse.py
CHANGED
|
@@ -30,6 +30,8 @@ from .filter import FunctionExtension
|
|
|
30
30
|
from .filter import InfixExpression
|
|
31
31
|
from .filter import IntegerLiteral
|
|
32
32
|
from .filter import ListLiteral
|
|
33
|
+
from .filter import Literal
|
|
34
|
+
from .filter import Nil
|
|
33
35
|
from .filter import Path
|
|
34
36
|
from .filter import PrefixExpression
|
|
35
37
|
from .filter import RegexLiteral
|
|
@@ -191,6 +193,23 @@ class Parser:
|
|
|
191
193
|
"<=",
|
|
192
194
|
"<",
|
|
193
195
|
"!=",
|
|
196
|
+
"=~",
|
|
197
|
+
]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Infix operators that accept filter expression literals.
|
|
201
|
+
INFIX_LITERAL_OPERATORS = frozenset(
|
|
202
|
+
[
|
|
203
|
+
"==",
|
|
204
|
+
">=",
|
|
205
|
+
">",
|
|
206
|
+
"<=",
|
|
207
|
+
"<",
|
|
208
|
+
"!=",
|
|
209
|
+
"<>",
|
|
210
|
+
"=~",
|
|
211
|
+
"in",
|
|
212
|
+
"contains",
|
|
194
213
|
]
|
|
195
214
|
)
|
|
196
215
|
|
|
@@ -455,6 +474,12 @@ class Parser:
|
|
|
455
474
|
stream.expect_peek(TOKEN_COMMA)
|
|
456
475
|
stream.next_token()
|
|
457
476
|
|
|
477
|
+
if stream.peek.kind == TOKEN_RBRACKET:
|
|
478
|
+
raise JSONPathSyntaxError(
|
|
479
|
+
"unexpected trailing comma",
|
|
480
|
+
token=stream.peek,
|
|
481
|
+
)
|
|
482
|
+
|
|
458
483
|
stream.next_token()
|
|
459
484
|
|
|
460
485
|
if not list_items:
|
|
@@ -477,6 +502,13 @@ class Parser:
|
|
|
477
502
|
f"result of {expr.name}() must be compared", token=tok
|
|
478
503
|
)
|
|
479
504
|
|
|
505
|
+
if isinstance(expr, (Literal, Nil)):
|
|
506
|
+
raise JSONPathSyntaxError(
|
|
507
|
+
"filter expression literals outside of "
|
|
508
|
+
"function expressions must be compared",
|
|
509
|
+
token=tok,
|
|
510
|
+
)
|
|
511
|
+
|
|
480
512
|
return Filter(env=self.env, token=tok, expression=BooleanExpression(expr))
|
|
481
513
|
|
|
482
514
|
def parse_boolean(self, stream: TokenStream) -> FilterExpression:
|
|
@@ -520,6 +552,20 @@ class Parser:
|
|
|
520
552
|
self._raise_for_non_comparable_function(left, tok)
|
|
521
553
|
self._raise_for_non_comparable_function(right, tok)
|
|
522
554
|
|
|
555
|
+
if operator not in self.INFIX_LITERAL_OPERATORS:
|
|
556
|
+
if isinstance(left, (Literal, Nil)):
|
|
557
|
+
raise JSONPathSyntaxError(
|
|
558
|
+
"filter expression literals outside of "
|
|
559
|
+
"function expressions must be compared",
|
|
560
|
+
token=tok,
|
|
561
|
+
)
|
|
562
|
+
if isinstance(right, (Literal, Nil)):
|
|
563
|
+
raise JSONPathSyntaxError(
|
|
564
|
+
"filter expression literals outside of "
|
|
565
|
+
"function expressions must be compared",
|
|
566
|
+
token=tok,
|
|
567
|
+
)
|
|
568
|
+
|
|
523
569
|
return InfixExpression(left, operator, right)
|
|
524
570
|
|
|
525
571
|
def parse_grouped_expression(self, stream: TokenStream) -> FilterExpression:
|
|
@@ -532,6 +578,13 @@ class Parser:
|
|
|
532
578
|
raise JSONPathSyntaxError(
|
|
533
579
|
"unbalanced parentheses", token=stream.current
|
|
534
580
|
)
|
|
581
|
+
|
|
582
|
+
if stream.current.kind not in self.BINARY_OPERATORS:
|
|
583
|
+
raise JSONPathSyntaxError(
|
|
584
|
+
f"expected an expression, found '{stream.current.value}'",
|
|
585
|
+
token=stream.current,
|
|
586
|
+
)
|
|
587
|
+
|
|
535
588
|
expr = self.parse_infix_expression(stream, expr)
|
|
536
589
|
|
|
537
590
|
stream.expect(TOKEN_RPAREN)
|
|
@@ -539,7 +592,6 @@ class Parser:
|
|
|
539
592
|
|
|
540
593
|
def parse_root_path(self, stream: TokenStream) -> FilterExpression:
|
|
541
594
|
root = stream.next_token()
|
|
542
|
-
assert root.kind in {TOKEN_ROOT, TOKEN_FAKE_ROOT} # XXX:
|
|
543
595
|
return RootPath(
|
|
544
596
|
JSONPath(
|
|
545
597
|
env=self.env,
|
jsonpath/patch.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""JSON Patch, as per RFC 6902."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import copy
|
|
@@ -85,6 +86,78 @@ class OpAdd(Op):
|
|
|
85
86
|
return {"op": self.name, "path": str(self.path), "value": self.value}
|
|
86
87
|
|
|
87
88
|
|
|
89
|
+
class OpAddNe(OpAdd):
|
|
90
|
+
"""A non-standard _add if not exists_ operation.
|
|
91
|
+
|
|
92
|
+
This is like _OpAdd_, but only adds object/dict keys/values if they key does
|
|
93
|
+
not already exist.
|
|
94
|
+
|
|
95
|
+
**New in version 1.2.0**
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
__slots__ = ("path", "value")
|
|
99
|
+
|
|
100
|
+
name = "addne"
|
|
101
|
+
|
|
102
|
+
def apply(
|
|
103
|
+
self, data: Union[MutableSequence[object], MutableMapping[str, object]]
|
|
104
|
+
) -> Union[MutableSequence[object], MutableMapping[str, object]]:
|
|
105
|
+
"""Apply this patch operation to _data_."""
|
|
106
|
+
parent, obj = self.path.resolve_parent(data)
|
|
107
|
+
if parent is None:
|
|
108
|
+
# Replace the root object.
|
|
109
|
+
# The following op, if any, will raise a JSONPatchError if needed.
|
|
110
|
+
return self.value # type: ignore
|
|
111
|
+
|
|
112
|
+
target = self.path.parts[-1]
|
|
113
|
+
if isinstance(parent, MutableSequence):
|
|
114
|
+
if obj is UNDEFINED:
|
|
115
|
+
parent.append(self.value)
|
|
116
|
+
else:
|
|
117
|
+
parent.insert(int(target), self.value)
|
|
118
|
+
elif isinstance(parent, MutableMapping) and target not in parent:
|
|
119
|
+
parent[target] = self.value
|
|
120
|
+
return data
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class OpAddAp(OpAdd):
|
|
124
|
+
"""A non-standard add operation that appends to arrays/lists .
|
|
125
|
+
|
|
126
|
+
This is like _OpAdd_, but assumes an index of "-" if the path can not
|
|
127
|
+
be resolved.
|
|
128
|
+
|
|
129
|
+
**New in version 1.2.0**
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
__slots__ = ("path", "value")
|
|
133
|
+
|
|
134
|
+
name = "addap"
|
|
135
|
+
|
|
136
|
+
def apply(
|
|
137
|
+
self, data: Union[MutableSequence[object], MutableMapping[str, object]]
|
|
138
|
+
) -> Union[MutableSequence[object], MutableMapping[str, object]]:
|
|
139
|
+
"""Apply this patch operation to _data_."""
|
|
140
|
+
parent, obj = self.path.resolve_parent(data)
|
|
141
|
+
if parent is None:
|
|
142
|
+
# Replace the root object.
|
|
143
|
+
# The following op, if any, will raise a JSONPatchError if needed.
|
|
144
|
+
return self.value # type: ignore
|
|
145
|
+
|
|
146
|
+
target = self.path.parts[-1]
|
|
147
|
+
if isinstance(parent, MutableSequence):
|
|
148
|
+
if obj is UNDEFINED:
|
|
149
|
+
parent.append(self.value)
|
|
150
|
+
else:
|
|
151
|
+
parent.insert(int(target), self.value)
|
|
152
|
+
elif isinstance(parent, MutableMapping):
|
|
153
|
+
parent[target] = self.value
|
|
154
|
+
else:
|
|
155
|
+
raise JSONPatchError(
|
|
156
|
+
f"unexpected operation on {parent.__class__.__name__!r}"
|
|
157
|
+
)
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
|
|
88
161
|
class OpRemove(Op):
|
|
89
162
|
"""The JSON Patch _remove_ operation."""
|
|
90
163
|
|
|
@@ -340,6 +413,16 @@ class JSONPatch:
|
|
|
340
413
|
path=self._op_pointer(operation, "path", "add", i),
|
|
341
414
|
value=self._op_value(operation, "value", "add", i),
|
|
342
415
|
)
|
|
416
|
+
elif op == "addne":
|
|
417
|
+
self.addne(
|
|
418
|
+
path=self._op_pointer(operation, "path", "addne", i),
|
|
419
|
+
value=self._op_value(operation, "value", "addne", i),
|
|
420
|
+
)
|
|
421
|
+
elif op == "addap":
|
|
422
|
+
self.addne(
|
|
423
|
+
path=self._op_pointer(operation, "path", "addap", i),
|
|
424
|
+
value=self._op_value(operation, "value", "addap", i),
|
|
425
|
+
)
|
|
343
426
|
elif op == "remove":
|
|
344
427
|
self.remove(path=self._op_pointer(operation, "path", "add", i))
|
|
345
428
|
elif op == "replace":
|
|
@@ -424,6 +507,38 @@ class JSONPatch:
|
|
|
424
507
|
self.ops.append(OpAdd(path=pointer, value=value))
|
|
425
508
|
return self
|
|
426
509
|
|
|
510
|
+
def addne(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
|
|
511
|
+
"""Append an _addne_ operation to this patch.
|
|
512
|
+
|
|
513
|
+
Arguments:
|
|
514
|
+
path: A string representation of a JSON Pointer, or one that has
|
|
515
|
+
already been parsed.
|
|
516
|
+
value: The object to add.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
This `JSONPatch` instance, so we can build a JSON Patch by chaining
|
|
520
|
+
calls to JSON Patch operation methods.
|
|
521
|
+
"""
|
|
522
|
+
pointer = self._ensure_pointer(path)
|
|
523
|
+
self.ops.append(OpAddNe(path=pointer, value=value))
|
|
524
|
+
return self
|
|
525
|
+
|
|
526
|
+
def addap(self: Self, path: Union[str, JSONPointer], value: object) -> Self:
|
|
527
|
+
"""Append an _addap_ operation to this patch.
|
|
528
|
+
|
|
529
|
+
Arguments:
|
|
530
|
+
path: A string representation of a JSON Pointer, or one that has
|
|
531
|
+
already been parsed.
|
|
532
|
+
value: The object to add.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
This `JSONPatch` instance, so we can build a JSON Patch by chaining
|
|
536
|
+
calls to JSON Patch operation methods.
|
|
537
|
+
"""
|
|
538
|
+
pointer = self._ensure_pointer(path)
|
|
539
|
+
self.ops.append(OpAddAp(path=pointer, value=value))
|
|
540
|
+
return self
|
|
541
|
+
|
|
427
542
|
def remove(self: Self, path: Union[str, JSONPointer]) -> Self:
|
|
428
543
|
"""Append a _remove_ operation to this patch.
|
|
429
544
|
|
|
@@ -551,6 +666,7 @@ class JSONPatch:
|
|
|
551
666
|
raise JSONPatchError(f"{err} ({op.name}:{i})") from err
|
|
552
667
|
except (JSONPointerError, JSONPatchError) as err:
|
|
553
668
|
raise JSONPatchError(f"{err} ({op.name}:{i})") from err
|
|
669
|
+
|
|
554
670
|
return _data
|
|
555
671
|
|
|
556
672
|
def asdicts(self) -> List[Dict[str, object]]:
|
jsonpath/path.py
CHANGED
|
@@ -15,6 +15,7 @@ from typing import TypeVar
|
|
|
15
15
|
from typing import Union
|
|
16
16
|
|
|
17
17
|
from jsonpath._data import load_data
|
|
18
|
+
from jsonpath.fluent_api import Query
|
|
18
19
|
from jsonpath.match import FilterContextVars
|
|
19
20
|
from jsonpath.match import JSONPathMatch
|
|
20
21
|
from jsonpath.selectors import IndexSelector
|
|
@@ -210,6 +211,30 @@ class JSONPath:
|
|
|
210
211
|
except StopIteration:
|
|
211
212
|
return None
|
|
212
213
|
|
|
214
|
+
def query(
|
|
215
|
+
self,
|
|
216
|
+
data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
|
|
217
|
+
*,
|
|
218
|
+
filter_context: Optional[FilterContextVars] = None,
|
|
219
|
+
) -> Query:
|
|
220
|
+
"""Return a `Query` iterator over matches found by applying this path to _data_.
|
|
221
|
+
|
|
222
|
+
Arguments:
|
|
223
|
+
data: A JSON document or Python object implementing the `Sequence`
|
|
224
|
+
or `Mapping` interfaces.
|
|
225
|
+
filter_context: Arbitrary data made available to filters using
|
|
226
|
+
the _filter context_ selector.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
A query iterator.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
JSONPathSyntaxError: If the path is invalid.
|
|
233
|
+
JSONPathTypeError: If a filter expression attempts to use types in
|
|
234
|
+
an incompatible way.
|
|
235
|
+
"""
|
|
236
|
+
return Query(self.finditer(data, filter_context=filter_context), self.env)
|
|
237
|
+
|
|
213
238
|
def empty(self) -> bool:
|
|
214
239
|
"""Return `True` if this path has no selectors."""
|
|
215
240
|
return not bool(self.selectors)
|
|
@@ -407,6 +432,30 @@ class CompoundJSONPath:
|
|
|
407
432
|
|
|
408
433
|
return matches
|
|
409
434
|
|
|
435
|
+
def query(
|
|
436
|
+
self,
|
|
437
|
+
data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
|
|
438
|
+
*,
|
|
439
|
+
filter_context: Optional[FilterContextVars] = None,
|
|
440
|
+
) -> Query:
|
|
441
|
+
"""Return a `Query` iterator over matches found by applying this path to _data_.
|
|
442
|
+
|
|
443
|
+
Arguments:
|
|
444
|
+
data: A JSON document or Python object implementing the `Sequence`
|
|
445
|
+
or `Mapping` interfaces.
|
|
446
|
+
filter_context: Arbitrary data made available to filters using
|
|
447
|
+
the _filter context_ selector.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
A query iterator.
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
JSONPathSyntaxError: If the path is invalid.
|
|
454
|
+
JSONPathTypeError: If a filter expression attempts to use types in
|
|
455
|
+
an incompatible way.
|
|
456
|
+
"""
|
|
457
|
+
return Query(self.finditer(data, filter_context=filter_context), self.env)
|
|
458
|
+
|
|
410
459
|
def union(self, path: JSONPath) -> CompoundJSONPath:
|
|
411
460
|
"""Union of this path and another path."""
|
|
412
461
|
return self.__class__(
|
jsonpath/pointer.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""JSON Pointer. See https://datatracker.ietf.org/doc/html/rfc6901."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import codecs
|
|
@@ -326,6 +327,9 @@ class JSONPointer:
|
|
|
326
327
|
def __eq__(self, other: object) -> bool:
|
|
327
328
|
return isinstance(other, JSONPointer) and self.parts == other.parts
|
|
328
329
|
|
|
330
|
+
def __hash__(self) -> int:
|
|
331
|
+
return hash(self.parts)
|
|
332
|
+
|
|
329
333
|
def __repr__(self) -> str:
|
|
330
334
|
return f"JSONPointer({self._s!r})"
|
|
331
335
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-jsonpath
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: JSONPath, JSON Pointer and JSON Patch for Python.
|
|
5
5
|
Project-URL: Documentation, https://jg-rp.github.io/python-jsonpath/
|
|
6
6
|
Project-URL: Issues, https://github.com/jg-rp/python-jsonpath/issues
|
|
@@ -56,6 +56,7 @@ We follow <a href="https://datatracker.ietf.org/doc/html/rfc9535">RFC 9535</a> a
|
|
|
56
56
|
|
|
57
57
|
- [Install](#install)
|
|
58
58
|
- [Links](#links)
|
|
59
|
+
- [Related projects](#related-projects)
|
|
59
60
|
- [Examples](#examples)
|
|
60
61
|
- [License](#license)
|
|
61
62
|
|
|
@@ -88,6 +89,14 @@ conda install -c conda-forge python-jsonpath
|
|
|
88
89
|
- Source code: https://github.com/jg-rp/python-jsonpath
|
|
89
90
|
- Issue tracker: https://github.com/jg-rp/python-jsonpath/issues
|
|
90
91
|
|
|
92
|
+
## Related projects
|
|
93
|
+
|
|
94
|
+
- [Python JSONPath RFC 9535](https://github.com/jg-rp/python-jsonpath-rfc9535) - An implementation of JSONPath that follows RFC 9535 much more strictly. If you require maximum interoperability with JSONPath implemented in other languages - at the expense of extra features - choose python-jsonpath-rfc9535 over python-jsonpath.
|
|
95
|
+
|
|
96
|
+
python-jsonpath-rfc9535 matches RFC 9535's JSONPath model internally and is careful to match the spec's terminology. It also includes utilities for verifying and testing the [JSONPath Compliance Test Suite](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite). Most notably the nondeterministic behavior of some JSONPath selectors.
|
|
97
|
+
|
|
98
|
+
- [JSON P3](https://github.com/jg-rp/json-p3) - RFC 9535 implemented in TypeScript. JSON P3 does not include all the non-standard features of Python JSONPath, but does define some optional [extra syntax](https://jg-rp.github.io/json-p3/guides/jsonpath-extra).
|
|
99
|
+
|
|
91
100
|
## Examples
|
|
92
101
|
|
|
93
102
|
### JSONPath
|
|
@@ -110,7 +119,7 @@ print(user_names) # ['John', 'Sally', 'Jane']
|
|
|
110
119
|
|
|
111
120
|
### JSON Pointer
|
|
112
121
|
|
|
113
|
-
|
|
122
|
+
We include an [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) compliant implementation of JSON Pointer. See JSON Pointer [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#pointerresolvepointer-data), [guide](https://jg-rp.github.io/python-jsonpath/pointers/) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPointer)
|
|
114
123
|
|
|
115
124
|
```python
|
|
116
125
|
from jsonpath import pointer
|
|
@@ -133,7 +142,7 @@ print(jane_score) # 55
|
|
|
133
142
|
|
|
134
143
|
### JSON Patch
|
|
135
144
|
|
|
136
|
-
|
|
145
|
+
We also include an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) compliant implementation of JSON Patch. See JSON Patch [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#patchapplypatch-data) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch)
|
|
137
146
|
|
|
138
147
|
```python
|
|
139
148
|
from jsonpath import patch
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
jsonpath/__about__.py,sha256=
|
|
2
|
-
jsonpath/__init__.py,sha256=
|
|
1
|
+
jsonpath/__about__.py,sha256=4Wt_cFljtpy0n1ce0PO_Q1JDVHTGvGvg27BJF3RI-_c,132
|
|
2
|
+
jsonpath/__init__.py,sha256=bhEkdyCEv_5MS9J-VyMSRAyIRc-jmKFjLee8-3SYQ3U,2368
|
|
3
3
|
jsonpath/__main__.py,sha256=6Y5wOE7U-MHymopXOsxofaY30tVZYPGTJO0L4vytoUw,61
|
|
4
4
|
jsonpath/_data.py,sha256=JEpu5Kg0_kgxYKUilBcHVdTmPxf3-Vc0NgaW6olsqyY,577
|
|
5
5
|
jsonpath/cli.py,sha256=scpWfJXl1jLQ80ZyXqMCu8JtRgZUXluC11x6OkC4oeA,10026
|
|
6
|
-
jsonpath/env.py,sha256=
|
|
6
|
+
jsonpath/env.py,sha256=EqlLT6_WE7WbChNs_xDipF7V8uGG3EnyKabJ_bstpGs,22815
|
|
7
7
|
jsonpath/exceptions.py,sha256=5n-9vaKTu5asWllHT0IN-824ewcNCyQIyKvQ58TERVk,4351
|
|
8
|
-
jsonpath/filter.py,sha256=
|
|
9
|
-
jsonpath/fluent_api.py,sha256=
|
|
10
|
-
jsonpath/lex.py,sha256=
|
|
8
|
+
jsonpath/filter.py,sha256=_u09ewALY26RG0UvStTgtbIvt_MgjHHKkl6-FJlK7qg,20688
|
|
9
|
+
jsonpath/fluent_api.py,sha256=mfAA2t-xUGOmGVr_1h9lxo3y4FsAvaxOpvOH8jZ4DHU,9117
|
|
10
|
+
jsonpath/lex.py,sha256=U99pjKxlGEIKM9QfH_8ax7X8ajJL4CI2TW3Lev2sZGM,10272
|
|
11
11
|
jsonpath/match.py,sha256=b8fkPHsYjJSrTEOi68MFEyOCj0-FoQjFmMq-Ex4qvo0,3503
|
|
12
|
-
jsonpath/parse.py,sha256=
|
|
13
|
-
jsonpath/patch.py,sha256=
|
|
14
|
-
jsonpath/path.py,sha256=
|
|
15
|
-
jsonpath/pointer.py,sha256=
|
|
12
|
+
jsonpath/parse.py,sha256=s7BuFsSgX2GuDubrFr-SDcCk2ZInGzPV04FNFcUwxPM,25214
|
|
13
|
+
jsonpath/patch.py,sha256=tSr8-cGOY4lgnSX1PTYotObIZfakbTRaktrO4M4dqgQ,25321
|
|
14
|
+
jsonpath/path.py,sha256=RUeP7k_WcvP5ZnYblcRDd_-wgo9VnMKyIevTcRImYE4,16421
|
|
15
|
+
jsonpath/pointer.py,sha256=v8k16orIFFOHY1q_QRa5dj4gtPKA--YLA9srOLZpK_E,22299
|
|
16
16
|
jsonpath/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
jsonpath/selectors.py,sha256=u7YcHBt_yEQv0CeTtsOwVQuP5bHLT0_lnulUwShCKdQ,27370
|
|
18
18
|
jsonpath/stream.py,sha256=S1xxgOsJlivu6Y8-kejMoYm7MgSlhjnoX_H5cem5Yhk,2815
|
|
@@ -28,8 +28,8 @@ jsonpath/function_extensions/match.py,sha256=KjsH33fCFGonp2RV__FuaeIOTwLLcvgaaCi
|
|
|
28
28
|
jsonpath/function_extensions/search.py,sha256=O11fnkHlbvf0QPrLISYfhlPXBvVPBr-U8V0dGbd614Y,710
|
|
29
29
|
jsonpath/function_extensions/typeof.py,sha256=yCAj9zOqSnam1mfHCGolNHWDmsBOvU3rAhbZDYycx50,1780
|
|
30
30
|
jsonpath/function_extensions/value.py,sha256=fQMbPUV87Jn1nOwAlBpTeLmLIG5ejH0XQBOM_SR-Us4,721
|
|
31
|
-
python_jsonpath-1.
|
|
32
|
-
python_jsonpath-1.
|
|
33
|
-
python_jsonpath-1.
|
|
34
|
-
python_jsonpath-1.
|
|
35
|
-
python_jsonpath-1.
|
|
31
|
+
python_jsonpath-1.2.0.dist-info/METADATA,sha256=ShYCGzDgI9YuCbZre9aeA5y1GS-guyMVYiphgacaR4M,6306
|
|
32
|
+
python_jsonpath-1.2.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
|
33
|
+
python_jsonpath-1.2.0.dist-info/entry_points.txt,sha256=xvbWnAebJyOMI_9ugK0xrpFRlwmEsAJD2kNHU0Dvscc,43
|
|
34
|
+
python_jsonpath-1.2.0.dist-info/licenses/LICENSE.txt,sha256=u7PksAQGI1QYWcERHeauMseZ4XAzDKUrKW8Z4wbeU1k,1101
|
|
35
|
+
python_jsonpath-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|