json-stream 2.3.4__tar.gz → 2.4.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.3.4/src/json_stream.egg-info → json_stream-2.4.0}/PKG-INFO +62 -3
- {json_stream-2.3.4 → json_stream-2.4.0}/README.md +61 -2
- {json_stream-2.3.4 → json_stream-2.4.0}/pyproject.toml +1 -1
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/__init__.py +2 -2
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/httpx/__init__.py +8 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/httpx/tests/test_httpx.py +42 -6
- json_stream-2.4.0/src/json_stream/loader.py +19 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/requests/__init__.py +8 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/requests/tests/test_requests.py +42 -6
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/select_tokenizer.py +1 -1
- json_stream-2.4.0/src/json_stream/tests/test_load_many.py +102 -0
- json_stream-2.4.0/src/json_stream/tests/test_visit_many.py +42 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_writer.py +2 -2
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/visitor.py +13 -7
- {json_stream-2.3.4 → json_stream-2.4.0/src/json_stream.egg-info}/PKG-INFO +62 -3
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream.egg-info/SOURCES.txt +2 -0
- json_stream-2.3.4/src/json_stream/loader.py +0 -12
- {json_stream-2.3.4 → json_stream-2.4.0}/LICENSE.txt +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/setup.cfg +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/base.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/dump/__init__.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/dump/tests/__init__.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/dump/tests/test_dump.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/dump/threading.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/httpx/tests/__init__.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/iterators.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/requests/tests/__init__.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/__init__.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_buffering.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_collection_abcs.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_iterators.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_loader.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_tokenizer.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_tokenizer_integration.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_util.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tests/test_visitor.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/tokenizer.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/util.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream/writer.py +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream.egg-info/dependency_links.txt +0 -0
- {json_stream-2.3.4 → json_stream-2.4.0}/src/json_stream.egg-info/requires.txt +0 -0
- {json_stream-2.3.4 → json_stream-2.4.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.4.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
|
|
@@ -61,8 +61,8 @@ Simple streaming JSON parser and encoder.
|
|
|
61
61
|
|
|
62
62
|
When [reading](#reading) JSON data, `json-stream` can decode JSON data in
|
|
63
63
|
a streaming manner, providing a pythonic dict/list-like interface, or a
|
|
64
|
-
[visitor-based
|
|
65
|
-
or [iterators](#iterators).
|
|
64
|
+
[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
66
|
|
|
67
67
|
When [writing](#writing) JSON data, `json-stream` can stream JSON objects
|
|
68
68
|
as you generate them.
|
|
@@ -278,6 +278,65 @@ z at path ('xxxx', 3)
|
|
|
278
278
|
[] at path ('xxxx', 5)
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
+
### <a id="multiple"></a> Multiple JSON documents: `load_many()` and `visit_many()`
|
|
282
|
+
|
|
283
|
+
Sometimes JSON data arrives as a sequence of top‑level JSON texts rather than a single array/object. json-stream supports this pattern with:
|
|
284
|
+
- `json_stream.load_many(...)`: yields each top-level JSON value as it is parsed.
|
|
285
|
+
- `json_stream.visit_many(...)`: visits each top-level JSON value and yields control after each one.
|
|
286
|
+
|
|
287
|
+
These functions are useful for common streaming formats:
|
|
288
|
+
- 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".
|
|
289
|
+
- Concatenated or sequential JSON documents: complete JSON texts written back-to-back without delimiters.
|
|
290
|
+
|
|
291
|
+
Note about concatenated JSON without explicit delimiters:
|
|
292
|
+
|
|
293
|
+
When multiple top‑level JSON documents are simply concatenated with
|
|
294
|
+
no delimiters (no newlines or no spaces), json-stream can unambiguously
|
|
295
|
+
detect the document boundaries for the following top-level values:
|
|
296
|
+
- objects: `{ ... }`
|
|
297
|
+
- arrays: `[ ... ]`
|
|
298
|
+
- literals: `true`, `false`, `null`
|
|
299
|
+
|
|
300
|
+
However, numbers and strings require a delimiter between consecutive
|
|
301
|
+
documents so the tokenizer can tell where one ends and the next begins.
|
|
302
|
+
A delimiter can be as simple as a single whitespace or a newline.
|
|
303
|
+
|
|
304
|
+
Examples:
|
|
305
|
+
- Valid without delimiters (will parse as 2 docs):
|
|
306
|
+
- `{"a":1}{"b":2}` → `{...}`, `{...}`
|
|
307
|
+
- `[1][2]` → `[1]`, `[2]`
|
|
308
|
+
- `truefalse` → `true`, `false`
|
|
309
|
+
- `null[]` → `null`, `[]`
|
|
310
|
+
- These will error without a delimiter between documents (e.g. a space or newline):
|
|
311
|
+
- `3true`
|
|
312
|
+
- `0"x"`
|
|
313
|
+
- `"hi""there"
|
|
314
|
+
|
|
315
|
+
#### Examples
|
|
316
|
+
|
|
317
|
+
Read many from a file containing NDJSON or concatenated JSON:
|
|
318
|
+
```python
|
|
319
|
+
import json_stream
|
|
320
|
+
|
|
321
|
+
with open("events.ndjson", "rb") as f: # NDJSON: one JSON object per line
|
|
322
|
+
for item in json_stream.load_many(f):
|
|
323
|
+
# each item is the top-level item in each JSON text
|
|
324
|
+
handle(item)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Visit many values in many documents:
|
|
328
|
+
```python
|
|
329
|
+
import json_stream
|
|
330
|
+
|
|
331
|
+
def visitor(value, path):
|
|
332
|
+
# called depth-first for each value within the current top-level JSON text
|
|
333
|
+
...
|
|
334
|
+
|
|
335
|
+
for _ in json_stream.visit_many(open("events.ndjson", "rb"), visitor):
|
|
336
|
+
# loop advances one top-level JSON text at a time
|
|
337
|
+
pass
|
|
338
|
+
```
|
|
339
|
+
|
|
281
340
|
### <a id="urls"></a> Stream a URL
|
|
282
341
|
|
|
283
342
|
`json_stream` knows how to stream directly from a URL using a variety of packages.
|
|
@@ -9,8 +9,8 @@ Simple streaming JSON parser and encoder.
|
|
|
9
9
|
|
|
10
10
|
When [reading](#reading) JSON data, `json-stream` can decode JSON data in
|
|
11
11
|
a streaming manner, providing a pythonic dict/list-like interface, or a
|
|
12
|
-
[visitor-based
|
|
13
|
-
or [iterators](#iterators).
|
|
12
|
+
[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
14
|
|
|
15
15
|
When [writing](#writing) JSON data, `json-stream` can stream JSON objects
|
|
16
16
|
as you generate them.
|
|
@@ -226,6 +226,65 @@ z at path ('xxxx', 3)
|
|
|
226
226
|
[] at path ('xxxx', 5)
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
+
### <a id="multiple"></a> Multiple JSON documents: `load_many()` and `visit_many()`
|
|
230
|
+
|
|
231
|
+
Sometimes JSON data arrives as a sequence of top‑level JSON texts rather than a single array/object. json-stream supports this pattern with:
|
|
232
|
+
- `json_stream.load_many(...)`: yields each top-level JSON value as it is parsed.
|
|
233
|
+
- `json_stream.visit_many(...)`: visits each top-level JSON value and yields control after each one.
|
|
234
|
+
|
|
235
|
+
These functions are useful for common streaming formats:
|
|
236
|
+
- 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".
|
|
237
|
+
- Concatenated or sequential JSON documents: complete JSON texts written back-to-back without delimiters.
|
|
238
|
+
|
|
239
|
+
Note about concatenated JSON without explicit delimiters:
|
|
240
|
+
|
|
241
|
+
When multiple top‑level JSON documents are simply concatenated with
|
|
242
|
+
no delimiters (no newlines or no spaces), json-stream can unambiguously
|
|
243
|
+
detect the document boundaries for the following top-level values:
|
|
244
|
+
- objects: `{ ... }`
|
|
245
|
+
- arrays: `[ ... ]`
|
|
246
|
+
- literals: `true`, `false`, `null`
|
|
247
|
+
|
|
248
|
+
However, numbers and strings require a delimiter between consecutive
|
|
249
|
+
documents so the tokenizer can tell where one ends and the next begins.
|
|
250
|
+
A delimiter can be as simple as a single whitespace or a newline.
|
|
251
|
+
|
|
252
|
+
Examples:
|
|
253
|
+
- Valid without delimiters (will parse as 2 docs):
|
|
254
|
+
- `{"a":1}{"b":2}` → `{...}`, `{...}`
|
|
255
|
+
- `[1][2]` → `[1]`, `[2]`
|
|
256
|
+
- `truefalse` → `true`, `false`
|
|
257
|
+
- `null[]` → `null`, `[]`
|
|
258
|
+
- These will error without a delimiter between documents (e.g. a space or newline):
|
|
259
|
+
- `3true`
|
|
260
|
+
- `0"x"`
|
|
261
|
+
- `"hi""there"
|
|
262
|
+
|
|
263
|
+
#### Examples
|
|
264
|
+
|
|
265
|
+
Read many from a file containing NDJSON or concatenated JSON:
|
|
266
|
+
```python
|
|
267
|
+
import json_stream
|
|
268
|
+
|
|
269
|
+
with open("events.ndjson", "rb") as f: # NDJSON: one JSON object per line
|
|
270
|
+
for item in json_stream.load_many(f):
|
|
271
|
+
# each item is the top-level item in each JSON text
|
|
272
|
+
handle(item)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Visit many values in many documents:
|
|
276
|
+
```python
|
|
277
|
+
import json_stream
|
|
278
|
+
|
|
279
|
+
def visitor(value, path):
|
|
280
|
+
# called depth-first for each value within the current top-level JSON text
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
for _ in json_stream.visit_many(open("events.ndjson", "rb"), visitor):
|
|
284
|
+
# loop advances one top-level JSON text at a time
|
|
285
|
+
pass
|
|
286
|
+
```
|
|
287
|
+
|
|
229
288
|
### <a id="urls"></a> Stream a URL
|
|
230
289
|
|
|
231
290
|
`json_stream` knows how to stream directly from a URL using a variety of packages.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from json_stream.loader import load # noqa: F401
|
|
2
|
-
from json_stream.visitor import visit # noqa: F401
|
|
1
|
+
from json_stream.loader import load, load_many # noqa: F401
|
|
2
|
+
from json_stream.visitor import visit, visit_many # noqa: F401
|
|
3
3
|
from json_stream.writer import streamable_list, streamable_dict # noqa: F401
|
|
4
4
|
from json_stream.util import to_standard_types
|
|
@@ -13,5 +13,13 @@ def load(response, persistent=False, tokenizer=default_tokenizer, chunk_size=CON
|
|
|
13
13
|
return json_stream.load(_to_iterable(response, chunk_size), persistent=persistent, tokenizer=tokenizer)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def load_many(response, persistent=False, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
17
|
+
return json_stream.load_many(_to_iterable(response, chunk_size), persistent=persistent, tokenizer=tokenizer)
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
def visit(response, visitor, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
17
21
|
return json_stream.visit(_to_iterable(response, chunk_size), visitor, tokenizer=tokenizer)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def visit_many(response, visitor, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
25
|
+
return json_stream.visit_many(_to_iterable(response, chunk_size), visitor, tokenizer=tokenizer)
|
|
@@ -4,7 +4,8 @@ from itertools import zip_longest
|
|
|
4
4
|
from unittest import TestCase
|
|
5
5
|
from unittest.mock import Mock
|
|
6
6
|
|
|
7
|
-
from json_stream
|
|
7
|
+
from json_stream import to_standard_types
|
|
8
|
+
from json_stream.httpx import load, visit, load_many, visit_many
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class TestLoad(TestCase):
|
|
@@ -16,13 +17,14 @@ class TestLoad(TestCase):
|
|
|
16
17
|
args = [iter(iterable)] * n
|
|
17
18
|
return zip_longest(*args, fillvalue="")
|
|
18
19
|
|
|
19
|
-
def _create_mock_response(self):
|
|
20
|
+
def _create_mock_response(self, data=None):
|
|
20
21
|
# httpx iter_bytes returns an iterable of bytes
|
|
21
22
|
response = Mock()
|
|
22
|
-
data
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if data is None:
|
|
24
|
+
data = json.dumps({
|
|
25
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
26
|
+
"b": "b",
|
|
27
|
+
})
|
|
26
28
|
content = ("".join(chunk).encode() for chunk in self.grouper(data, 1024))
|
|
27
29
|
response.iter_bytes.return_value = content
|
|
28
30
|
return response
|
|
@@ -69,3 +71,37 @@ class TestLoad(TestCase):
|
|
|
69
71
|
('a' * io.DEFAULT_BUFFER_SIZE, ('a',)),
|
|
70
72
|
('b', ('b',)),
|
|
71
73
|
], visited)
|
|
74
|
+
|
|
75
|
+
def test_load_many(self):
|
|
76
|
+
expected = [{
|
|
77
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
78
|
+
}, {
|
|
79
|
+
"b": "b" * io.DEFAULT_BUFFER_SIZE,
|
|
80
|
+
}]
|
|
81
|
+
data = "".join(json.dumps(i) for i in expected)
|
|
82
|
+
response = self._create_mock_response(data=data)
|
|
83
|
+
count = 0
|
|
84
|
+
for item, exp in zip(load_many(response), expected):
|
|
85
|
+
self.assertEqual(exp, to_standard_types(item))
|
|
86
|
+
count += 1
|
|
87
|
+
self.assertEqual(count, len(expected))
|
|
88
|
+
|
|
89
|
+
def test_visit_many(self):
|
|
90
|
+
items = [{
|
|
91
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
92
|
+
}, {
|
|
93
|
+
"b": "b" * io.DEFAULT_BUFFER_SIZE,
|
|
94
|
+
}]
|
|
95
|
+
data = "".join(json.dumps(e) for e in items)
|
|
96
|
+
response = self._create_mock_response(data=data)
|
|
97
|
+
current_visit = []
|
|
98
|
+
visits = []
|
|
99
|
+
|
|
100
|
+
def visitor(item, path):
|
|
101
|
+
current_visit.append((item, path))
|
|
102
|
+
for _ in visit_many(response, visitor):
|
|
103
|
+
visits.append(current_visit)
|
|
104
|
+
current_visit = []
|
|
105
|
+
self.assertEqual(len(visits), len(items))
|
|
106
|
+
self.assertEqual(visits[0], [('a' * io.DEFAULT_BUFFER_SIZE, ('a',))])
|
|
107
|
+
self.assertEqual(visits[1], [('b' * io.DEFAULT_BUFFER_SIZE, ('b',))])
|
|
@@ -0,0 +1,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
|
|
@@ -13,5 +13,13 @@ def load(response, persistent=False, tokenizer=default_tokenizer, chunk_size=CON
|
|
|
13
13
|
return json_stream.load(_to_iterable(response, chunk_size), persistent=persistent, tokenizer=tokenizer)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def load_many(response, persistent=False, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
17
|
+
return json_stream.load_many(_to_iterable(response, chunk_size), persistent=persistent, tokenizer=tokenizer)
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
def visit(response, visitor, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
17
21
|
return json_stream.visit(_to_iterable(response, chunk_size), visitor, tokenizer=tokenizer)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def visit_many(response, visitor, tokenizer=default_tokenizer, chunk_size=CONTENT_CHUNK_SIZE):
|
|
25
|
+
return json_stream.visit_many(_to_iterable(response, chunk_size), visitor, tokenizer=tokenizer)
|
|
@@ -4,7 +4,8 @@ from itertools import zip_longest
|
|
|
4
4
|
from unittest import TestCase
|
|
5
5
|
from unittest.mock import Mock
|
|
6
6
|
|
|
7
|
-
from json_stream
|
|
7
|
+
from json_stream import to_standard_types
|
|
8
|
+
from json_stream.requests import load, visit, load_many, visit_many
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class TestLoad(TestCase):
|
|
@@ -17,13 +18,14 @@ class TestLoad(TestCase):
|
|
|
17
18
|
args = [iter(iterable)] * n
|
|
18
19
|
return zip_longest(*args, fillvalue="")
|
|
19
20
|
|
|
20
|
-
def _create_mock_response(self):
|
|
21
|
+
def _create_mock_response(self, data=None):
|
|
21
22
|
# requests iter_content returns an iterable of bytes
|
|
22
23
|
response = Mock()
|
|
23
|
-
data
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if data is None:
|
|
25
|
+
data = json.dumps({
|
|
26
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
27
|
+
"b": "b",
|
|
28
|
+
})
|
|
27
29
|
content = ("".join(chunk).encode() for chunk in self.grouper(data, 1024))
|
|
28
30
|
response.iter_content.return_value = content
|
|
29
31
|
return response
|
|
@@ -70,3 +72,37 @@ class TestLoad(TestCase):
|
|
|
70
72
|
('a' * io.DEFAULT_BUFFER_SIZE, ('a',)),
|
|
71
73
|
('b', ('b',)),
|
|
72
74
|
], visited)
|
|
75
|
+
|
|
76
|
+
def test_load_many(self):
|
|
77
|
+
expected = [{
|
|
78
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
79
|
+
}, {
|
|
80
|
+
"b": "b" * io.DEFAULT_BUFFER_SIZE,
|
|
81
|
+
}]
|
|
82
|
+
data = "".join(json.dumps(i) for i in expected)
|
|
83
|
+
response = self._create_mock_response(data=data)
|
|
84
|
+
count = 0
|
|
85
|
+
for item, exp in zip(load_many(response), expected):
|
|
86
|
+
self.assertEqual(exp, to_standard_types(item))
|
|
87
|
+
count += 1
|
|
88
|
+
self.assertEqual(count, len(expected))
|
|
89
|
+
|
|
90
|
+
def test_visit_many(self):
|
|
91
|
+
items = [{
|
|
92
|
+
"a": "a" * io.DEFAULT_BUFFER_SIZE,
|
|
93
|
+
}, {
|
|
94
|
+
"b": "b" * io.DEFAULT_BUFFER_SIZE,
|
|
95
|
+
}]
|
|
96
|
+
data = "".join(json.dumps(e) for e in items)
|
|
97
|
+
response = self._create_mock_response(data=data)
|
|
98
|
+
current_visit = []
|
|
99
|
+
visits = []
|
|
100
|
+
|
|
101
|
+
def visitor(item, path):
|
|
102
|
+
current_visit.append((item, path))
|
|
103
|
+
for _ in visit_many(response, visitor):
|
|
104
|
+
visits.append(current_visit)
|
|
105
|
+
current_visit = []
|
|
106
|
+
self.assertEqual(len(visits), len(items))
|
|
107
|
+
self.assertEqual(visits[0], [('a' * io.DEFAULT_BUFFER_SIZE, ('a',))])
|
|
108
|
+
self.assertEqual(visits[1], [('b' * io.DEFAULT_BUFFER_SIZE, ('b',))])
|
|
@@ -5,7 +5,7 @@ from json_stream_rs_tokenizer import rust_tokenizer_or_raise, ExtensionException
|
|
|
5
5
|
|
|
6
6
|
try:
|
|
7
7
|
default_tokenizer = rust_tokenizer_or_raise()
|
|
8
|
-
except ExtensionException as e:
|
|
8
|
+
except ExtensionException as e: # pragma: no cover
|
|
9
9
|
warn(str(e), category=ImportWarning) # ImportWarnings are ignored by default
|
|
10
10
|
default_tokenizer = tokenize
|
|
11
11
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from io import StringIO, BytesIO
|
|
3
|
+
from unittest import TestCase
|
|
4
|
+
|
|
5
|
+
import json_stream
|
|
6
|
+
from json_stream import to_standard_types
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestLoadMany(TestCase):
|
|
10
|
+
def test_load_many(self):
|
|
11
|
+
# NDJSON-like: one JSON value per line
|
|
12
|
+
payload = "\n".join([
|
|
13
|
+
'{"a": 1}',
|
|
14
|
+
'[1, 2]',
|
|
15
|
+
'3',
|
|
16
|
+
'true',
|
|
17
|
+
'null',
|
|
18
|
+
'"x"',
|
|
19
|
+
'{}',
|
|
20
|
+
'[]',
|
|
21
|
+
])
|
|
22
|
+
stream = StringIO(payload)
|
|
23
|
+
items = [
|
|
24
|
+
to_standard_types(v) for v in json_stream.load_many(stream, persistent=True)
|
|
25
|
+
]
|
|
26
|
+
self.assertListEqual(
|
|
27
|
+
items,
|
|
28
|
+
[
|
|
29
|
+
{"a": 1},
|
|
30
|
+
[1, 2],
|
|
31
|
+
3,
|
|
32
|
+
True,
|
|
33
|
+
None,
|
|
34
|
+
"x",
|
|
35
|
+
{},
|
|
36
|
+
[],
|
|
37
|
+
],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def test_load_many_concatenated(self):
|
|
41
|
+
payload = b'{"a": 1}[1,2]truenull"x"{}[]'
|
|
42
|
+
stream = BytesIO(payload)
|
|
43
|
+
items = [
|
|
44
|
+
to_standard_types(v) for v in json_stream.load_many(stream, persistent=False)
|
|
45
|
+
]
|
|
46
|
+
# Even when persistent=False, materialization should consume correctly per item
|
|
47
|
+
self.assertListEqual(
|
|
48
|
+
items,
|
|
49
|
+
[
|
|
50
|
+
{"a": 1},
|
|
51
|
+
[1, 2],
|
|
52
|
+
True,
|
|
53
|
+
None,
|
|
54
|
+
"x",
|
|
55
|
+
{},
|
|
56
|
+
[],
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@unittest.expectedFailure
|
|
61
|
+
def test_load_many_incompatible_concatenations(self):
|
|
62
|
+
# this fails because the tokeniser want's certain primitives to be followed by delimiter
|
|
63
|
+
payloads = [
|
|
64
|
+
[b'{"a": 1}[]', [{"a": 1}, []]], # this one works
|
|
65
|
+
[b'3true', [3, True]],
|
|
66
|
+
[b'3.0true', [3.0, True]],
|
|
67
|
+
[b'1.2e11true', [120000000000.0, True]],
|
|
68
|
+
[b'0true', [0, True]],
|
|
69
|
+
[b'""true', [0, True]],
|
|
70
|
+
]
|
|
71
|
+
errors = []
|
|
72
|
+
for payload, expected in payloads:
|
|
73
|
+
try:
|
|
74
|
+
stream = BytesIO(payload)
|
|
75
|
+
items = [
|
|
76
|
+
to_standard_types(v) for v in json_stream.load_many(stream, persistent=False)
|
|
77
|
+
]
|
|
78
|
+
self.assertListEqual(expected, items)
|
|
79
|
+
except ValueError as e:
|
|
80
|
+
errors.append(f"{payload}: {e}")
|
|
81
|
+
self.assertListEqual([], errors)
|
|
82
|
+
|
|
83
|
+
def test_load_many_skips_after_item_partially_consumed(self):
|
|
84
|
+
# Ensure that after yielding an object/array, the generator resumes and continues
|
|
85
|
+
# to the next top-level JSON text correctly.
|
|
86
|
+
payload = '{"first": [1, 2, 3]} {"second": {"x": 1}, "unconsumed": 7} 4'
|
|
87
|
+
stream = StringIO(payload)
|
|
88
|
+
|
|
89
|
+
gen = json_stream.load_many(stream, persistent=True)
|
|
90
|
+
|
|
91
|
+
first = next(gen)
|
|
92
|
+
# consume partially then fully
|
|
93
|
+
self.assertEqual(list(first["first"]), [1, 2, 3])
|
|
94
|
+
|
|
95
|
+
second = next(gen)
|
|
96
|
+
self.assertEqual(dict(second["second"].items()), {"x": 1})
|
|
97
|
+
|
|
98
|
+
third = next(gen)
|
|
99
|
+
self.assertEqual(third, 4)
|
|
100
|
+
|
|
101
|
+
with self.assertRaises(StopIteration):
|
|
102
|
+
next(gen)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from io import StringIO
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
|
|
4
|
+
import json_stream
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestVisitMany(TestCase):
|
|
8
|
+
def test_visit_many(self):
|
|
9
|
+
payload = "\n".join([
|
|
10
|
+
'{"a": 1}',
|
|
11
|
+
'[1, 2]',
|
|
12
|
+
'3',
|
|
13
|
+
'true',
|
|
14
|
+
'null',
|
|
15
|
+
'"x"',
|
|
16
|
+
'{}',
|
|
17
|
+
'[]',
|
|
18
|
+
])
|
|
19
|
+
stream = StringIO(payload)
|
|
20
|
+
|
|
21
|
+
visited_batches = []
|
|
22
|
+
current = []
|
|
23
|
+
|
|
24
|
+
def visitor(item, path):
|
|
25
|
+
current.append((item, path))
|
|
26
|
+
|
|
27
|
+
for _ in json_stream.visit_many(stream, visitor):
|
|
28
|
+
visited_batches.append(current)
|
|
29
|
+
current = []
|
|
30
|
+
|
|
31
|
+
# We only verify that per-top-level JSON text we received at least one visit
|
|
32
|
+
# and that the first visited path matches the expected top-level structure.
|
|
33
|
+
# Detailed traversal behavior is covered by other visitor tests.
|
|
34
|
+
self.assertEqual(len(visited_batches), 8)
|
|
35
|
+
self.assertEqual(visited_batches[0][0], (1, ('a',))) # {"a": 1}
|
|
36
|
+
self.assertEqual(visited_batches[1][0], (1, (0,))) # [1, 2]
|
|
37
|
+
self.assertEqual(visited_batches[2][0], (3, ())) # 3
|
|
38
|
+
self.assertEqual(visited_batches[3][0], (True, ())) # true
|
|
39
|
+
self.assertEqual(visited_batches[4][0], (None, ())) # null
|
|
40
|
+
self.assertEqual(visited_batches[5][0], ("x", ())) # "x"
|
|
41
|
+
self.assertEqual(visited_batches[6][0], ({}, ())) # {}
|
|
42
|
+
self.assertEqual(visited_batches[7][0], ([], ())) # []
|
|
@@ -7,7 +7,7 @@ from json_stream.writer import streamable_dict, streamable_list
|
|
|
7
7
|
|
|
8
8
|
class BaseTestWriter:
|
|
9
9
|
def dump(self, o):
|
|
10
|
-
raise NotImplementedError()
|
|
10
|
+
raise NotImplementedError() # pragma: no cover
|
|
11
11
|
|
|
12
12
|
def test_writer_wrapper(self):
|
|
13
13
|
def dict_content(n):
|
|
@@ -35,7 +35,7 @@ class BaseTestWriter:
|
|
|
35
35
|
@streamable_list
|
|
36
36
|
def empty_list():
|
|
37
37
|
for i in range(0): # never yields
|
|
38
|
-
yield i
|
|
38
|
+
yield i # pragma: no cover
|
|
39
39
|
|
|
40
40
|
o = empty_list()
|
|
41
41
|
result = self.dump(o)
|
|
@@ -20,12 +20,18 @@ def _visit(obj, visitor, path):
|
|
|
20
20
|
visitor(obj, path)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def
|
|
23
|
+
def visit_many(fp_or_iterator, visitor, tokenizer=default_tokenizer):
|
|
24
24
|
fp = ensure_file(fp_or_iterator)
|
|
25
25
|
token_stream = tokenizer(fp)
|
|
26
|
-
token_type, token
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
for token_type, token in token_stream:
|
|
27
|
+
if token_type == TokenType.OPERATOR:
|
|
28
|
+
obj = StreamingJSONBase.factory(token, token_stream, persistent=False)
|
|
29
|
+
_visit(obj, visitor, ())
|
|
30
|
+
obj.read_all()
|
|
31
|
+
else:
|
|
32
|
+
_visit(token, visitor, ())
|
|
33
|
+
yield
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def visit(fp_or_iterator, visitor, tokenizer=default_tokenizer):
|
|
37
|
+
next(visit_many(fp_or_iterator, visitor, tokenizer))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: json-stream
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -61,8 +61,8 @@ Simple streaming JSON parser and encoder.
|
|
|
61
61
|
|
|
62
62
|
When [reading](#reading) JSON data, `json-stream` can decode JSON data in
|
|
63
63
|
a streaming manner, providing a pythonic dict/list-like interface, or a
|
|
64
|
-
[visitor-based
|
|
65
|
-
or [iterators](#iterators).
|
|
64
|
+
[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
66
|
|
|
67
67
|
When [writing](#writing) JSON data, `json-stream` can stream JSON objects
|
|
68
68
|
as you generate them.
|
|
@@ -278,6 +278,65 @@ z at path ('xxxx', 3)
|
|
|
278
278
|
[] at path ('xxxx', 5)
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
+
### <a id="multiple"></a> Multiple JSON documents: `load_many()` and `visit_many()`
|
|
282
|
+
|
|
283
|
+
Sometimes JSON data arrives as a sequence of top‑level JSON texts rather than a single array/object. json-stream supports this pattern with:
|
|
284
|
+
- `json_stream.load_many(...)`: yields each top-level JSON value as it is parsed.
|
|
285
|
+
- `json_stream.visit_many(...)`: visits each top-level JSON value and yields control after each one.
|
|
286
|
+
|
|
287
|
+
These functions are useful for common streaming formats:
|
|
288
|
+
- 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".
|
|
289
|
+
- Concatenated or sequential JSON documents: complete JSON texts written back-to-back without delimiters.
|
|
290
|
+
|
|
291
|
+
Note about concatenated JSON without explicit delimiters:
|
|
292
|
+
|
|
293
|
+
When multiple top‑level JSON documents are simply concatenated with
|
|
294
|
+
no delimiters (no newlines or no spaces), json-stream can unambiguously
|
|
295
|
+
detect the document boundaries for the following top-level values:
|
|
296
|
+
- objects: `{ ... }`
|
|
297
|
+
- arrays: `[ ... ]`
|
|
298
|
+
- literals: `true`, `false`, `null`
|
|
299
|
+
|
|
300
|
+
However, numbers and strings require a delimiter between consecutive
|
|
301
|
+
documents so the tokenizer can tell where one ends and the next begins.
|
|
302
|
+
A delimiter can be as simple as a single whitespace or a newline.
|
|
303
|
+
|
|
304
|
+
Examples:
|
|
305
|
+
- Valid without delimiters (will parse as 2 docs):
|
|
306
|
+
- `{"a":1}{"b":2}` → `{...}`, `{...}`
|
|
307
|
+
- `[1][2]` → `[1]`, `[2]`
|
|
308
|
+
- `truefalse` → `true`, `false`
|
|
309
|
+
- `null[]` → `null`, `[]`
|
|
310
|
+
- These will error without a delimiter between documents (e.g. a space or newline):
|
|
311
|
+
- `3true`
|
|
312
|
+
- `0"x"`
|
|
313
|
+
- `"hi""there"
|
|
314
|
+
|
|
315
|
+
#### Examples
|
|
316
|
+
|
|
317
|
+
Read many from a file containing NDJSON or concatenated JSON:
|
|
318
|
+
```python
|
|
319
|
+
import json_stream
|
|
320
|
+
|
|
321
|
+
with open("events.ndjson", "rb") as f: # NDJSON: one JSON object per line
|
|
322
|
+
for item in json_stream.load_many(f):
|
|
323
|
+
# each item is the top-level item in each JSON text
|
|
324
|
+
handle(item)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Visit many values in many documents:
|
|
328
|
+
```python
|
|
329
|
+
import json_stream
|
|
330
|
+
|
|
331
|
+
def visitor(value, path):
|
|
332
|
+
# called depth-first for each value within the current top-level JSON text
|
|
333
|
+
...
|
|
334
|
+
|
|
335
|
+
for _ in json_stream.visit_many(open("events.ndjson", "rb"), visitor):
|
|
336
|
+
# loop advances one top-level JSON text at a time
|
|
337
|
+
pass
|
|
338
|
+
```
|
|
339
|
+
|
|
281
340
|
### <a id="urls"></a> Stream a URL
|
|
282
341
|
|
|
283
342
|
`json_stream` knows how to stream directly from a URL using a variety of packages.
|
|
@@ -29,9 +29,11 @@ src/json_stream/tests/__init__.py
|
|
|
29
29
|
src/json_stream/tests/test_buffering.py
|
|
30
30
|
src/json_stream/tests/test_collection_abcs.py
|
|
31
31
|
src/json_stream/tests/test_iterators.py
|
|
32
|
+
src/json_stream/tests/test_load_many.py
|
|
32
33
|
src/json_stream/tests/test_loader.py
|
|
33
34
|
src/json_stream/tests/test_tokenizer.py
|
|
34
35
|
src/json_stream/tests/test_tokenizer_integration.py
|
|
35
36
|
src/json_stream/tests/test_util.py
|
|
37
|
+
src/json_stream/tests/test_visit_many.py
|
|
36
38
|
src/json_stream/tests/test_visitor.py
|
|
37
39
|
src/json_stream/tests/test_writer.py
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
fp = ensure_file(fp_or_iterable)
|
|
8
|
-
token_stream = tokenizer(fp)
|
|
9
|
-
token_type, token = next(token_stream)
|
|
10
|
-
if token_type == TokenType.OPERATOR:
|
|
11
|
-
return StreamingJSONBase.factory(token, token_stream, persistent)
|
|
12
|
-
return token
|
|
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
|