Skip to content

Commit 5b1514b

Browse files
authored
Merge pull request #53 from jg-rp/fluent-iapi
Add a fluent API for JSONPathMatch iterators.
2 parents c529b3f + 67c32bf commit 5b1514b

File tree

12 files changed

+636
-43
lines changed

12 files changed

+636
-43
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Python JSONPath Change Log
22

3+
## Version 1.1.0 (unreleased)
4+
5+
**Features**
6+
7+
- Added the "query API", a fluent, chainable API for manipulating `JSONPathMatch` iterators.
8+
39
## Version 1.0.0
410

511
[RFC 9535](https://datatracker.ietf.org/doc/html/rfc9535) (JSONPath: Query Expressions for JSON) is now out, replacing the [draft IETF JSONPath base](https://datatracker.ietf.org/doc/html/draft-ietf-jsonpath-base-21).

docs/api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
::: jsonpath.CompoundJSONPath
1212
handler: python
1313

14+
::: jsonpath.Query
15+
handler: python
16+
1417
::: jsonpath.function_extensions.FilterFunction
1518
handler: python
1619

docs/query.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Query Iterators
2+
3+
**_New in version 1.1.0_**
4+
5+
In addition to [`findall()`](api.md#jsonpath.JSONPathEnvironment.findall) and [`finditer()`](api.md#jsonpath.JSONPathEnvironment.finditer), covered in the [quick start guide](./quickstart.md), Python JSONPath offers a fluent _query_ iterator interface.
6+
7+
[`Query`](api.md#jsonpath.Query) objects provide chainable methods for manipulating a [`JSONPathMatch`](api.md#jsonpath.JSONPathMatch) iterator, just like you'd get from `finditer()`. Obtain a `Query` object using the package-level `query()` function or [`JSONPathEnvironment.query()`](api.md#jsonpath.JSONPathEnvironment.query).
8+
9+
This example uses the query API to skip the first five matches, limit the total number of matches to ten, and get the value associated with each match.
10+
11+
```python
12+
from jsonpath import query
13+
14+
# data = ...
15+
16+
values = (
17+
query("$.some[[email protected]]", data)
18+
.skip(5)
19+
.limit(10)
20+
.values()
21+
)
22+
23+
for value in values:
24+
# ...
25+
```
26+
27+
`Query` objects are iterable and can only be iterated once. Pass the query to `list()` (or other sequence) to get a list of results that can be iterated multiple times or otherwise manipulated.
28+
29+
```python
30+
from jsonpath import query
31+
32+
# data = ...
33+
34+
values = list(
35+
query("$.some[[email protected]]", data)
36+
.skip(5)
37+
.limit(10)
38+
.values()
39+
)
40+
41+
print(values[1])
42+
```
43+
44+
## Chainable methods
45+
46+
The following `Query` methods all return `self` (the same `Query` instance), so method calls can be chained to further manipulate the underlying iterator.
47+
48+
| Method | Aliases | Description |
49+
| --------------- | --------------- | -------------------------------------------------- |
50+
| `skip(n: int)` | `drop` | Drop up to _n_ matches from the iterator. |
51+
| `limit(n: int)` | `head`, `first` | Yield at most _n_ matches from the iterator. |
52+
| `tail(n: int)` | `last` | Drop matches from the iterator up to the last _n_. |
53+
54+
## Terminal methods
55+
56+
These are terminal methods of the `Query` class. They can not be chained.
57+
58+
| Method | Aliases | Description |
59+
| ------------- | ------- | ------------------------------------------------------------------------------------------- |
60+
| `values()` | | Return an iterable of objects, one for each match in the iterable. |
61+
| `locations()` | | Return an iterable of normalized paths, one for each match in the iterable. |
62+
| `items()` | | Return an iterable of (object, normalized path) tuples, one for each match in the iterable. |
63+
| `pointers()` | | Return an iterable of `JSONPointer` instances, one for each match in the iterable. |
64+
| `first_one()` | `one` | Return the first `JSONPathMatch`, or `None` if there were no matches. |
65+
| `last_one()` | | Return the last `JSONPathMatch`, or `None` if there were no matches. |
66+
67+
## Take
68+
69+
[`Query.take(self, n: int)`](api.md#jsonpath.Query.take) returns a new `Query` instance, iterating over the next _n_ matches. It leaves the existing query in a safe state, ready to resume iteration of remaining matches.
70+
71+
```python
72+
from jsonpath import query
73+
74+
it = query("$.some.*", {"some": [0, 1, 2, 3]})
75+
76+
for match in it.take(2):
77+
print(match.value) # 0, 1
78+
79+
for value in it.values():
80+
print(value) # 2, 3
81+
```
82+
83+
## Tee
84+
85+
And finally there's `tee()`, which creates multiple independent queries from one query iterator. It is not safe to use the initial `Query` instance after calling `tee()`.
86+
87+
```python
88+
from jsonpath import query
89+
90+
it1, it2 = query("$.some[[email protected]]", data).tee()
91+
92+
head = it1.head(10) # first 10 matches
93+
tail = it2.tail(10) # last 10 matches
94+
```

docs/quickstart.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ print(data) # {'some': {'other': 'thing', 'foo': {'bar': [1], 'else': 'thing'}}
294294

295295
## What's Next?
296296

297-
Read about user-defined filter functions at [Function Extensions](advanced.md#function-extensions), or see how to make extra data available to filters with [Extra Filter Context](advanced.md#extra-filter-context).
297+
Read about the [Query Iterators](query.md) API or [user-defined filter functions](advanced.md#function-extensions). Also see how to make extra data available to filters with [Extra Filter Context](advanced.md#extra-filter-context).
298298

299299
`findall()`, `finditer()` and `compile()` are shortcuts that use the default[`JSONPathEnvironment`](api.md#jsonpath.JSONPathEnvironment). `jsonpath.findall(path, data)` is equivalent to:
300300

jsonpath/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .exceptions import RelativeJSONPointerIndexError
1818
from .exceptions import RelativeJSONPointerSyntaxError
1919
from .filter import UNDEFINED
20+
from .fluent_api import Query
2021
from .lex import Lexer
2122
from .match import JSONPathMatch
2223
from .parse import Parser
@@ -30,10 +31,12 @@
3031
__all__ = (
3132
"compile",
3233
"CompoundJSONPath",
34+
"find",
3335
"findall_async",
3436
"findall",
3537
"finditer_async",
3638
"finditer",
39+
"first",
3740
"JSONPatch",
3841
"JSONPath",
3942
"JSONPathEnvironment",
@@ -52,6 +55,8 @@
5255
"Lexer",
5356
"match",
5457
"Parser",
58+
"query",
59+
"Query",
5560
"RelativeJSONPointer",
5661
"RelativeJSONPointerError",
5762
"RelativeJSONPointerIndexError",
@@ -69,3 +74,4 @@
6974
finditer = DEFAULT_ENV.finditer
7075
finditer_async = DEFAULT_ENV.finditer_async
7176
match = DEFAULT_ENV.match
77+
query = DEFAULT_ENV.query

jsonpath/env.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .filter import FunctionExtension
2828
from .filter import InfixExpression
2929
from .filter import Path
30+
from .fluent_api import Query
3031
from .function_extensions import ExpressionType
3132
from .function_extensions import FilterFunction
3233
from .function_extensions import validate
@@ -76,8 +77,6 @@ class attributes `root_token`, `self_token` and `filter_context_token`.
7677
- Hook in to mapping and sequence item getting by overriding `getitem()`.
7778
- Change filter comparison operator behavior by overriding `compare()`.
7879
79-
## Class attributes
80-
8180
Arguments:
8281
filter_caching (bool): If `True`, filter expressions will be cached
8382
where possible.
@@ -89,6 +88,8 @@ class attributes `root_token`, `self_token` and `filter_context_token`.
8988
9089
**New in version 0.10.0**
9190
91+
## Class attributes
92+
9293
Attributes:
9394
fake_root_token (str): The pattern used to select a "fake" root node, one level
9495
above the real root node.
@@ -229,9 +230,9 @@ def findall(
229230
*,
230231
filter_context: Optional[FilterContextVars] = None,
231232
) -> List[object]:
232-
"""Find all objects in `data` matching the given JSONPath `path`.
233+
"""Find all objects in _data_ matching the JSONPath _path_.
233234
234-
If `data` is a string or a file-like objects, it will be loaded
235+
If _data_ is a string or a file-like objects, it will be loaded
235236
using `json.loads()` and the default `JSONDecoder`.
236237
237238
Arguments:
@@ -259,10 +260,10 @@ def finditer(
259260
*,
260261
filter_context: Optional[FilterContextVars] = None,
261262
) -> Iterable[JSONPathMatch]:
262-
"""Generate `JSONPathMatch` objects for each match.
263+
"""Generate `JSONPathMatch` objects for each match of _path_ in _data_.
263264
264-
If `data` is a string or a file-like objects, it will be loaded
265-
using `json.loads()` and the default `JSONDecoder`.
265+
If _data_ is a string or a file-like objects, it will be loaded using
266+
`json.loads()` and the default `JSONDecoder`.
266267
267268
Arguments:
268269
path: The JSONPath as a string.
@@ -310,6 +311,50 @@ def match(
310311
"""
311312
return self.compile(path).match(data, filter_context=filter_context)
312313

314+
def query(
315+
self,
316+
path: str,
317+
data: Union[str, IOBase, Sequence[Any], Mapping[str, Any]],
318+
filter_context: Optional[FilterContextVars] = None,
319+
) -> Query:
320+
"""Return a `Query` object over matches found by applying _path_ to _data_.
321+
322+
`Query` objects are iterable.
323+
324+
```
325+
for match in jsonpath.query("$.foo..bar", data):
326+
...
327+
```
328+
329+
You can skip and limit results with `Query.skip()` and `Query.limit()`.
330+
331+
```
332+
matches = (
333+
jsonpath.query("$.foo..bar", data)
334+
.skip(5)
335+
.limit(10)
336+
)
337+
338+
for match in matches
339+
...
340+
```
341+
342+
`Query.tail()` will get the last _n_ results.
343+
344+
```
345+
for match in jsonpath.query("$.foo..bar", data).tail(5):
346+
...
347+
```
348+
349+
Get values for each match using `Query.values()`.
350+
351+
```
352+
for obj in jsonpath.query("$.foo..bar", data).limit(5).values():
353+
...
354+
```
355+
"""
356+
return Query(self.finditer(path, data, filter_context=filter_context))
357+
313358
async def findall_async(
314359
self,
315360
path: str,

0 commit comments

Comments
 (0)