json-stream 2.1.0__tar.gz → 2.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {json-stream-2.1.0/src/json_stream.egg-info → json_stream-2.4.1}/PKG-INFO +106 -21
- {json-stream-2.1.0 → json_stream-2.4.1}/README.md +96 -17
- {json-stream-2.1.0 → json_stream-2.4.1}/pyproject.toml +5 -3
- json_stream-2.4.1/src/json_stream/__init__.py +4 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/base.py +13 -1
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/dump/__init__.py +4 -5
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/dump/tests/test_dump.py +18 -2
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/httpx/__init__.py +8 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/httpx/tests/test_httpx.py +42 -6
- json_stream-2.4.1/src/json_stream/iterators.py +33 -0
- json_stream-2.4.1/src/json_stream/loader.py +19 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/requests/__init__.py +8 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/requests/tests/test_requests.py +42 -6
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/select_tokenizer.py +1 -1
- json_stream-2.4.1/src/json_stream/tests/test_buffering.py +33 -0
- json_stream-2.4.1/src/json_stream/tests/test_collection_abcs.py +17 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_iterators.py +13 -0
- json_stream-2.4.1/src/json_stream/tests/test_load_many.py +102 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_loader.py +44 -1
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_tokenizer.py +82 -24
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_tokenizer_integration.py +7 -12
- json_stream-2.4.1/src/json_stream/tests/test_util.py +15 -0
- json_stream-2.4.1/src/json_stream/tests/test_visit_many.py +42 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_visitor.py +5 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_writer.py +20 -12
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tokenizer.py +84 -61
- json_stream-2.4.1/src/json_stream/util.py +56 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/visitor.py +14 -4
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/writer.py +6 -2
- {json-stream-2.1.0 → json_stream-2.4.1/src/json_stream.egg-info}/PKG-INFO +106 -21
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream.egg-info/SOURCES.txt +6 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream.egg-info/requires.txt +1 -1
- json-stream-2.1.0/src/json_stream/__init__.py +0 -3
- json-stream-2.1.0/src/json_stream/iterators.py +0 -25
- json-stream-2.1.0/src/json_stream/loader.py +0 -10
- {json-stream-2.1.0 → json_stream-2.4.1}/LICENSE.txt +0 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/setup.cfg +0 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/dump/tests/__init__.py +0 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/dump/threading.py +0 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/httpx/tests/__init__.py +0 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/requests/tests/__init__.py +0 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/__init__.py +0 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream.egg-info/dependency_links.txt +0 -0
- {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: json-stream
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: Streaming JSON encoder and decoder
|
|
5
5
|
Author-email: Jamie Cockburn <jamie_cockburn@hotmail.co.uk>
|
|
6
6
|
License: Copyright (c) 2020 Jamie Cockburn
|
|
@@ -33,20 +33,27 @@ Classifier: Intended Audience :: Developers
|
|
|
33
33
|
Classifier: Topic :: Software Development :: Libraries
|
|
34
34
|
Classifier: License :: OSI Approved :: MIT License
|
|
35
35
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
36
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
37
36
|
Classifier: Programming Language :: Python :: 3.8
|
|
38
37
|
Classifier: Programming Language :: Python :: 3.9
|
|
39
38
|
Classifier: Programming Language :: Python :: 3.10
|
|
40
39
|
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
41
43
|
Requires-Python: <4,>=3.5
|
|
42
44
|
Description-Content-Type: text/markdown
|
|
45
|
+
License-File: LICENSE.txt
|
|
46
|
+
Requires-Dist: json-stream-rs-tokenizer>=0.4.17
|
|
43
47
|
Provides-Extra: requests
|
|
48
|
+
Requires-Dist: requests; extra == "requests"
|
|
44
49
|
Provides-Extra: httpx
|
|
45
|
-
|
|
50
|
+
Requires-Dist: httpx; extra == "httpx"
|
|
51
|
+
Dynamic: license-file
|
|
46
52
|
|
|
47
53
|
# json-stream
|
|
48
54
|
|
|
49
55
|
[](https://github.com/daggaz/json-stream/actions/workflows/tests.yml)
|
|
56
|
+
[](https://codecov.io/gh/daggaz/json-stream)
|
|
50
57
|
[](https://pypi.org/project/json-stream)
|
|
51
58
|
[](https://pypi.org/project/json-stream/)
|
|
52
59
|
[](https://www.buymeacoffee.com/daggaz)
|
|
@@ -55,8 +62,8 @@ Simple streaming JSON parser and encoder.
|
|
|
55
62
|
|
|
56
63
|
When [reading](#reading) JSON data, `json-stream` can decode JSON data in
|
|
57
64
|
a streaming manner, providing a pythonic dict/list-like interface, or a
|
|
58
|
-
[visitor-based
|
|
59
|
-
or [iterators](#iterators).
|
|
65
|
+
[visitor-based interface](#visitor). It can stream from files, [URLs](#urls)
|
|
66
|
+
or [iterators](#iterators). It can process [multiple JSON documents](#multiple) in a single stream.
|
|
60
67
|
|
|
61
68
|
When [writing](#writing) JSON data, `json-stream` can stream JSON objects
|
|
62
69
|
as you generate them.
|
|
@@ -109,8 +116,8 @@ It is also possible to "mix" the modes as you consume the data.
|
|
|
109
116
|
|
|
110
117
|
This mode is appropriate if you can consume the data iteratively. You cannot
|
|
111
118
|
move backwards through the stream to read data that has already been skipped
|
|
112
|
-
over. It is the mode you **must** use if you want process large amounts of
|
|
113
|
-
JSON data without consuming large amounts of memory
|
|
119
|
+
over. It is the mode you **must** use if you want to process large amounts of
|
|
120
|
+
JSON data without consuming large amounts of memory.
|
|
114
121
|
|
|
115
122
|
In transient mode, only the data currently being read is stored in memory. Any
|
|
116
123
|
data previously read from the stream is discarded (it's up to you what to do
|
|
@@ -272,6 +279,65 @@ z at path ('xxxx', 3)
|
|
|
272
279
|
[] at path ('xxxx', 5)
|
|
273
280
|
```
|
|
274
281
|
|
|
282
|
+
### <a id="multiple"></a> Multiple JSON documents: `load_many()` and `visit_many()`
|
|
283
|
+
|
|
284
|
+
Sometimes JSON data arrives as a sequence of top‑level JSON texts rather than a single array/object. json-stream supports this pattern with:
|
|
285
|
+
- `json_stream.load_many(...)`: yields each top-level JSON value as it is parsed.
|
|
286
|
+
- `json_stream.visit_many(...)`: visits each top-level JSON value and yields control after each one.
|
|
287
|
+
|
|
288
|
+
These functions are useful for common streaming formats:
|
|
289
|
+
- NDJSON (Newline-Delimited JSON, also known as JSON Lines, content-type often application/x-ndjson): one JSON value per line, separated by a single "\n".
|
|
290
|
+
- Concatenated or sequential JSON documents: complete JSON texts written back-to-back without delimiters.
|
|
291
|
+
|
|
292
|
+
Note about concatenated JSON without explicit delimiters:
|
|
293
|
+
|
|
294
|
+
When multiple top‑level JSON documents are simply concatenated with
|
|
295
|
+
no delimiters (no newlines or no spaces), json-stream can unambiguously
|
|
296
|
+
detect the document boundaries for the following top-level values:
|
|
297
|
+
- objects: `{ ... }`
|
|
298
|
+
- arrays: `[ ... ]`
|
|
299
|
+
- literals: `true`, `false`, `null`
|
|
300
|
+
|
|
301
|
+
However, numbers and strings require a delimiter between consecutive
|
|
302
|
+
documents so the tokenizer can tell where one ends and the next begins.
|
|
303
|
+
A delimiter can be as simple as a single whitespace or a newline.
|
|
304
|
+
|
|
305
|
+
Examples:
|
|
306
|
+
- Valid without delimiters (will parse as 2 docs):
|
|
307
|
+
- `{"a":1}{"b":2}` → `{...}`, `{...}`
|
|
308
|
+
- `[1][2]` → `[1]`, `[2]`
|
|
309
|
+
- `truefalse` → `true`, `false`
|
|
310
|
+
- `null[]` → `null`, `[]`
|
|
311
|
+
- These will error without a delimiter between documents (e.g. a space or newline):
|
|
312
|
+
- `3true`
|
|
313
|
+
- `0"x"`
|
|
314
|
+
- `"hi""there"
|
|
315
|
+
|
|
316
|
+
#### Examples
|
|
317
|
+
|
|
318
|
+
Read many from a file containing NDJSON or concatenated JSON:
|
|
319
|
+
```python
|
|
320
|
+
import json_stream
|
|
321
|
+
|
|
322
|
+
with open("events.ndjson", "rb") as f: # NDJSON: one JSON object per line
|
|
323
|
+
for item in json_stream.load_many(f):
|
|
324
|
+
# each item is the top-level item in each JSON text
|
|
325
|
+
handle(item)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Visit many values in many documents:
|
|
329
|
+
```python
|
|
330
|
+
import json_stream
|
|
331
|
+
|
|
332
|
+
def visitor(value, path):
|
|
333
|
+
# called depth-first for each value within the current top-level JSON text
|
|
334
|
+
...
|
|
335
|
+
|
|
336
|
+
for _ in json_stream.visit_many(open("events.ndjson", "rb"), visitor):
|
|
337
|
+
# loop advances one top-level JSON text at a time
|
|
338
|
+
pass
|
|
339
|
+
```
|
|
340
|
+
|
|
275
341
|
### <a id="urls"></a> Stream a URL
|
|
276
342
|
|
|
277
343
|
`json_stream` knows how to stream directly from a URL using a variety of packages.
|
|
@@ -378,8 +444,7 @@ with httpx.Client() as client, client.stream('GET', 'http://example.com/data.jso
|
|
|
378
444
|
|
|
379
445
|
### <a id="iterators"></a> Stream an iterable
|
|
380
446
|
|
|
381
|
-
`json-stream`'s parsing functions can take any iterable
|
|
382
|
-
`byte` objects.
|
|
447
|
+
`json-stream`'s parsing functions can take any iterable that produces encoded JSON chunks. The chunks can be `byte`s or `str`s.
|
|
383
448
|
|
|
384
449
|
```python
|
|
385
450
|
import json_stream
|
|
@@ -432,6 +497,21 @@ with JSONStreamEncoder():
|
|
|
432
497
|
some_library_function_out_of_your_control(data)
|
|
433
498
|
```
|
|
434
499
|
|
|
500
|
+
### Converting to standard Python types
|
|
501
|
+
|
|
502
|
+
To convert a json-stream `dict`-like or `list`-like object and all its
|
|
503
|
+
descendants to a standard `list` and `dict`, you can use the
|
|
504
|
+
`json_stream.to_standard_types` utility:
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
# JSON: {"round": 1, "results": [1, 2, 3]}
|
|
508
|
+
data = json_stream.load(f)
|
|
509
|
+
results = data["results"]
|
|
510
|
+
print(results) # prints <TransientStreamingJSONList: TRANSIENT, STREAMING>
|
|
511
|
+
converted = json_stream.to_standard_types(results)
|
|
512
|
+
print(converted) # prints [1, 2, 3]
|
|
513
|
+
```
|
|
514
|
+
|
|
435
515
|
#### Thread safety (experimental)
|
|
436
516
|
|
|
437
517
|
There is also a thread-safe version of the `json.dump` context manager:
|
|
@@ -682,23 +762,28 @@ parsing a simple JSON list as a stream.
|
|
|
682
762
|
|
|
683
763
|
## Yajl-Py
|
|
684
764
|
|
|
685
|
-
[Yajl-Py](https://pykler.github.io/yajl-py/) is a wrapper around the
|
|
765
|
+
[Yajl-Py](https://pykler.github.io/yajl-py/) is a wrapper around the C YAJL JSON library that can be used to
|
|
686
766
|
generate SAX style events while parsing JSON.
|
|
687
767
|
|
|
688
768
|
### Why not Yajl-Py?
|
|
689
769
|
|
|
690
|
-
*
|
|
770
|
+
* No pure python implementation
|
|
691
771
|
* It does not provide a pythonic `dict`/`list`-like interface
|
|
692
772
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
773
|
+
## jsonslicer
|
|
774
|
+
|
|
775
|
+
[jsonslicer](https://github.com/AMDmi3/jsonslicer) is another wrapper around the YAJL C library with a
|
|
776
|
+
path lookup based interface.
|
|
777
|
+
|
|
778
|
+
### Why not jsonslicer?
|
|
779
|
+
|
|
780
|
+
* No pure python implementation
|
|
781
|
+
* It does not provide a pythonic `dict`/`list`-like interface
|
|
782
|
+
* Must know all data paths lookup in advance (or make multiple passes)
|
|
783
|
+
|
|
784
|
+
# Contributing
|
|
785
|
+
|
|
786
|
+
See the project [contribution guide](https://github.com/daggaz/json-stream/blob/master/CONTRIBUTING.md).
|
|
702
787
|
|
|
703
788
|
# Donations
|
|
704
789
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# json-stream
|
|
2
2
|
|
|
3
3
|
[](https://github.com/daggaz/json-stream/actions/workflows/tests.yml)
|
|
4
|
+
[](https://codecov.io/gh/daggaz/json-stream)
|
|
4
5
|
[](https://pypi.org/project/json-stream)
|
|
5
6
|
[](https://pypi.org/project/json-stream/)
|
|
6
7
|
[](https://www.buymeacoffee.com/daggaz)
|
|
@@ -9,8 +10,8 @@ Simple streaming JSON parser and encoder.
|
|
|
9
10
|
|
|
10
11
|
When [reading](#reading) JSON data, `json-stream` can decode JSON data in
|
|
11
12
|
a streaming manner, providing a pythonic dict/list-like interface, or a
|
|
12
|
-
[visitor-based
|
|
13
|
-
or [iterators](#iterators).
|
|
13
|
+
[visitor-based interface](#visitor). It can stream from files, [URLs](#urls)
|
|
14
|
+
or [iterators](#iterators). It can process [multiple JSON documents](#multiple) in a single stream.
|
|
14
15
|
|
|
15
16
|
When [writing](#writing) JSON data, `json-stream` can stream JSON objects
|
|
16
17
|
as you generate them.
|
|
@@ -63,8 +64,8 @@ It is also possible to "mix" the modes as you consume the data.
|
|
|
63
64
|
|
|
64
65
|
This mode is appropriate if you can consume the data iteratively. You cannot
|
|
65
66
|
move backwards through the stream to read data that has already been skipped
|
|
66
|
-
over. It is the mode you **must** use if you want process large amounts of
|
|
67
|
-
JSON data without consuming large amounts of memory
|
|
67
|
+
over. It is the mode you **must** use if you want to process large amounts of
|
|
68
|
+
JSON data without consuming large amounts of memory.
|
|
68
69
|
|
|
69
70
|
In transient mode, only the data currently being read is stored in memory. Any
|
|
70
71
|
data previously read from the stream is discarded (it's up to you what to do
|
|
@@ -226,6 +227,65 @@ z at path ('xxxx', 3)
|
|
|
226
227
|
[] at path ('xxxx', 5)
|
|
227
228
|
```
|
|
228
229
|
|
|
230
|
+
### <a id="multiple"></a> Multiple JSON documents: `load_many()` and `visit_many()`
|
|
231
|
+
|
|
232
|
+
Sometimes JSON data arrives as a sequence of top‑level JSON texts rather than a single array/object. json-stream supports this pattern with:
|
|
233
|
+
- `json_stream.load_many(...)`: yields each top-level JSON value as it is parsed.
|
|
234
|
+
- `json_stream.visit_many(...)`: visits each top-level JSON value and yields control after each one.
|
|
235
|
+
|
|
236
|
+
These functions are useful for common streaming formats:
|
|
237
|
+
- NDJSON (Newline-Delimited JSON, also known as JSON Lines, content-type often application/x-ndjson): one JSON value per line, separated by a single "\n".
|
|
238
|
+
- Concatenated or sequential JSON documents: complete JSON texts written back-to-back without delimiters.
|
|
239
|
+
|
|
240
|
+
Note about concatenated JSON without explicit delimiters:
|
|
241
|
+
|
|
242
|
+
When multiple top‑level JSON documents are simply concatenated with
|
|
243
|
+
no delimiters (no newlines or no spaces), json-stream can unambiguously
|
|
244
|
+
detect the document boundaries for the following top-level values:
|
|
245
|
+
- objects: `{ ... }`
|
|
246
|
+
- arrays: `[ ... ]`
|
|
247
|
+
- literals: `true`, `false`, `null`
|
|
248
|
+
|
|
249
|
+
However, numbers and strings require a delimiter between consecutive
|
|
250
|
+
documents so the tokenizer can tell where one ends and the next begins.
|
|
251
|
+
A delimiter can be as simple as a single whitespace or a newline.
|
|
252
|
+
|
|
253
|
+
Examples:
|
|
254
|
+
- Valid without delimiters (will parse as 2 docs):
|
|
255
|
+
- `{"a":1}{"b":2}` → `{...}`, `{...}`
|
|
256
|
+
- `[1][2]` → `[1]`, `[2]`
|
|
257
|
+
- `truefalse` → `true`, `false`
|
|
258
|
+
- `null[]` → `null`, `[]`
|
|
259
|
+
- These will error without a delimiter between documents (e.g. a space or newline):
|
|
260
|
+
- `3true`
|
|
261
|
+
- `0"x"`
|
|
262
|
+
- `"hi""there"
|
|
263
|
+
|
|
264
|
+
#### Examples
|
|
265
|
+
|
|
266
|
+
Read many from a file containing NDJSON or concatenated JSON:
|
|
267
|
+
```python
|
|
268
|
+
import json_stream
|
|
269
|
+
|
|
270
|
+
with open("events.ndjson", "rb") as f: # NDJSON: one JSON object per line
|
|
271
|
+
for item in json_stream.load_many(f):
|
|
272
|
+
# each item is the top-level item in each JSON text
|
|
273
|
+
handle(item)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Visit many values in many documents:
|
|
277
|
+
```python
|
|
278
|
+
import json_stream
|
|
279
|
+
|
|
280
|
+
def visitor(value, path):
|
|
281
|
+
# called depth-first for each value within the current top-level JSON text
|
|
282
|
+
...
|
|
283
|
+
|
|
284
|
+
for _ in json_stream.visit_many(open("events.ndjson", "rb"), visitor):
|
|
285
|
+
# loop advances one top-level JSON text at a time
|
|
286
|
+
pass
|
|
287
|
+
```
|
|
288
|
+
|
|
229
289
|
### <a id="urls"></a> Stream a URL
|
|
230
290
|
|
|
231
291
|
`json_stream` knows how to stream directly from a URL using a variety of packages.
|
|
@@ -332,8 +392,7 @@ with httpx.Client() as client, client.stream('GET', 'http://example.com/data.jso
|
|
|
332
392
|
|
|
333
393
|
### <a id="iterators"></a> Stream an iterable
|
|
334
394
|
|
|
335
|
-
`json-stream`'s parsing functions can take any iterable
|
|
336
|
-
`byte` objects.
|
|
395
|
+
`json-stream`'s parsing functions can take any iterable that produces encoded JSON chunks. The chunks can be `byte`s or `str`s.
|
|
337
396
|
|
|
338
397
|
```python
|
|
339
398
|
import json_stream
|
|
@@ -386,6 +445,21 @@ with JSONStreamEncoder():
|
|
|
386
445
|
some_library_function_out_of_your_control(data)
|
|
387
446
|
```
|
|
388
447
|
|
|
448
|
+
### Converting to standard Python types
|
|
449
|
+
|
|
450
|
+
To convert a json-stream `dict`-like or `list`-like object and all its
|
|
451
|
+
descendants to a standard `list` and `dict`, you can use the
|
|
452
|
+
`json_stream.to_standard_types` utility:
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
# JSON: {"round": 1, "results": [1, 2, 3]}
|
|
456
|
+
data = json_stream.load(f)
|
|
457
|
+
results = data["results"]
|
|
458
|
+
print(results) # prints <TransientStreamingJSONList: TRANSIENT, STREAMING>
|
|
459
|
+
converted = json_stream.to_standard_types(results)
|
|
460
|
+
print(converted) # prints [1, 2, 3]
|
|
461
|
+
```
|
|
462
|
+
|
|
389
463
|
#### Thread safety (experimental)
|
|
390
464
|
|
|
391
465
|
There is also a thread-safe version of the `json.dump` context manager:
|
|
@@ -636,23 +710,28 @@ parsing a simple JSON list as a stream.
|
|
|
636
710
|
|
|
637
711
|
## Yajl-Py
|
|
638
712
|
|
|
639
|
-
[Yajl-Py](https://pykler.github.io/yajl-py/) is a wrapper around the
|
|
713
|
+
[Yajl-Py](https://pykler.github.io/yajl-py/) is a wrapper around the C YAJL JSON library that can be used to
|
|
640
714
|
generate SAX style events while parsing JSON.
|
|
641
715
|
|
|
642
716
|
### Why not Yajl-Py?
|
|
643
717
|
|
|
644
|
-
*
|
|
718
|
+
* No pure python implementation
|
|
645
719
|
* It does not provide a pythonic `dict`/`list`-like interface
|
|
646
720
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
721
|
+
## jsonslicer
|
|
722
|
+
|
|
723
|
+
[jsonslicer](https://github.com/AMDmi3/jsonslicer) is another wrapper around the YAJL C library with a
|
|
724
|
+
path lookup based interface.
|
|
725
|
+
|
|
726
|
+
### Why not jsonslicer?
|
|
727
|
+
|
|
728
|
+
* No pure python implementation
|
|
729
|
+
* It does not provide a pythonic `dict`/`list`-like interface
|
|
730
|
+
* Must know all data paths lookup in advance (or make multiple passes)
|
|
731
|
+
|
|
732
|
+
# Contributing
|
|
733
|
+
|
|
734
|
+
See the project [contribution guide](https://github.com/daggaz/json-stream/blob/master/CONTRIBUTING.md).
|
|
656
735
|
|
|
657
736
|
# Donations
|
|
658
737
|
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "json-stream"
|
|
8
|
-
version = "2.1
|
|
8
|
+
version = "2.4.1"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Jamie Cockburn", email="jamie_cockburn@hotmail.co.uk"},
|
|
11
11
|
]
|
|
@@ -18,14 +18,16 @@ classifiers = [
|
|
|
18
18
|
"Topic :: Software Development :: Libraries",
|
|
19
19
|
"License :: OSI Approved :: MIT License",
|
|
20
20
|
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
-
"Programming Language :: Python :: 3.7",
|
|
22
21
|
"Programming Language :: Python :: 3.8",
|
|
23
22
|
"Programming Language :: Python :: 3.9",
|
|
24
23
|
"Programming Language :: Python :: 3.10",
|
|
25
24
|
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
26
28
|
]
|
|
27
29
|
requires-python = ">=3.5,<4"
|
|
28
|
-
dependencies = ["json-stream-rs-tokenizer >= 0.4.
|
|
30
|
+
dependencies = ["json-stream-rs-tokenizer >= 0.4.17"]
|
|
29
31
|
dynamic = ["readme"]
|
|
30
32
|
|
|
31
33
|
[project.urls]
|
|
@@ -3,7 +3,7 @@ import copy
|
|
|
3
3
|
from abc import ABC
|
|
4
4
|
from collections import OrderedDict
|
|
5
5
|
from itertools import chain
|
|
6
|
-
from typing import Optional, Iterator, Any
|
|
6
|
+
from typing import Optional, Iterator, Any, Mapping, Sequence
|
|
7
7
|
|
|
8
8
|
from json_stream.tokenizer import TokenType
|
|
9
9
|
|
|
@@ -13,6 +13,8 @@ class TransientAccessException(Exception):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class StreamingJSONBase(ABC):
|
|
16
|
+
INCOMPLETE_ERROR = "Unexpected end of file"
|
|
17
|
+
|
|
16
18
|
@classmethod
|
|
17
19
|
def factory(cls, token, token_stream, persistent):
|
|
18
20
|
if persistent:
|
|
@@ -47,6 +49,8 @@ class StreamingJSONBase(ABC):
|
|
|
47
49
|
try:
|
|
48
50
|
item = self._load_item()
|
|
49
51
|
except StopIteration:
|
|
52
|
+
if self.streaming:
|
|
53
|
+
raise ValueError(self.INCOMPLETE_ERROR)
|
|
50
54
|
return
|
|
51
55
|
yield item
|
|
52
56
|
|
|
@@ -134,6 +138,8 @@ class TransientStreamingJSONBase(StreamingJSONBase, ABC):
|
|
|
134
138
|
|
|
135
139
|
|
|
136
140
|
class StreamingJSONList(StreamingJSONBase, ABC):
|
|
141
|
+
INCOMPLETE_ERROR = "Unterminated list at end of file"
|
|
142
|
+
|
|
137
143
|
def _load_item(self):
|
|
138
144
|
token_type, v = next(self._stream)
|
|
139
145
|
if token_type == TokenType.OPERATOR:
|
|
@@ -153,6 +159,7 @@ class StreamingJSONList(StreamingJSONBase, ABC):
|
|
|
153
159
|
return self._iter_items()
|
|
154
160
|
|
|
155
161
|
|
|
162
|
+
@Sequence.register
|
|
156
163
|
class PersistentStreamingJSONList(PersistentStreamingJSONBase, StreamingJSONList):
|
|
157
164
|
def _init_persistent_data(self):
|
|
158
165
|
return []
|
|
@@ -178,6 +185,7 @@ class PersistentStreamingJSONList(PersistentStreamingJSONBase, StreamingJSONList
|
|
|
178
185
|
return self._find_item(k)
|
|
179
186
|
|
|
180
187
|
|
|
188
|
+
@Sequence.register
|
|
181
189
|
class TransientStreamingJSONList(TransientStreamingJSONBase, StreamingJSONList):
|
|
182
190
|
def __init__(self, token_stream):
|
|
183
191
|
super().__init__(token_stream)
|
|
@@ -198,6 +206,8 @@ class TransientStreamingJSONList(TransientStreamingJSONBase, StreamingJSONList):
|
|
|
198
206
|
|
|
199
207
|
|
|
200
208
|
class StreamingJSONObject(StreamingJSONBase, ABC):
|
|
209
|
+
INCOMPLETE_ERROR = "Unterminated object at end of file"
|
|
210
|
+
|
|
201
211
|
def _load_item(self):
|
|
202
212
|
token_type, k = next(self._stream)
|
|
203
213
|
if token_type == TokenType.OPERATOR:
|
|
@@ -242,6 +252,7 @@ class StreamingJSONObject(StreamingJSONBase, ABC):
|
|
|
242
252
|
return default
|
|
243
253
|
|
|
244
254
|
|
|
255
|
+
@Mapping.register
|
|
245
256
|
class PersistentStreamingJSONObject(PersistentStreamingJSONBase, StreamingJSONObject):
|
|
246
257
|
def _init_persistent_data(self):
|
|
247
258
|
return OrderedDict()
|
|
@@ -268,6 +279,7 @@ class PersistentStreamingJSONObject(PersistentStreamingJSONBase, StreamingJSONOb
|
|
|
268
279
|
return self._find_item(k)
|
|
269
280
|
|
|
270
281
|
|
|
282
|
+
@Mapping.register
|
|
271
283
|
class TransientStreamingJSONObject(TransientStreamingJSONBase, StreamingJSONObject):
|
|
272
284
|
def _find_item(self, k):
|
|
273
285
|
was_started = self._started
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
-
from json_stream
|
|
3
|
+
from json_stream import to_standard_types
|
|
4
|
+
from json_stream.base import StreamingJSONBase
|
|
4
5
|
|
|
5
6
|
_original_default = json.JSONEncoder().default
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class JSONStreamEncoder(json.JSONEncoder):
|
|
9
10
|
def default(self, obj):
|
|
10
|
-
if isinstance(obj,
|
|
11
|
-
return
|
|
12
|
-
if isinstance(obj, StreamingJSONList):
|
|
13
|
-
return list(obj)
|
|
11
|
+
if isinstance(obj, StreamingJSONBase):
|
|
12
|
+
return to_standard_types(obj)
|
|
14
13
|
return _original_default(obj)
|
|
15
14
|
|
|
16
15
|
def __enter__(self):
|
|
@@ -10,16 +10,32 @@ class TestDump(TestCase):
|
|
|
10
10
|
JSON = '{"count": 3, "results": ["a", "b", "c"]}'
|
|
11
11
|
|
|
12
12
|
def test_dump_default(self):
|
|
13
|
-
data = json_stream.load(StringIO(self.JSON)
|
|
13
|
+
data = json_stream.load(StringIO(self.JSON))
|
|
14
14
|
output = json.dumps(data, default=default)
|
|
15
15
|
self._assert_json_okay(output)
|
|
16
16
|
|
|
17
17
|
def test_dump_cls(self):
|
|
18
|
-
data = json_stream.load(StringIO(self.JSON)
|
|
18
|
+
data = json_stream.load(StringIO(self.JSON))
|
|
19
19
|
output = json.dumps(data, cls=JSONStreamEncoder)
|
|
20
20
|
self._assert_json_okay(output)
|
|
21
21
|
|
|
22
22
|
def test_dump_context(self):
|
|
23
|
+
data = json_stream.load(StringIO(self.JSON))
|
|
24
|
+
with JSONStreamEncoder():
|
|
25
|
+
output = json.dumps(data)
|
|
26
|
+
self._assert_json_okay(output)
|
|
27
|
+
|
|
28
|
+
def test_dump_default_persistent(self):
|
|
29
|
+
data = json_stream.load(StringIO(self.JSON), persistent=True)
|
|
30
|
+
output = json.dumps(data, default=default)
|
|
31
|
+
self._assert_json_okay(output)
|
|
32
|
+
|
|
33
|
+
def test_dump_cls_persistent(self):
|
|
34
|
+
data = json_stream.load(StringIO(self.JSON), persistent=True)
|
|
35
|
+
output = json.dumps(data, cls=JSONStreamEncoder)
|
|
36
|
+
self._assert_json_okay(output)
|
|
37
|
+
|
|
38
|
+
def test_dump_context_persistent(self):
|
|
23
39
|
data = json_stream.load(StringIO(self.JSON), persistent=True)
|
|
24
40
|
with JSONStreamEncoder():
|
|
25
41
|
output = json.dumps(data)
|
|
@@ -13,5 +13,13 @@ def load(response, persistent=False, tokenizer=default_tokenizer, chunk_size=CON
|
|
|
13
13
|
return json_stream.load(_to_iterable(response, chunk_size), persistent=persistent, tokenizer=tokenizer)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def load_many(response, persistent=False, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
17
|
+
return json_stream.load_many(_to_iterable(response, chunk_size), persistent=persistent, tokenizer=tokenizer)
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
def visit(response, visitor, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
17
21
|
return json_stream.visit(_to_iterable(response, chunk_size), visitor, tokenizer=tokenizer)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def visit_many(response, visitor, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
25
|
+
return json_stream.visit_many(_to_iterable(response, chunk_size), visitor, tokenizer=tokenizer)
|
|
@@ -4,7 +4,8 @@ from itertools import zip_longest
|
|
|
4
4
|
from unittest import TestCase
|
|
5
5
|
from unittest.mock import Mock
|
|
6
6
|
|
|
7
|
-
from json_stream
|
|
7
|
+
from json_stream import to_standard_types
|
|
8
|
+
from json_stream.httpx import load, visit, load_many, visit_many
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class TestLoad(TestCase):
|
|
@@ -16,13 +17,14 @@ class TestLoad(TestCase):
|
|
|
16
17
|
args = [iter(iterable)] * n
|
|
17
18
|
return zip_longest(*args, fillvalue="")
|
|
18
19
|
|
|
19
|
-
def _create_mock_response(self):
|
|
20
|
+
def _create_mock_response(self, data=None):
|
|
20
21
|
# httpx iter_bytes returns an iterable of bytes
|
|
21
22
|
response = Mock()
|
|
22
|
-
data
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if data is None:
|
|
24
|
+
data = json.dumps({
|
|
25
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
26
|
+
"b": "b",
|
|
27
|
+
})
|
|
26
28
|
content = ("".join(chunk).encode() for chunk in self.grouper(data, 1024))
|
|
27
29
|
response.iter_bytes.return_value = content
|
|
28
30
|
return response
|
|
@@ -69,3 +71,37 @@ class TestLoad(TestCase):
|
|
|
69
71
|
('a' * io.DEFAULT_BUFFER_SIZE, ('a',)),
|
|
70
72
|
('b', ('b',)),
|
|
71
73
|
], visited)
|
|
74
|
+
|
|
75
|
+
def test_load_many(self):
|
|
76
|
+
expected = [{
|
|
77
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
78
|
+
}, {
|
|
79
|
+
"b": "b" * io.DEFAULT_BUFFER_SIZE,
|
|
80
|
+
}]
|
|
81
|
+
data = "".join(json.dumps(i) for i in expected)
|
|
82
|
+
response = self._create_mock_response(data=data)
|
|
83
|
+
count = 0
|
|
84
|
+
for item, exp in zip(load_many(response), expected):
|
|
85
|
+
self.assertEqual(exp, to_standard_types(item))
|
|
86
|
+
count += 1
|
|
87
|
+
self.assertEqual(count, len(expected))
|
|
88
|
+
|
|
89
|
+
def test_visit_many(self):
|
|
90
|
+
items = [{
|
|
91
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
92
|
+
}, {
|
|
93
|
+
"b": "b" * io.DEFAULT_BUFFER_SIZE,
|
|
94
|
+
}]
|
|
95
|
+
data = "".join(json.dumps(e) for e in items)
|
|
96
|
+
response = self._create_mock_response(data=data)
|
|
97
|
+
current_visit = []
|
|
98
|
+
visits = []
|
|
99
|
+
|
|
100
|
+
def visitor(item, path):
|
|
101
|
+
current_visit.append((item, path))
|
|
102
|
+
for _ in visit_many(response, visitor):
|
|
103
|
+
visits.append(current_visit)
|
|
104
|
+
current_visit = []
|
|
105
|
+
self.assertEqual(len(visits), len(items))
|
|
106
|
+
self.assertEqual(visits[0], [('a' * io.DEFAULT_BUFFER_SIZE, ('a',))])
|
|
107
|
+
self.assertEqual(visits[1], [('b' * io.DEFAULT_BUFFER_SIZE, ('b',))])
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import io
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IterableStream(io.RawIOBase):
|
|
5
|
+
def __init__(self, iterable):
|
|
6
|
+
self.iterator = iter(iterable)
|
|
7
|
+
self.remainder = None # type: bytes | None
|
|
8
|
+
|
|
9
|
+
def _normalize_chunk(self, chunk):
|
|
10
|
+
# Ensure chunk is bytes for writing into a binary buffer
|
|
11
|
+
if isinstance(chunk, str):
|
|
12
|
+
return chunk.encode()
|
|
13
|
+
return chunk
|
|
14
|
+
|
|
15
|
+
def readinto(self, buffer):
|
|
16
|
+
try:
|
|
17
|
+
# Wrap `buffer: WriteableBuffer` in memoryview to ensure len() and slicing
|
|
18
|
+
mv = memoryview(buffer)
|
|
19
|
+
chunk = self.remainder or self._normalize_chunk(next(self.iterator))
|
|
20
|
+
length = min(len(mv), len(chunk))
|
|
21
|
+
mv[:length], self.remainder = chunk[:length], chunk[length:]
|
|
22
|
+
return length
|
|
23
|
+
except StopIteration:
|
|
24
|
+
return 0 # indicate EOF
|
|
25
|
+
|
|
26
|
+
def readable(self):
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ensure_file(fp_or_iterable):
|
|
31
|
+
if hasattr(fp_or_iterable, 'read'):
|
|
32
|
+
return fp_or_iterable
|
|
33
|
+
return IterableStream(fp_or_iterable) # will raise TypeError if not iterable
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from json_stream.base import StreamingJSONBase, TokenType
|
|
2
|
+
from json_stream.iterators import ensure_file
|
|
3
|
+
from json_stream.select_tokenizer import default_tokenizer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load(fp_or_iterable, persistent=False, tokenizer=default_tokenizer):
|
|
7
|
+
return next(load_many(fp_or_iterable, persistent, tokenizer))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_many(fp_or_iterable, persistent=False, tokenizer=default_tokenizer):
|
|
11
|
+
fp = ensure_file(fp_or_iterable)
|
|
12
|
+
token_stream = tokenizer(fp)
|
|
13
|
+
for token_type, token in token_stream:
|
|
14
|
+
if token_type == TokenType.OPERATOR:
|
|
15
|
+
data = StreamingJSONBase.factory(token, token_stream, persistent)
|
|
16
|
+
yield data
|
|
17
|
+
data.read_all()
|
|
18
|
+
else:
|
|
19
|
+
yield token
|