json-stream 2.4.0__tar.gz → 2.5.0__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.4.0/src/json_stream.egg-info → json_stream-2.5.0}/PKG-INFO +74 -5
- {json_stream-2.4.0 → json_stream-2.5.0}/README.md +73 -4
- {json_stream-2.4.0 → json_stream-2.5.0}/pyproject.toml +1 -1
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/base.py +4 -0
- json_stream-2.5.0/src/json_stream/iterators.py +33 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/loader.py +4 -4
- json_stream-2.5.0/src/json_stream/tests/test_binary_resumption.py +97 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_iterators.py +13 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/visitor.py +4 -4
- {json_stream-2.4.0 → json_stream-2.5.0/src/json_stream.egg-info}/PKG-INFO +74 -5
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream.egg-info/SOURCES.txt +1 -0
- json_stream-2.4.0/src/json_stream/iterators.py +0 -25
- {json_stream-2.4.0 → json_stream-2.5.0}/LICENSE.txt +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/setup.cfg +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/__init__.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/dump/__init__.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/dump/tests/__init__.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/dump/tests/test_dump.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/dump/threading.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/httpx/__init__.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/httpx/tests/__init__.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/httpx/tests/test_httpx.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/requests/__init__.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/requests/tests/__init__.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/requests/tests/test_requests.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/select_tokenizer.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/__init__.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_buffering.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_collection_abcs.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_load_many.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_loader.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_tokenizer.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_tokenizer_integration.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_util.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_visit_many.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_visitor.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_writer.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tokenizer.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/util.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/writer.py +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream.egg-info/dependency_links.txt +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream.egg-info/requires.txt +0 -0
- {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: json-stream
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
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
|
|
@@ -53,6 +53,7 @@ Dynamic: license-file
|
|
|
53
53
|
# json-stream
|
|
54
54
|
|
|
55
55
|
[](https://github.com/daggaz/json-stream/actions/workflows/tests.yml)
|
|
56
|
+
[](https://codecov.io/gh/daggaz/json-stream)
|
|
56
57
|
[](https://pypi.org/project/json-stream)
|
|
57
58
|
[](https://pypi.org/project/json-stream/)
|
|
58
59
|
[](https://www.buymeacoffee.com/daggaz)
|
|
@@ -62,7 +63,8 @@ Simple streaming JSON parser and encoder.
|
|
|
62
63
|
When [reading](#reading) JSON data, `json-stream` can decode JSON data in
|
|
63
64
|
a streaming manner, providing a pythonic dict/list-like interface, or a
|
|
64
65
|
[visitor-based interface](#visitor). It can stream from files, [URLs](#urls)
|
|
65
|
-
or [iterators](#iterators). It can process [multiple JSON documents](#multiple)
|
|
66
|
+
or [iterators](#iterators). It can process [multiple JSON documents](#multiple)
|
|
67
|
+
in a single stream, and can read JSON [mixed with other non-JSON data](#reading-mixed-data).
|
|
66
68
|
|
|
67
69
|
When [writing](#writing) JSON data, `json-stream` can stream JSON objects
|
|
68
70
|
as you generate them.
|
|
@@ -443,8 +445,7 @@ with httpx.Client() as client, client.stream('GET', 'http://example.com/data.jso
|
|
|
443
445
|
|
|
444
446
|
### <a id="iterators"></a> Stream an iterable
|
|
445
447
|
|
|
446
|
-
`json-stream`'s parsing functions can take any iterable
|
|
447
|
-
`byte` objects.
|
|
448
|
+
`json-stream`'s parsing functions can take any iterable that produces encoded JSON chunks. The chunks can be `byte`s or `str`s.
|
|
448
449
|
|
|
449
450
|
```python
|
|
450
451
|
import json_stream
|
|
@@ -547,13 +548,81 @@ significant parsing speedup compared to pure python implementation.
|
|
|
547
548
|
`json-stream` will fallback to its pure python tokenizer implementation
|
|
548
549
|
if `json-stream-rs-tokenizer` is not available.
|
|
549
550
|
|
|
551
|
+
#### <a id="reading-mixed-data"></a> Reading mixed data
|
|
552
|
+
|
|
553
|
+
When using the Rust tokenizer, you can also use `json-stream` to parse mixed
|
|
554
|
+
data, for example a file with a JSON followed by binary data.
|
|
555
|
+
|
|
556
|
+
To do this, you should pass `correct_cursor=True` to `load()`. The ensures the
|
|
557
|
+
rust tokenizer keeps track of the exact stream position it has read up to. This
|
|
558
|
+
comes with a **significant performance cost** for un-seekable streams.
|
|
559
|
+
|
|
560
|
+
After reading the JSON data, call `read_all()` on the top-level object returned
|
|
561
|
+
by `load()` to ensure you have read up to the end of the JSON data, and then call
|
|
562
|
+
`.tokenizer.park_cursor()` to "park" the underlying file cursor at the correct
|
|
563
|
+
position.
|
|
564
|
+
|
|
565
|
+
```python
|
|
566
|
+
import json_stream
|
|
567
|
+
|
|
568
|
+
with open('test.bin', 'rb') as f:
|
|
569
|
+
# read JSON header
|
|
570
|
+
header = json_stream.load(f, correct_cursor=True)
|
|
571
|
+
# ... process JSON header ...
|
|
572
|
+
header.read_all()
|
|
573
|
+
|
|
574
|
+
# ensure the tokenizer has "parked" the file
|
|
575
|
+
# cursor at the end of the JSON data
|
|
576
|
+
header.tokenizer.park_cursor()
|
|
577
|
+
|
|
578
|
+
# now we can read binary data from the same file
|
|
579
|
+
binary_start = f.tell()
|
|
580
|
+
data = f.read()
|
|
581
|
+
|
|
582
|
+
#### <a id="mixed-scenarios"></a> Other mixed data scenarios
|
|
583
|
+
|
|
584
|
+
`json-stream` can also handle streams that start with binary data, or have binary
|
|
585
|
+
data between multiple JSON documents.
|
|
586
|
+
|
|
587
|
+
##### Binary then JSON
|
|
588
|
+
|
|
589
|
+
You can simply read the binary data from the file before calling `load()`.
|
|
590
|
+
|
|
591
|
+
```python
|
|
592
|
+
with open('test.bin', 'rb') as f:
|
|
593
|
+
binary_data = f.read(1024)
|
|
594
|
+
data = json_stream.load(f)
|
|
595
|
+
# ... process JSON ...
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
##### JSON then binary then JSON
|
|
599
|
+
|
|
600
|
+
You must use `correct_cursor=True` for any JSON document that is followed by
|
|
601
|
+
binary data.
|
|
602
|
+
|
|
603
|
+
```python
|
|
604
|
+
with open('test.bin', 'rb') as f:
|
|
605
|
+
# 1. Read first JSON
|
|
606
|
+
data1 = json_stream.load(f, correct_cursor=True)
|
|
607
|
+
# ... process data1 ...
|
|
608
|
+
data1.read_all()
|
|
609
|
+
data1.tokenizer.park_cursor()
|
|
610
|
+
|
|
611
|
+
# 2. Read binary data
|
|
612
|
+
binary_data = f.read(1024)
|
|
613
|
+
|
|
614
|
+
# 3. Read second JSON
|
|
615
|
+
data2 = json_stream.load(f)
|
|
616
|
+
# ... process data2 ...
|
|
617
|
+
```
|
|
618
|
+
|
|
550
619
|
### Custom tokenizer
|
|
551
620
|
|
|
552
621
|
You can supply an alternative JSON tokenizer implementation. Simply pass
|
|
553
622
|
a tokenizer to the `load()` or `visit()` methods.
|
|
554
623
|
|
|
555
624
|
```python
|
|
556
|
-
json_stream.load(f, tokenizer=some_tokenizer)
|
|
625
|
+
json_stream.load(f, tokenizer=some_tokenizer, **tokenizer_kwargs)
|
|
557
626
|
```
|
|
558
627
|
|
|
559
628
|
The requests methods also accept a customer tokenizer parameter.
|
|
@@ -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)
|
|
@@ -10,7 +11,8 @@ Simple streaming JSON parser and encoder.
|
|
|
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
13
|
[visitor-based interface](#visitor). It can stream from files, [URLs](#urls)
|
|
13
|
-
or [iterators](#iterators). It can process [multiple JSON documents](#multiple)
|
|
14
|
+
or [iterators](#iterators). It can process [multiple JSON documents](#multiple)
|
|
15
|
+
in a single stream, and can read JSON [mixed with other non-JSON data](#reading-mixed-data).
|
|
14
16
|
|
|
15
17
|
When [writing](#writing) JSON data, `json-stream` can stream JSON objects
|
|
16
18
|
as you generate them.
|
|
@@ -391,8 +393,7 @@ with httpx.Client() as client, client.stream('GET', 'http://example.com/data.jso
|
|
|
391
393
|
|
|
392
394
|
### <a id="iterators"></a> Stream an iterable
|
|
393
395
|
|
|
394
|
-
`json-stream`'s parsing functions can take any iterable
|
|
395
|
-
`byte` objects.
|
|
396
|
+
`json-stream`'s parsing functions can take any iterable that produces encoded JSON chunks. The chunks can be `byte`s or `str`s.
|
|
396
397
|
|
|
397
398
|
```python
|
|
398
399
|
import json_stream
|
|
@@ -495,13 +496,81 @@ significant parsing speedup compared to pure python implementation.
|
|
|
495
496
|
`json-stream` will fallback to its pure python tokenizer implementation
|
|
496
497
|
if `json-stream-rs-tokenizer` is not available.
|
|
497
498
|
|
|
499
|
+
#### <a id="reading-mixed-data"></a> Reading mixed data
|
|
500
|
+
|
|
501
|
+
When using the Rust tokenizer, you can also use `json-stream` to parse mixed
|
|
502
|
+
data, for example a file with a JSON followed by binary data.
|
|
503
|
+
|
|
504
|
+
To do this, you should pass `correct_cursor=True` to `load()`. The ensures the
|
|
505
|
+
rust tokenizer keeps track of the exact stream position it has read up to. This
|
|
506
|
+
comes with a **significant performance cost** for un-seekable streams.
|
|
507
|
+
|
|
508
|
+
After reading the JSON data, call `read_all()` on the top-level object returned
|
|
509
|
+
by `load()` to ensure you have read up to the end of the JSON data, and then call
|
|
510
|
+
`.tokenizer.park_cursor()` to "park" the underlying file cursor at the correct
|
|
511
|
+
position.
|
|
512
|
+
|
|
513
|
+
```python
|
|
514
|
+
import json_stream
|
|
515
|
+
|
|
516
|
+
with open('test.bin', 'rb') as f:
|
|
517
|
+
# read JSON header
|
|
518
|
+
header = json_stream.load(f, correct_cursor=True)
|
|
519
|
+
# ... process JSON header ...
|
|
520
|
+
header.read_all()
|
|
521
|
+
|
|
522
|
+
# ensure the tokenizer has "parked" the file
|
|
523
|
+
# cursor at the end of the JSON data
|
|
524
|
+
header.tokenizer.park_cursor()
|
|
525
|
+
|
|
526
|
+
# now we can read binary data from the same file
|
|
527
|
+
binary_start = f.tell()
|
|
528
|
+
data = f.read()
|
|
529
|
+
|
|
530
|
+
#### <a id="mixed-scenarios"></a> Other mixed data scenarios
|
|
531
|
+
|
|
532
|
+
`json-stream` can also handle streams that start with binary data, or have binary
|
|
533
|
+
data between multiple JSON documents.
|
|
534
|
+
|
|
535
|
+
##### Binary then JSON
|
|
536
|
+
|
|
537
|
+
You can simply read the binary data from the file before calling `load()`.
|
|
538
|
+
|
|
539
|
+
```python
|
|
540
|
+
with open('test.bin', 'rb') as f:
|
|
541
|
+
binary_data = f.read(1024)
|
|
542
|
+
data = json_stream.load(f)
|
|
543
|
+
# ... process JSON ...
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
##### JSON then binary then JSON
|
|
547
|
+
|
|
548
|
+
You must use `correct_cursor=True` for any JSON document that is followed by
|
|
549
|
+
binary data.
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
with open('test.bin', 'rb') as f:
|
|
553
|
+
# 1. Read first JSON
|
|
554
|
+
data1 = json_stream.load(f, correct_cursor=True)
|
|
555
|
+
# ... process data1 ...
|
|
556
|
+
data1.read_all()
|
|
557
|
+
data1.tokenizer.park_cursor()
|
|
558
|
+
|
|
559
|
+
# 2. Read binary data
|
|
560
|
+
binary_data = f.read(1024)
|
|
561
|
+
|
|
562
|
+
# 3. Read second JSON
|
|
563
|
+
data2 = json_stream.load(f)
|
|
564
|
+
# ... process data2 ...
|
|
565
|
+
```
|
|
566
|
+
|
|
498
567
|
### Custom tokenizer
|
|
499
568
|
|
|
500
569
|
You can supply an alternative JSON tokenizer implementation. Simply pass
|
|
501
570
|
a tokenizer to the `load()` or `visit()` methods.
|
|
502
571
|
|
|
503
572
|
```python
|
|
504
|
-
json_stream.load(f, tokenizer=some_tokenizer)
|
|
573
|
+
json_stream.load(f, tokenizer=some_tokenizer, **tokenizer_kwargs)
|
|
505
574
|
```
|
|
506
575
|
|
|
507
576
|
The requests methods also accept a customer tokenizer parameter.
|
|
@@ -36,6 +36,10 @@ class StreamingJSONBase(ABC):
|
|
|
36
36
|
self._stream = token_stream
|
|
37
37
|
self._child: Optional[StreamingJSONBase] = None
|
|
38
38
|
|
|
39
|
+
@property
|
|
40
|
+
def tokenizer(self):
|
|
41
|
+
return self._stream
|
|
42
|
+
|
|
39
43
|
def _clear_child(self):
|
|
40
44
|
if self._child is not None:
|
|
41
45
|
self._child.read_all()
|
|
@@ -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
|
|
@@ -3,13 +3,13 @@ from json_stream.iterators import ensure_file
|
|
|
3
3
|
from json_stream.select_tokenizer import default_tokenizer
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def load(fp_or_iterable, persistent=False, tokenizer=default_tokenizer):
|
|
7
|
-
return next(load_many(fp_or_iterable, persistent, tokenizer))
|
|
6
|
+
def load(fp_or_iterable, persistent=False, tokenizer=default_tokenizer, **tokenizer_kwargs):
|
|
7
|
+
return next(load_many(fp_or_iterable, persistent, tokenizer, **tokenizer_kwargs))
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def load_many(fp_or_iterable, persistent=False, tokenizer=default_tokenizer):
|
|
10
|
+
def load_many(fp_or_iterable, persistent=False, tokenizer=default_tokenizer, **tokenizer_kwargs):
|
|
11
11
|
fp = ensure_file(fp_or_iterable)
|
|
12
|
-
token_stream = tokenizer(fp)
|
|
12
|
+
token_stream = tokenizer(fp, **tokenizer_kwargs)
|
|
13
13
|
for token_type, token in token_stream:
|
|
14
14
|
if token_type == TokenType.OPERATOR:
|
|
15
15
|
data = StreamingJSONBase.factory(token, token_stream, persistent)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
from unittest import TestCase, skipUnless
|
|
4
|
+
import json_stream
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import json_stream_rs_tokenizer
|
|
8
|
+
HAS_RS_TOKENIZER = hasattr(json_stream_rs_tokenizer, 'RustTokenizer')
|
|
9
|
+
except ImportError:
|
|
10
|
+
HAS_RS_TOKENIZER = False
|
|
11
|
+
|
|
12
|
+
class TestBinaryResumption(TestCase):
|
|
13
|
+
@skipUnless(HAS_RS_TOKENIZER, 'Rust tokenizer not available')
|
|
14
|
+
def test_json_then_binary(self):
|
|
15
|
+
json_header = json.dumps({"header": "info"})
|
|
16
|
+
binary_data = b'\x00\x01\x02\x03'
|
|
17
|
+
test_data = json_header.encode('utf-8') + binary_data
|
|
18
|
+
test_file = io.BytesIO(test_data)
|
|
19
|
+
|
|
20
|
+
# Load with correct_cursor=True
|
|
21
|
+
header = json_stream.load(test_file, correct_cursor=True)
|
|
22
|
+
|
|
23
|
+
# Consume all data from header
|
|
24
|
+
header.read_all()
|
|
25
|
+
|
|
26
|
+
# Signal that we are done with JSON and want to resume binary read
|
|
27
|
+
header.tokenizer.park_cursor()
|
|
28
|
+
|
|
29
|
+
# Verify file cursor position
|
|
30
|
+
self.assertEqual(test_file.tell(), len(json_header))
|
|
31
|
+
|
|
32
|
+
# Verify binary data
|
|
33
|
+
remaining = test_file.read()
|
|
34
|
+
self.assertEqual(remaining, binary_data)
|
|
35
|
+
|
|
36
|
+
@skipUnless(HAS_RS_TOKENIZER, 'Rust tokenizer not available')
|
|
37
|
+
def test_binary_then_json(self):
|
|
38
|
+
binary_data = b'binary_start'
|
|
39
|
+
json_data = b'{"a": 1}'
|
|
40
|
+
test_data = binary_data + json_data
|
|
41
|
+
test_file = io.BytesIO(test_data)
|
|
42
|
+
|
|
43
|
+
# Read binary
|
|
44
|
+
read_binary = test_file.read(len(binary_data))
|
|
45
|
+
self.assertEqual(read_binary, binary_data)
|
|
46
|
+
|
|
47
|
+
# Load JSON
|
|
48
|
+
data = json_stream.load(test_file)
|
|
49
|
+
self.assertEqual(dict(data.items()), {"a": 1})
|
|
50
|
+
|
|
51
|
+
@skipUnless(HAS_RS_TOKENIZER, 'Rust tokenizer not available')
|
|
52
|
+
def test_json_then_binary_then_json(self):
|
|
53
|
+
json_1 = b'{"first": true}'
|
|
54
|
+
binary_middle = b'middle_binary'
|
|
55
|
+
json_2 = b'{"second": false}'
|
|
56
|
+
test_data = json_1 + binary_middle + json_2
|
|
57
|
+
test_file = io.BytesIO(test_data)
|
|
58
|
+
|
|
59
|
+
# Load first JSON
|
|
60
|
+
data1 = json_stream.load(test_file, correct_cursor=True)
|
|
61
|
+
self.assertEqual(dict(data1.items()), {"first": True})
|
|
62
|
+
data1.read_all()
|
|
63
|
+
data1.tokenizer.park_cursor()
|
|
64
|
+
self.assertEqual(test_file.tell(), len(json_1))
|
|
65
|
+
|
|
66
|
+
# Read middle binary
|
|
67
|
+
read_middle = test_file.read(len(binary_middle))
|
|
68
|
+
self.assertEqual(read_middle, binary_middle)
|
|
69
|
+
|
|
70
|
+
# Load second JSON
|
|
71
|
+
data2 = json_stream.load(test_file)
|
|
72
|
+
self.assertEqual(dict(data2.items()), {"second": False})
|
|
73
|
+
|
|
74
|
+
@skipUnless(HAS_RS_TOKENIZER, 'Rust tokenizer not available')
|
|
75
|
+
def test_load_many_then_binary(self):
|
|
76
|
+
json_1 = '{"a": 1}'
|
|
77
|
+
json_2 = '{"b": 2}'
|
|
78
|
+
binary_data = b'binary'
|
|
79
|
+
test_data = json_1.encode('utf-8') + json_2.encode('utf-8') + binary_data
|
|
80
|
+
|
|
81
|
+
test_file = io.BytesIO(test_data)
|
|
82
|
+
|
|
83
|
+
loader = json_stream.load_many(test_file, correct_cursor=True)
|
|
84
|
+
|
|
85
|
+
# Read first JSON
|
|
86
|
+
doc1 = next(loader)
|
|
87
|
+
doc1.read_all()
|
|
88
|
+
|
|
89
|
+
# Read second JSON
|
|
90
|
+
doc2 = next(loader)
|
|
91
|
+
doc2.read_all()
|
|
92
|
+
|
|
93
|
+
# Now park cursor
|
|
94
|
+
doc2.tokenizer.park_cursor()
|
|
95
|
+
|
|
96
|
+
self.assertEqual(test_file.tell(), len(json_1) + len(json_2))
|
|
97
|
+
self.assertEqual(test_file.read(), binary_data)
|
|
@@ -15,3 +15,16 @@ class TestIterableStream(TestCase):
|
|
|
15
15
|
# stream it and check the result
|
|
16
16
|
stream = IterableStream(data)
|
|
17
17
|
self.assertEqual(stream.read(), b"".join(data))
|
|
18
|
+
|
|
19
|
+
def test_read_str_chunks(self):
|
|
20
|
+
# create some chunks of text data
|
|
21
|
+
data_str = (
|
|
22
|
+
"a" * io.DEFAULT_BUFFER_SIZE,
|
|
23
|
+
"b" * (io.DEFAULT_BUFFER_SIZE + 1),
|
|
24
|
+
"c" * (io.DEFAULT_BUFFER_SIZE - 1),
|
|
25
|
+
)
|
|
26
|
+
expected = ("".join(data_str)).encode()
|
|
27
|
+
|
|
28
|
+
# stream it and check the result
|
|
29
|
+
stream = IterableStream(data_str)
|
|
30
|
+
self.assertEqual(stream.read(), expected)
|
|
@@ -20,9 +20,9 @@ def _visit(obj, visitor, path):
|
|
|
20
20
|
visitor(obj, path)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def visit_many(fp_or_iterator, visitor, tokenizer=default_tokenizer):
|
|
23
|
+
def visit_many(fp_or_iterator, visitor, tokenizer=default_tokenizer, **tokenizer_kwargs):
|
|
24
24
|
fp = ensure_file(fp_or_iterator)
|
|
25
|
-
token_stream = tokenizer(fp)
|
|
25
|
+
token_stream = tokenizer(fp, **tokenizer_kwargs)
|
|
26
26
|
for token_type, token in token_stream:
|
|
27
27
|
if token_type == TokenType.OPERATOR:
|
|
28
28
|
obj = StreamingJSONBase.factory(token, token_stream, persistent=False)
|
|
@@ -33,5 +33,5 @@ def visit_many(fp_or_iterator, visitor, tokenizer=default_tokenizer):
|
|
|
33
33
|
yield
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def visit(fp_or_iterator, visitor, tokenizer=default_tokenizer):
|
|
37
|
-
next(visit_many(fp_or_iterator, visitor, tokenizer))
|
|
36
|
+
def visit(fp_or_iterator, visitor, tokenizer=default_tokenizer, **tokenizer_kwargs):
|
|
37
|
+
next(visit_many(fp_or_iterator, visitor, tokenizer, **tokenizer_kwargs))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: json-stream
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
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
|
|
@@ -53,6 +53,7 @@ Dynamic: license-file
|
|
|
53
53
|
# json-stream
|
|
54
54
|
|
|
55
55
|
[](https://github.com/daggaz/json-stream/actions/workflows/tests.yml)
|
|
56
|
+
[](https://codecov.io/gh/daggaz/json-stream)
|
|
56
57
|
[](https://pypi.org/project/json-stream)
|
|
57
58
|
[](https://pypi.org/project/json-stream/)
|
|
58
59
|
[](https://www.buymeacoffee.com/daggaz)
|
|
@@ -62,7 +63,8 @@ Simple streaming JSON parser and encoder.
|
|
|
62
63
|
When [reading](#reading) JSON data, `json-stream` can decode JSON data in
|
|
63
64
|
a streaming manner, providing a pythonic dict/list-like interface, or a
|
|
64
65
|
[visitor-based interface](#visitor). It can stream from files, [URLs](#urls)
|
|
65
|
-
or [iterators](#iterators). It can process [multiple JSON documents](#multiple)
|
|
66
|
+
or [iterators](#iterators). It can process [multiple JSON documents](#multiple)
|
|
67
|
+
in a single stream, and can read JSON [mixed with other non-JSON data](#reading-mixed-data).
|
|
66
68
|
|
|
67
69
|
When [writing](#writing) JSON data, `json-stream` can stream JSON objects
|
|
68
70
|
as you generate them.
|
|
@@ -443,8 +445,7 @@ with httpx.Client() as client, client.stream('GET', 'http://example.com/data.jso
|
|
|
443
445
|
|
|
444
446
|
### <a id="iterators"></a> Stream an iterable
|
|
445
447
|
|
|
446
|
-
`json-stream`'s parsing functions can take any iterable
|
|
447
|
-
`byte` objects.
|
|
448
|
+
`json-stream`'s parsing functions can take any iterable that produces encoded JSON chunks. The chunks can be `byte`s or `str`s.
|
|
448
449
|
|
|
449
450
|
```python
|
|
450
451
|
import json_stream
|
|
@@ -547,13 +548,81 @@ significant parsing speedup compared to pure python implementation.
|
|
|
547
548
|
`json-stream` will fallback to its pure python tokenizer implementation
|
|
548
549
|
if `json-stream-rs-tokenizer` is not available.
|
|
549
550
|
|
|
551
|
+
#### <a id="reading-mixed-data"></a> Reading mixed data
|
|
552
|
+
|
|
553
|
+
When using the Rust tokenizer, you can also use `json-stream` to parse mixed
|
|
554
|
+
data, for example a file with a JSON followed by binary data.
|
|
555
|
+
|
|
556
|
+
To do this, you should pass `correct_cursor=True` to `load()`. The ensures the
|
|
557
|
+
rust tokenizer keeps track of the exact stream position it has read up to. This
|
|
558
|
+
comes with a **significant performance cost** for un-seekable streams.
|
|
559
|
+
|
|
560
|
+
After reading the JSON data, call `read_all()` on the top-level object returned
|
|
561
|
+
by `load()` to ensure you have read up to the end of the JSON data, and then call
|
|
562
|
+
`.tokenizer.park_cursor()` to "park" the underlying file cursor at the correct
|
|
563
|
+
position.
|
|
564
|
+
|
|
565
|
+
```python
|
|
566
|
+
import json_stream
|
|
567
|
+
|
|
568
|
+
with open('test.bin', 'rb') as f:
|
|
569
|
+
# read JSON header
|
|
570
|
+
header = json_stream.load(f, correct_cursor=True)
|
|
571
|
+
# ... process JSON header ...
|
|
572
|
+
header.read_all()
|
|
573
|
+
|
|
574
|
+
# ensure the tokenizer has "parked" the file
|
|
575
|
+
# cursor at the end of the JSON data
|
|
576
|
+
header.tokenizer.park_cursor()
|
|
577
|
+
|
|
578
|
+
# now we can read binary data from the same file
|
|
579
|
+
binary_start = f.tell()
|
|
580
|
+
data = f.read()
|
|
581
|
+
|
|
582
|
+
#### <a id="mixed-scenarios"></a> Other mixed data scenarios
|
|
583
|
+
|
|
584
|
+
`json-stream` can also handle streams that start with binary data, or have binary
|
|
585
|
+
data between multiple JSON documents.
|
|
586
|
+
|
|
587
|
+
##### Binary then JSON
|
|
588
|
+
|
|
589
|
+
You can simply read the binary data from the file before calling `load()`.
|
|
590
|
+
|
|
591
|
+
```python
|
|
592
|
+
with open('test.bin', 'rb') as f:
|
|
593
|
+
binary_data = f.read(1024)
|
|
594
|
+
data = json_stream.load(f)
|
|
595
|
+
# ... process JSON ...
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
##### JSON then binary then JSON
|
|
599
|
+
|
|
600
|
+
You must use `correct_cursor=True` for any JSON document that is followed by
|
|
601
|
+
binary data.
|
|
602
|
+
|
|
603
|
+
```python
|
|
604
|
+
with open('test.bin', 'rb') as f:
|
|
605
|
+
# 1. Read first JSON
|
|
606
|
+
data1 = json_stream.load(f, correct_cursor=True)
|
|
607
|
+
# ... process data1 ...
|
|
608
|
+
data1.read_all()
|
|
609
|
+
data1.tokenizer.park_cursor()
|
|
610
|
+
|
|
611
|
+
# 2. Read binary data
|
|
612
|
+
binary_data = f.read(1024)
|
|
613
|
+
|
|
614
|
+
# 3. Read second JSON
|
|
615
|
+
data2 = json_stream.load(f)
|
|
616
|
+
# ... process data2 ...
|
|
617
|
+
```
|
|
618
|
+
|
|
550
619
|
### Custom tokenizer
|
|
551
620
|
|
|
552
621
|
You can supply an alternative JSON tokenizer implementation. Simply pass
|
|
553
622
|
a tokenizer to the `load()` or `visit()` methods.
|
|
554
623
|
|
|
555
624
|
```python
|
|
556
|
-
json_stream.load(f, tokenizer=some_tokenizer)
|
|
625
|
+
json_stream.load(f, tokenizer=some_tokenizer, **tokenizer_kwargs)
|
|
557
626
|
```
|
|
558
627
|
|
|
559
628
|
The requests methods also accept a customer tokenizer parameter.
|
|
@@ -26,6 +26,7 @@ src/json_stream/requests/__init__.py
|
|
|
26
26
|
src/json_stream/requests/tests/__init__.py
|
|
27
27
|
src/json_stream/requests/tests/test_requests.py
|
|
28
28
|
src/json_stream/tests/__init__.py
|
|
29
|
+
src/json_stream/tests/test_binary_resumption.py
|
|
29
30
|
src/json_stream/tests/test_buffering.py
|
|
30
31
|
src/json_stream/tests/test_collection_abcs.py
|
|
31
32
|
src/json_stream/tests/test_iterators.py
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import io
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class IterableStream(io.RawIOBase):
|
|
5
|
-
def __init__(self, iterable):
|
|
6
|
-
self.iterator = iter(iterable)
|
|
7
|
-
self.remainder = None
|
|
8
|
-
|
|
9
|
-
def readinto(self, buffer):
|
|
10
|
-
try:
|
|
11
|
-
chunk = self.remainder or next(self.iterator)
|
|
12
|
-
length = min(len(buffer), len(chunk))
|
|
13
|
-
buffer[:length], self.remainder = chunk[:length], chunk[length:]
|
|
14
|
-
return length
|
|
15
|
-
except StopIteration:
|
|
16
|
-
return 0 # indicate EOF
|
|
17
|
-
|
|
18
|
-
def readable(self):
|
|
19
|
-
return True
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def ensure_file(fp_or_iterable):
|
|
23
|
-
if hasattr(fp_or_iterable, 'read'):
|
|
24
|
-
return fp_or_iterable
|
|
25
|
-
return IterableStream(fp_or_iterable) # will raise TypeError if not iterable
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|