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 CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2023-present James Prior <jamesgr.prior@gmail.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.4.0"
4
+ __version__ = "0.5.0-post.1"
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
- ) -> None:
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
- params = list(inspect.signature(func).parameters.values())
250
-
251
- # Keyword only params are not supported
252
- if len([p for p in params if p.kind in (p.KEYWORD_ONLY, p.VAR_KEYWORD)]):
253
- raise JSONPathTypeError(
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.match(left))
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 UNDEFINED
320
- return context.current
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 UNDEFINED
337
- return context.current
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 and not in_filter:
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[Union[IndexSelector, PropertySelector, SliceSelector]] = []
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 in (TOKEN_BARE_PROPERTY, TOKEN_STRING):
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=codecs.decode(
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 == TOKEN_EOF:
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
- # TODO: is an empty list an error?
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(tok, function_arguments)
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[Union[SliceSelector, IndexSelector, PropertySelector]],
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
+ [![PyPI - Version](https://img.shields.io/pypi/v/python-jsonpath.svg?style=flat-square)](https://pypi.org/project/python-jsonpath)
28
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/jg-rp/python-jsonpath/tests.yaml?branch=main&label=tests&style=flat-square)](https://github.com/jg-rp/python-jsonpath/actions)
29
+ [![PyPI - License](https://img.shields.io/pypi/l/python-jsonpath?style=flat-square)](https://github.com/jg-rp/python-jsonpath/blob/main/LICENSE.txt)
30
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/python-jsonpath.svg?style=flat-square)](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
- [![PyPI - Version](https://img.shields.io/pypi/v/python-jsonpath.svg?style=flat-square)](https://pypi.org/project/python-jsonpath)
28
- [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/jg-rp/python-jsonpath/tests.yaml?branch=main&label=tests&style=flat-square)](https://github.com/jg-rp/python-jsonpath/actions)
29
- [![PyPI - License](https://img.shields.io/pypi/l/python-jsonpath?style=flat-square)](https://github.com/jg-rp/python-jsonpath/blob/main/LICENSE.txt)
30
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/python-jsonpath.svg?style=flat-square)](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.