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.
Files changed (43) hide show
  1. {json_stream-2.4.0/src/json_stream.egg-info → json_stream-2.5.0}/PKG-INFO +74 -5
  2. {json_stream-2.4.0 → json_stream-2.5.0}/README.md +73 -4
  3. {json_stream-2.4.0 → json_stream-2.5.0}/pyproject.toml +1 -1
  4. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/base.py +4 -0
  5. json_stream-2.5.0/src/json_stream/iterators.py +33 -0
  6. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/loader.py +4 -4
  7. json_stream-2.5.0/src/json_stream/tests/test_binary_resumption.py +97 -0
  8. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_iterators.py +13 -0
  9. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/visitor.py +4 -4
  10. {json_stream-2.4.0 → json_stream-2.5.0/src/json_stream.egg-info}/PKG-INFO +74 -5
  11. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream.egg-info/SOURCES.txt +1 -0
  12. json_stream-2.4.0/src/json_stream/iterators.py +0 -25
  13. {json_stream-2.4.0 → json_stream-2.5.0}/LICENSE.txt +0 -0
  14. {json_stream-2.4.0 → json_stream-2.5.0}/setup.cfg +0 -0
  15. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/__init__.py +0 -0
  16. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/dump/__init__.py +0 -0
  17. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/dump/tests/__init__.py +0 -0
  18. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/dump/tests/test_dump.py +0 -0
  19. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/dump/threading.py +0 -0
  20. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/httpx/__init__.py +0 -0
  21. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/httpx/tests/__init__.py +0 -0
  22. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/httpx/tests/test_httpx.py +0 -0
  23. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/requests/__init__.py +0 -0
  24. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/requests/tests/__init__.py +0 -0
  25. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/requests/tests/test_requests.py +0 -0
  26. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/select_tokenizer.py +0 -0
  27. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/__init__.py +0 -0
  28. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_buffering.py +0 -0
  29. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_collection_abcs.py +0 -0
  30. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_load_many.py +0 -0
  31. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_loader.py +0 -0
  32. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_tokenizer.py +0 -0
  33. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_tokenizer_integration.py +0 -0
  34. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_util.py +0 -0
  35. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_visit_many.py +0 -0
  36. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_visitor.py +0 -0
  37. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tests/test_writer.py +0 -0
  38. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/tokenizer.py +0 -0
  39. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/util.py +0 -0
  40. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream/writer.py +0 -0
  41. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream.egg-info/dependency_links.txt +0 -0
  42. {json_stream-2.4.0 → json_stream-2.5.0}/src/json_stream.egg-info/requires.txt +0 -0
  43. {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.4.0
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
  [![Tests](https://github.com/daggaz/json-stream/actions/workflows/tests.yml/badge.svg)](https://github.com/daggaz/json-stream/actions/workflows/tests.yml)
56
+ [![codecov](https://codecov.io/gh/daggaz/json-stream/graph/badge.svg?token=SWWYF2KHMC)](https://codecov.io/gh/daggaz/json-stream)
56
57
  [![PyPI package and version badge](https://img.shields.io/pypi/v/json-stream)](https://pypi.org/project/json-stream)
57
58
  [![Supported Python versions badge](https://img.shields.io/pypi/pyversions/json-stream)](https://pypi.org/project/json-stream/)
58
59
  [![Donate](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-blue.svg)](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) in a single stream.
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 object that produces encoded JSON as
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
  [![Tests](https://github.com/daggaz/json-stream/actions/workflows/tests.yml/badge.svg)](https://github.com/daggaz/json-stream/actions/workflows/tests.yml)
4
+ [![codecov](https://codecov.io/gh/daggaz/json-stream/graph/badge.svg?token=SWWYF2KHMC)](https://codecov.io/gh/daggaz/json-stream)
4
5
  [![PyPI package and version badge](https://img.shields.io/pypi/v/json-stream)](https://pypi.org/project/json-stream)
5
6
  [![Supported Python versions badge](https://img.shields.io/pypi/pyversions/json-stream)](https://pypi.org/project/json-stream/)
6
7
  [![Donate](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-blue.svg)](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) in a single stream.
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 object that produces encoded JSON as
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.
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "json-stream"
8
- version = "2.4.0"
8
+ version = "2.5.0"
9
9
  authors = [
10
10
  {name = "Jamie Cockburn", email="jamie_cockburn@hotmail.co.uk"},
11
11
  ]
@@ -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.4.0
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
  [![Tests](https://github.com/daggaz/json-stream/actions/workflows/tests.yml/badge.svg)](https://github.com/daggaz/json-stream/actions/workflows/tests.yml)
56
+ [![codecov](https://codecov.io/gh/daggaz/json-stream/graph/badge.svg?token=SWWYF2KHMC)](https://codecov.io/gh/daggaz/json-stream)
56
57
  [![PyPI package and version badge](https://img.shields.io/pypi/v/json-stream)](https://pypi.org/project/json-stream)
57
58
  [![Supported Python versions badge](https://img.shields.io/pypi/pyversions/json-stream)](https://pypi.org/project/json-stream/)
58
59
  [![Donate](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-blue.svg)](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) in a single stream.
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 object that produces encoded JSON as
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