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.
Files changed (44) hide show
  1. {json-stream-2.1.0/src/json_stream.egg-info → json_stream-2.4.1}/PKG-INFO +106 -21
  2. {json-stream-2.1.0 → json_stream-2.4.1}/README.md +96 -17
  3. {json-stream-2.1.0 → json_stream-2.4.1}/pyproject.toml +5 -3
  4. json_stream-2.4.1/src/json_stream/__init__.py +4 -0
  5. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/base.py +13 -1
  6. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/dump/__init__.py +4 -5
  7. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/dump/tests/test_dump.py +18 -2
  8. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/httpx/__init__.py +8 -0
  9. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/httpx/tests/test_httpx.py +42 -6
  10. json_stream-2.4.1/src/json_stream/iterators.py +33 -0
  11. json_stream-2.4.1/src/json_stream/loader.py +19 -0
  12. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/requests/__init__.py +8 -0
  13. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/requests/tests/test_requests.py +42 -6
  14. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/select_tokenizer.py +1 -1
  15. json_stream-2.4.1/src/json_stream/tests/test_buffering.py +33 -0
  16. json_stream-2.4.1/src/json_stream/tests/test_collection_abcs.py +17 -0
  17. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_iterators.py +13 -0
  18. json_stream-2.4.1/src/json_stream/tests/test_load_many.py +102 -0
  19. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_loader.py +44 -1
  20. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_tokenizer.py +82 -24
  21. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_tokenizer_integration.py +7 -12
  22. json_stream-2.4.1/src/json_stream/tests/test_util.py +15 -0
  23. json_stream-2.4.1/src/json_stream/tests/test_visit_many.py +42 -0
  24. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_visitor.py +5 -0
  25. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/test_writer.py +20 -12
  26. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tokenizer.py +84 -61
  27. json_stream-2.4.1/src/json_stream/util.py +56 -0
  28. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/visitor.py +14 -4
  29. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/writer.py +6 -2
  30. {json-stream-2.1.0 → json_stream-2.4.1/src/json_stream.egg-info}/PKG-INFO +106 -21
  31. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream.egg-info/SOURCES.txt +6 -0
  32. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream.egg-info/requires.txt +1 -1
  33. json-stream-2.1.0/src/json_stream/__init__.py +0 -3
  34. json-stream-2.1.0/src/json_stream/iterators.py +0 -25
  35. json-stream-2.1.0/src/json_stream/loader.py +0 -10
  36. {json-stream-2.1.0 → json_stream-2.4.1}/LICENSE.txt +0 -0
  37. {json-stream-2.1.0 → json_stream-2.4.1}/setup.cfg +0 -0
  38. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/dump/tests/__init__.py +0 -0
  39. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/dump/threading.py +0 -0
  40. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/httpx/tests/__init__.py +0 -0
  41. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/requests/tests/__init__.py +0 -0
  42. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream/tests/__init__.py +0 -0
  43. {json-stream-2.1.0 → json_stream-2.4.1}/src/json_stream.egg-info/dependency_links.txt +0 -0
  44. {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
1
+ Metadata-Version: 2.4
2
2
  Name: json-stream
3
- Version: 2.1.0
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
- License-File: LICENSE.txt
50
+ Requires-Dist: httpx; extra == "httpx"
51
+ Dynamic: license-file
46
52
 
47
53
  # json-stream
48
54
 
49
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)
50
57
  [![PyPI package and version badge](https://img.shields.io/pypi/v/json-stream)](https://pypi.org/project/json-stream)
51
58
  [![Supported Python versions badge](https://img.shields.io/pypi/pyversions/json-stream)](https://pypi.org/project/json-stream/)
52
59
  [![Donate](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-blue.svg)](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 interfeace](#visitor). Can stream from files, [URLs](#urls)
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 required.
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 object that produces encoded JSON as
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 Yajl JSON library that can be used to
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
- * It's not pure python
770
+ * No pure python implementation
691
771
  * It does not provide a pythonic `dict`/`list`-like interface
692
772
 
693
- # Build
694
- ```bash
695
- cd ~/sources/json-stream/
696
- python3 -m venv ~/build/
697
- . ~/build/bin/activate
698
- pip install --upgrade build twine
699
- python -m build
700
- twine upload dist/*
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
  [![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)
@@ -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 interfeace](#visitor). Can stream from files, [URLs](#urls)
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 required.
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 object that produces encoded JSON as
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 Yajl JSON library that can be used to
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
- * It's not pure python
718
+ * No pure python implementation
645
719
  * It does not provide a pythonic `dict`/`list`-like interface
646
720
 
647
- # Build
648
- ```bash
649
- cd ~/sources/json-stream/
650
- python3 -m venv ~/build/
651
- . ~/build/bin/activate
652
- pip install --upgrade build twine
653
- python -m build
654
- twine upload dist/*
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.0"
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.8"]
30
+ dependencies = ["json-stream-rs-tokenizer >= 0.4.17"]
29
31
  dynamic = ["readme"]
30
32
 
31
33
  [project.urls]
@@ -0,0 +1,4 @@
1
+ from json_stream.loader import load, load_many # noqa: F401
2
+ from json_stream.visitor import visit, visit_many # noqa: F401
3
+ from json_stream.writer import streamable_list, streamable_dict # noqa: F401
4
+ from json_stream.util import to_standard_types
@@ -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.base import StreamingJSONObject, StreamingJSONList
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, StreamingJSONObject):
11
- return dict(obj)
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), persistent=True)
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), persistent=True)
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.httpx import load, visit
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 = json.dumps({
23
- "a": "a" * io.DEFAULT_BUFFER_SIZE,
24
- "b": "b",
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