json-stream 2.4.1__tar.gz → 2.5.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.
Files changed (42) hide show
  1. {json_stream-2.4.1/src/json_stream.egg-info → json_stream-2.5.1}/PKG-INFO +72 -3
  2. {json_stream-2.4.1 → json_stream-2.5.1}/README.md +71 -2
  3. {json_stream-2.4.1 → json_stream-2.5.1}/pyproject.toml +1 -1
  4. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/base.py +4 -0
  5. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/loader.py +4 -4
  6. json_stream-2.5.1/src/json_stream/tests/test_binary_resumption.py +97 -0
  7. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_tokenizer.py +28 -0
  8. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tokenizer.py +65 -1
  9. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/visitor.py +4 -4
  10. {json_stream-2.4.1 → json_stream-2.5.1/src/json_stream.egg-info}/PKG-INFO +72 -3
  11. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream.egg-info/SOURCES.txt +1 -0
  12. {json_stream-2.4.1 → json_stream-2.5.1}/LICENSE.txt +0 -0
  13. {json_stream-2.4.1 → json_stream-2.5.1}/setup.cfg +0 -0
  14. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/__init__.py +0 -0
  15. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/dump/__init__.py +0 -0
  16. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/dump/tests/__init__.py +0 -0
  17. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/dump/tests/test_dump.py +0 -0
  18. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/dump/threading.py +0 -0
  19. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/httpx/__init__.py +0 -0
  20. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/httpx/tests/__init__.py +0 -0
  21. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/httpx/tests/test_httpx.py +0 -0
  22. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/iterators.py +0 -0
  23. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/requests/__init__.py +0 -0
  24. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/requests/tests/__init__.py +0 -0
  25. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/requests/tests/test_requests.py +0 -0
  26. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/select_tokenizer.py +0 -0
  27. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/__init__.py +0 -0
  28. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_buffering.py +0 -0
  29. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_collection_abcs.py +0 -0
  30. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_iterators.py +0 -0
  31. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_load_many.py +0 -0
  32. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_loader.py +0 -0
  33. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_tokenizer_integration.py +0 -0
  34. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_util.py +0 -0
  35. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_visit_many.py +0 -0
  36. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_visitor.py +0 -0
  37. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/tests/test_writer.py +0 -0
  38. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/util.py +0 -0
  39. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream/writer.py +0 -0
  40. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream.egg-info/dependency_links.txt +0 -0
  41. {json_stream-2.4.1 → json_stream-2.5.1}/src/json_stream.egg-info/requires.txt +0 -0
  42. {json_stream-2.4.1 → json_stream-2.5.1}/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.1
3
+ Version: 2.5.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
@@ -63,7 +63,8 @@ Simple streaming JSON parser and encoder.
63
63
  When [reading](#reading) JSON data, `json-stream` can decode JSON data in
64
64
  a streaming manner, providing a pythonic dict/list-like interface, or a
65
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.
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).
67
68
 
68
69
  When [writing](#writing) JSON data, `json-stream` can stream JSON objects
69
70
  as you generate them.
@@ -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.
@@ -11,7 +11,8 @@ Simple streaming JSON parser and encoder.
11
11
  When [reading](#reading) JSON data, `json-stream` can decode JSON data in
12
12
  a streaming manner, providing a pythonic dict/list-like interface, or a
13
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
+ 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).
15
16
 
16
17
  When [writing](#writing) JSON data, `json-stream` can stream JSON objects
17
18
  as you generate them.
@@ -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.1"
8
+ version = "2.5.1"
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()
@@ -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)
@@ -5,6 +5,7 @@ https://github.com/danielyule/naya
5
5
 
6
6
  Copyright (c) 2019 Daniel Yule
7
7
  """
8
+ import math
8
9
  import re
9
10
  from io import StringIO
10
11
  from unittest import TestCase
@@ -59,6 +60,33 @@ class TestJsonTokenization(TestCase):
59
60
  self.assertRaises(ValueError, self.tokenize_sequence, "3.6ea")
60
61
  self.assertRaises(ValueError, self.tokenize_sequence, "67.8e+a")
61
62
 
63
+ def test_nan_infinity_parsing(self):
64
+ token_list = self.tokenize_sequence("NaN")
65
+ self.assertEqual(1, len(token_list))
66
+ ttype, token = token_list[0]
67
+ self.assertTrue(math.isnan(token))
68
+ self.assertEqual(ttype, TokenType.NUMBER)
69
+
70
+ self.assertNumberEquals(float("inf"), "Infinity")
71
+ self.assertNumberEquals(float("-inf"), "-Infinity")
72
+
73
+ self.assertRaises(ValueError, self.tokenize_sequence, "Na")
74
+ self.assertRaises(ValueError, self.tokenize_sequence, "Nax")
75
+ self.assertRaises(ValueError, self.tokenize_sequence, "In")
76
+ self.assertRaises(ValueError, self.tokenize_sequence, "Inf")
77
+ self.assertRaises(ValueError, self.tokenize_sequence, "Infi")
78
+ self.assertRaises(ValueError, self.tokenize_sequence, "Infin")
79
+ self.assertRaises(ValueError, self.tokenize_sequence, "Infini")
80
+ self.assertRaises(ValueError, self.tokenize_sequence, "Infinit")
81
+ self.assertRaises(ValueError, self.tokenize_sequence, "Infinityx")
82
+ self.assertRaises(ValueError, self.tokenize_sequence, "-In")
83
+ self.assertRaises(ValueError, self.tokenize_sequence, "-Inf")
84
+ self.assertRaises(ValueError, self.tokenize_sequence, "-Infi")
85
+ self.assertRaises(ValueError, self.tokenize_sequence, "-Infin")
86
+ self.assertRaises(ValueError, self.tokenize_sequence, "-Infini")
87
+ self.assertRaises(ValueError, self.tokenize_sequence, "-Infinit")
88
+ self.assertRaises(ValueError, self.tokenize_sequence, "-Infinityx")
89
+
62
90
  def test_operator_parsing(self):
63
91
  self.assertOperatorEquals("{", "{")
64
92
  self.assertOperatorEquals("}", "}")
@@ -45,6 +45,15 @@ class State:
45
45
  UNICODE_SURROGATE_START = 23
46
46
  UNICODE_SURROGATE_STRING_ESCAPE = 24
47
47
  UNICODE_SURROGATE = 25
48
+ NAN_1 = 26
49
+ NAN_2 = 27
50
+ INF_1 = 28
51
+ INF_2 = 29
52
+ INF_3 = 30
53
+ INF_4 = 31
54
+ INF_5 = 32
55
+ INF_6 = 33
56
+ INF_7 = 34
48
57
 
49
58
 
50
59
  class SpecialChar:
@@ -130,6 +139,10 @@ def tokenize(stream):
130
139
  next_state = State.TRUE_1
131
140
  elif char == "n":
132
141
  next_state = State.NULL_1
142
+ elif char == "N":
143
+ next_state = State.NAN_1
144
+ elif char == "I":
145
+ next_state = State.INF_1
133
146
  elif not char.isspace() and not char == SpecialChar.EOF:
134
147
  raise ValueError("Invalid JSON character: '{0}'".format(char))
135
148
  elif state == State.INTEGER:
@@ -169,8 +182,10 @@ def tokenize(stream):
169
182
  elif char in "123456789":
170
183
  next_state = State.INTEGER
171
184
  add_char = True
185
+ elif char == "I":
186
+ next_state = State.INF_1
172
187
  else:
173
- raise ValueError("A - must be followed by a digit. Got '{0}'".format(char))
188
+ raise ValueError("A - must be followed by a digit or Infinity. Got '{0}'".format(char))
174
189
  elif state == State.INTEGER_EXP_0:
175
190
  if char == "+" or char == "-" or char in "0123456789":
176
191
  next_state = State.INTEGER_EXP
@@ -262,6 +277,55 @@ def tokenize(stream):
262
277
  now_token = (TokenType.NULL, None)
263
278
  else:
264
279
  raise ValueError("Invalid JSON character: '{0}'".format(char))
280
+ elif state == State.NAN_1:
281
+ if char == "a":
282
+ next_state = State.NAN_2
283
+ else:
284
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
285
+ elif state == State.NAN_2:
286
+ if char == "N":
287
+ next_state = State.WHITESPACE
288
+ completed = True
289
+ now_token = (TokenType.NUMBER, float("NaN"))
290
+ else:
291
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
292
+ elif state == State.INF_1:
293
+ if char == "n":
294
+ next_state = State.INF_2
295
+ else:
296
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
297
+ elif state == State.INF_2:
298
+ if char == "f":
299
+ next_state = State.INF_3
300
+ else:
301
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
302
+ elif state == State.INF_3:
303
+ if char == "i":
304
+ next_state = State.INF_4
305
+ else:
306
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
307
+ elif state == State.INF_4:
308
+ if char == "n":
309
+ next_state = State.INF_5
310
+ else:
311
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
312
+ elif state == State.INF_5:
313
+ if char == "i":
314
+ next_state = State.INF_6
315
+ else:
316
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
317
+ elif state == State.INF_6:
318
+ if char == "t":
319
+ next_state = State.INF_7
320
+ else:
321
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
322
+ elif state == State.INF_7:
323
+ if char == "y":
324
+ next_state = State.WHITESPACE
325
+ completed = True
326
+ now_token = (TokenType.NUMBER, float("".join(token) + "Infinity"))
327
+ else:
328
+ raise ValueError("Invalid JSON character: '{0}'".format(char))
265
329
  elif state == State.STRING:
266
330
  if char == "\"":
267
331
  completed = True
@@ -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.1
3
+ Version: 2.5.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
@@ -63,7 +63,8 @@ Simple streaming JSON parser and encoder.
63
63
  When [reading](#reading) JSON data, `json-stream` can decode JSON data in
64
64
  a streaming manner, providing a pythonic dict/list-like interface, or a
65
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.
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).
67
68
 
68
69
  When [writing](#writing) JSON data, `json-stream` can stream JSON objects
69
70
  as you generate them.
@@ -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
File without changes
File without changes