streaming-json-parser 0.2.0__tar.gz → 0.2.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.
- streaming_json_parser-0.2.1/PKG-INFO +232 -0
- streaming_json_parser-0.2.1/README.md +175 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/pyproject.toml +4 -2
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser/high_performance_parser.py +47 -4
- streaming_json_parser-0.2.1/src/streaming_json_parser.egg-info/PKG-INFO +232 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser.egg-info/SOURCES.txt +2 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser.egg-info/requires.txt +1 -1
- streaming_json_parser-0.2.1/tests/test_benchmark_charts.py +52 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_benchmark_snapshot.py +79 -0
- streaming_json_parser-0.2.1/tests/test_community_benchmark.py +125 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_decode_helpers.py +41 -0
- streaming_json_parser-0.2.0/PKG-INFO +0 -214
- streaming_json_parser-0.2.0/README.md +0 -159
- streaming_json_parser-0.2.0/src/streaming_json_parser.egg-info/PKG-INFO +0 -214
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/LICENSE +0 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/setup.cfg +0 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser/__init__.py +0 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser.egg-info/dependency_links.txt +0 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser.egg-info/top_level.txt +0 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_high_performance_parser.py +0 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_native_incremental.py +0 -0
- {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_packaging.py +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: streaming-json-parser
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: High-performance JSON decoding and true incremental parsing for Python streams.
|
|
5
|
+
Author-email: Aramis Facchinetti <aramis.facchinetti16@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aramisfacchinetti/streaming-json-parser
|
|
8
|
+
Project-URL: Repository, https://github.com/aramisfacchinetti/streaming-json-parser
|
|
9
|
+
Project-URL: Issues, https://github.com/aramisfacchinetti/streaming-json-parser/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/aramisfacchinetti/streaming-json-parser/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Contributing, https://github.com/aramisfacchinetti/streaming-json-parser/blob/main/CONTRIBUTING.md
|
|
12
|
+
Keywords: streaming,json,parser,llm,large language model,incremental parsing
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Development Status :: 4 - Beta
|
|
21
|
+
Classifier: Intended Audience :: Developers
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Text Processing
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=9.0.3; extra == "test"
|
|
29
|
+
Requires-Dist: msgspec>=0.18; extra == "test"
|
|
30
|
+
Requires-Dist: pydantic-core>=2.0; extra == "test"
|
|
31
|
+
Requires-Dist: jiter>=0.12; extra == "test"
|
|
32
|
+
Provides-Extra: accelerated
|
|
33
|
+
Requires-Dist: msgspec>=0.18; extra == "accelerated"
|
|
34
|
+
Requires-Dist: orjson>=3.9; extra == "accelerated"
|
|
35
|
+
Requires-Dist: pysimdjson>=5; extra == "accelerated"
|
|
36
|
+
Requires-Dist: ujson>=5; extra == "accelerated"
|
|
37
|
+
Requires-Dist: python-rapidjson>=1.23; extra == "accelerated"
|
|
38
|
+
Requires-Dist: yyjson>=4; extra == "accelerated"
|
|
39
|
+
Provides-Extra: partial
|
|
40
|
+
Requires-Dist: pydantic-core>=2.0; extra == "partial"
|
|
41
|
+
Requires-Dist: jiter>=0.12; extra == "partial"
|
|
42
|
+
Provides-Extra: benchmark
|
|
43
|
+
Requires-Dist: ijson>=3.3; extra == "benchmark"
|
|
44
|
+
Requires-Dist: partial-json-parser>=0.2; extra == "benchmark"
|
|
45
|
+
Requires-Dist: partialjson>=1.1; extra == "benchmark"
|
|
46
|
+
Requires-Dist: json-repair>=0.63; extra == "benchmark"
|
|
47
|
+
Requires-Dist: jsonriver>=1.0; extra == "benchmark"
|
|
48
|
+
Requires-Dist: untruncate-json>=1.1; extra == "benchmark"
|
|
49
|
+
Requires-Dist: ujson>=5; extra == "benchmark"
|
|
50
|
+
Requires-Dist: python-rapidjson>=1.23; extra == "benchmark"
|
|
51
|
+
Requires-Dist: yyjson>=4; extra == "benchmark"
|
|
52
|
+
Requires-Dist: pydantic-core>=2.0; extra == "benchmark"
|
|
53
|
+
Requires-Dist: jiter>=0.12; extra == "benchmark"
|
|
54
|
+
Provides-Extra: native-build
|
|
55
|
+
Requires-Dist: maturin<2,>=1; extra == "native-build"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+
# Streaming JSON Parser
|
|
59
|
+
|
|
60
|
+
**Decode complete JSON documents or parse JSON as chunks arrive.** `streaming-json-parser` provides a strict incremental parser with partial values and explicit parse states, alongside complete-document decoding, NDJSON, and selective extraction for Python.
|
|
61
|
+
|
|
62
|
+
[](https://github.com/aramisfacchinetti/streaming-json-parser/actions/workflows/test.yml)
|
|
63
|
+
[](https://pypi.org/project/streaming-json-parser/)
|
|
64
|
+
[](https://pypi.org/project/streaming-json-parser/)
|
|
65
|
+
[](LICENSE)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m pip install streaming-json-parser
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quickstart: parse while the stream is still arriving
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from streaming_json_parser import ParseStatus, StreamingJsonParser
|
|
75
|
+
|
|
76
|
+
parser = StreamingJsonParser()
|
|
77
|
+
|
|
78
|
+
partial = parser.feed(b'{"message":"hel')
|
|
79
|
+
assert partial.status is ParseStatus.PARTIAL
|
|
80
|
+
assert partial.value == {"message": "hel"}
|
|
81
|
+
|
|
82
|
+
complete = parser.feed(b'lo"}')
|
|
83
|
+
assert complete.status is ParseStatus.COMPLETE
|
|
84
|
+
assert complete.value == {"message": "hello"}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The parser resumes from its previous state on each chunk. It distinguishes `EMPTY`, `PARTIAL`, `COMPLETE`, and `INVALID` results; partial strings are available before the closing quote arrives. By default, `.value` refers to parser-owned state and may change after later chunks; pass `copy_value=True` to `feed()` when you need a retained snapshot.
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
## What it supports
|
|
92
|
+
|
|
93
|
+
- Strict incremental parsing across arbitrary input chunks, with partial values and explicit statuses.
|
|
94
|
+
- Complete JSON decoding, including reusable and workload-tuned decoders.
|
|
95
|
+
- NDJSON decoding and streaming records.
|
|
96
|
+
- Selective path extraction from complete JSON documents and NDJSON without always building every Python object.
|
|
97
|
+
- Structural partial snapshots for callers that do not need unfinished string values. This is a separate finishing mode, not the strict incremental state machine.
|
|
98
|
+
- Optional Rust acceleration through `streaming-json-parser-native`.
|
|
99
|
+
|
|
100
|
+
The Python implementation works without optional packages. The project is currently in beta while its public API settles.
|
|
101
|
+
|
|
102
|
+
## Performance
|
|
103
|
+
|
|
104
|
+
These charts show three separate workloads from the checked-in benchmark snapshot. Bars report total process CPU time for each repeated batch; **lower is better**. Every measured result is shown. Values are generated from [`docs/benchmark-snapshot.json`](docs/benchmark-snapshot.json), and the environment, versions, workload sizes, and timing method are recorded in the [benchmark report](docs/benchmark-snapshot.md).
|
|
105
|
+
|
|
106
|
+
### Complete JSON decoding
|
|
107
|
+
|
|
108
|
+
Decodes the same roughly 1 MB JSON object into a complete value. This is ordinary whole-document decoding; it does not measure incremental parsing.
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
### Complete-document selective extraction
|
|
113
|
+
|
|
114
|
+
Reads the same two object paths (`meta.name` and `tail.count`) from a 5,000-row JSON document. The full-decode baselines decode the document before accessing those paths.
|
|
115
|
+
|
|
116
|
+

|
|
117
|
+
|
|
118
|
+
### NDJSON selective extraction
|
|
119
|
+
|
|
120
|
+
Reads `row.id` and `row.value` from each of 5,000 newline-delimited records. Full-decode baselines materialize each record before selecting its fields.
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
These charts compare operations with the same output paths within each workload. Strict incremental parsers, structural partial finishers, and permissive JSON-repair tools have different semantics, so they are measured separately in the [API scorecard](docs/current-api-scorecard.md) and [partial-strategy benchmark harness](scripts/benchmark_partial_strategy_matrix.py).
|
|
125
|
+
|
|
126
|
+
### Real-world Python JSON benchmark corpus
|
|
127
|
+
|
|
128
|
+
This additional chart adapts the complete-document load workload and public datasets from the community-maintained [TkTech JSON benchmark](https://github.com/TkTech/json_benchmark). It decodes six whole documents into ordinary Python values and checks every included decoder against `json.loads` before timing. Each panel has its own scale and reports input throughput; **higher is better**. This is a Python community benchmark, not a formal industry standard. The suite's SAX/event streaming cases are omitted because they do not produce the same output as this parser.
|
|
129
|
+
|
|
130
|
+

|
|
131
|
+
|
|
132
|
+
Results vary by dataset and decoder; compare the per-file values, dataset hashes, package versions, methodology, and upstream revision in the [community corpus report](docs/community-json-benchmark.md) and [JSON snapshot](docs/community-json-benchmark.json). For an already-complete document, use a complete-document API. Sending it through `StreamingJsonParser.feed()` also performs incremental state handling, so it is not a substitute for `decode_complete_json()`. These corpus timings are separate from the generated synthetic workloads above.
|
|
133
|
+
|
|
134
|
+
## Choose an API
|
|
135
|
+
|
|
136
|
+
| Workload | API | Notes |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| One complete JSON document | `decode_complete_json(data)` | Returns a regular Python value. |
|
|
139
|
+
| Repeated complete-document decoding | `make_tuned_complete_json_decoder(...)` | Calibrates against a representative sample and payload size. |
|
|
140
|
+
| JSON arriving in chunks | `StreamingJsonParser` | Strict resumable parser; inspect `ParseResult.status` and `.value` after each `feed()`. |
|
|
141
|
+
| Newline-delimited records | `decode_ndjson(data)` or `StreamingJsonParser(framing="ndjson")` | Call `finish()` to consume a final record without a newline. |
|
|
142
|
+
| Incomplete prefix; unfinished strings can be omitted | `decode_structural_partial_json(prefix)` | Structural finisher; not equivalent to strict incremental parsing. |
|
|
143
|
+
| A few fields from JSON or NDJSON | `make_tuned_json_path_extractor(..., framing="single" or "ndjson")` | Reuse the extractor for a stable workload. |
|
|
144
|
+
| Typed records | `make_ndjson_decoder(record_type=...)` | Optional typed decoding through `msgspec`. |
|
|
145
|
+
|
|
146
|
+
There is no single best backend for every input. The tuned factories can benchmark compatible backends once during setup when given a representative `sample` and `payload_size_hint`.
|
|
147
|
+
|
|
148
|
+
## Common operations
|
|
149
|
+
|
|
150
|
+
### Decode a complete document
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from streaming_json_parser import decode_complete_json
|
|
154
|
+
|
|
155
|
+
value = decode_complete_json(b'{"name":"example","ok":true}')
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For large read-only payloads, `decode_complete_json_view()` can return a view backed by `simdjson`; use it only when proxy/view semantics are suitable for your application.
|
|
159
|
+
|
|
160
|
+
### Read NDJSON records
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from streaming_json_parser import decode_ndjson
|
|
164
|
+
|
|
165
|
+
records = decode_ndjson(b'{"id":1}\n{"id":2}\n')
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The stateful parser also supports `framing="ndjson"` and `poll_many()` to drain complete records as they become available.
|
|
169
|
+
|
|
170
|
+
### Finish a structural partial value
|
|
171
|
+
|
|
172
|
+
Structural partial mode is useful when a caller has an incomplete prefix and needs completed objects or arrays from it. By default, it omits an unfinished trailing string; use `trailing_strings=True` or `partial_mode="structural_trailing_strings"` when that text must be retained. It may complete scalar prefixes differently from the strict parser.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from streaming_json_parser import decode_structural_partial_json
|
|
176
|
+
|
|
177
|
+
value = decode_structural_partial_json('{"items":[1,2')
|
|
178
|
+
assert value == {"items": [1, 2]}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Optional acceleration
|
|
182
|
+
|
|
183
|
+
Install the Rust extension separately when a compatible wheel is available:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
python -m pip install streaming-json-parser-native
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The native package is optional. Backend-specific packages such as `msgspec`, `orjson`, and `simdjson` are also optional; APIs fall back to the Python implementation where applicable.
|
|
190
|
+
|
|
191
|
+
## Reproduce the benchmarks
|
|
192
|
+
|
|
193
|
+
The benchmark snapshot includes the date, source revision and dirty-state flag, Python and platform details, processor and architecture, installed benchmark-package versions, native-extension availability, payload sizes, record counts, and selected paths. The methodology records its clock, warm-up, sample count, and repetitions.
|
|
194
|
+
|
|
195
|
+
To run the benchmark with the optional alternatives installed, then regenerate all reports and SVGs:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
python -m pip install -e '.[test,benchmark,accelerated]'
|
|
199
|
+
python -m pip install 'streaming-json-parser-native==0.2.0'
|
|
200
|
+
make benchmark-artifacts
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The native package is separate from the Python extras. Install it to reproduce the snapshot's native benchmark rows; the snapshot records the exact optional package versions used.
|
|
204
|
+
|
|
205
|
+
To run the adapted whole-document benchmark against the public corpus, clone the benchmark data and pass its `data/` directory. This is separate from `make benchmark-artifacts` so the everyday benchmark does not download external files:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
git clone --depth 1 https://github.com/TkTech/json_benchmark.git /tmp/tktech-json-benchmark
|
|
209
|
+
make benchmark-community-corpus COMMUNITY_JSON_CORPUS_DIR=/tmp/tktech-json-benchmark/data
|
|
210
|
+
make verify-community-benchmark-artifacts
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
To verify generated Markdown, scorecard, charts, and the current benchmark results against the committed snapshot:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
make verify-benchmark-artifacts
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Verification checks generated files against the authoritative JSON snapshot and reruns the same benchmark slices. Timing checks allow shared machine slowdowns, but detect large per-implementation regressions. For meaningful comparisons, use the same dependency versions and a similar machine; compare the provenance fields in the snapshot.
|
|
220
|
+
|
|
221
|
+
## Development
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
python -m pip install -e '.[test]'
|
|
225
|
+
python -m pytest -q
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
See [contributing guidance](CONTRIBUTING.md), the [changelog](CHANGELOG.md), the [current API scorecard](docs/current-api-scorecard.md), [open issues](https://github.com/aramisfacchinetti/streaming-json-parser/issues), and [manual GitHub settings follow-up](docs/github-settings-follow-up.md).
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Streaming JSON Parser
|
|
2
|
+
|
|
3
|
+
**Decode complete JSON documents or parse JSON as chunks arrive.** `streaming-json-parser` provides a strict incremental parser with partial values and explicit parse states, alongside complete-document decoding, NDJSON, and selective extraction for Python.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/aramisfacchinetti/streaming-json-parser/actions/workflows/test.yml)
|
|
6
|
+
[](https://pypi.org/project/streaming-json-parser/)
|
|
7
|
+
[](https://pypi.org/project/streaming-json-parser/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python -m pip install streaming-json-parser
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart: parse while the stream is still arriving
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from streaming_json_parser import ParseStatus, StreamingJsonParser
|
|
18
|
+
|
|
19
|
+
parser = StreamingJsonParser()
|
|
20
|
+
|
|
21
|
+
partial = parser.feed(b'{"message":"hel')
|
|
22
|
+
assert partial.status is ParseStatus.PARTIAL
|
|
23
|
+
assert partial.value == {"message": "hel"}
|
|
24
|
+
|
|
25
|
+
complete = parser.feed(b'lo"}')
|
|
26
|
+
assert complete.status is ParseStatus.COMPLETE
|
|
27
|
+
assert complete.value == {"message": "hello"}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The parser resumes from its previous state on each chunk. It distinguishes `EMPTY`, `PARTIAL`, `COMPLETE`, and `INVALID` results; partial strings are available before the closing quote arrives. By default, `.value` refers to parser-owned state and may change after later chunks; pass `copy_value=True` to `feed()` when you need a retained snapshot.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
## What it supports
|
|
35
|
+
|
|
36
|
+
- Strict incremental parsing across arbitrary input chunks, with partial values and explicit statuses.
|
|
37
|
+
- Complete JSON decoding, including reusable and workload-tuned decoders.
|
|
38
|
+
- NDJSON decoding and streaming records.
|
|
39
|
+
- Selective path extraction from complete JSON documents and NDJSON without always building every Python object.
|
|
40
|
+
- Structural partial snapshots for callers that do not need unfinished string values. This is a separate finishing mode, not the strict incremental state machine.
|
|
41
|
+
- Optional Rust acceleration through `streaming-json-parser-native`.
|
|
42
|
+
|
|
43
|
+
The Python implementation works without optional packages. The project is currently in beta while its public API settles.
|
|
44
|
+
|
|
45
|
+
## Performance
|
|
46
|
+
|
|
47
|
+
These charts show three separate workloads from the checked-in benchmark snapshot. Bars report total process CPU time for each repeated batch; **lower is better**. Every measured result is shown. Values are generated from [`docs/benchmark-snapshot.json`](docs/benchmark-snapshot.json), and the environment, versions, workload sizes, and timing method are recorded in the [benchmark report](docs/benchmark-snapshot.md).
|
|
48
|
+
|
|
49
|
+
### Complete JSON decoding
|
|
50
|
+
|
|
51
|
+
Decodes the same roughly 1 MB JSON object into a complete value. This is ordinary whole-document decoding; it does not measure incremental parsing.
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
### Complete-document selective extraction
|
|
56
|
+
|
|
57
|
+
Reads the same two object paths (`meta.name` and `tail.count`) from a 5,000-row JSON document. The full-decode baselines decode the document before accessing those paths.
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
### NDJSON selective extraction
|
|
62
|
+
|
|
63
|
+
Reads `row.id` and `row.value` from each of 5,000 newline-delimited records. Full-decode baselines materialize each record before selecting its fields.
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
These charts compare operations with the same output paths within each workload. Strict incremental parsers, structural partial finishers, and permissive JSON-repair tools have different semantics, so they are measured separately in the [API scorecard](docs/current-api-scorecard.md) and [partial-strategy benchmark harness](scripts/benchmark_partial_strategy_matrix.py).
|
|
68
|
+
|
|
69
|
+
### Real-world Python JSON benchmark corpus
|
|
70
|
+
|
|
71
|
+
This additional chart adapts the complete-document load workload and public datasets from the community-maintained [TkTech JSON benchmark](https://github.com/TkTech/json_benchmark). It decodes six whole documents into ordinary Python values and checks every included decoder against `json.loads` before timing. Each panel has its own scale and reports input throughput; **higher is better**. This is a Python community benchmark, not a formal industry standard. The suite's SAX/event streaming cases are omitted because they do not produce the same output as this parser.
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
Results vary by dataset and decoder; compare the per-file values, dataset hashes, package versions, methodology, and upstream revision in the [community corpus report](docs/community-json-benchmark.md) and [JSON snapshot](docs/community-json-benchmark.json). For an already-complete document, use a complete-document API. Sending it through `StreamingJsonParser.feed()` also performs incremental state handling, so it is not a substitute for `decode_complete_json()`. These corpus timings are separate from the generated synthetic workloads above.
|
|
76
|
+
|
|
77
|
+
## Choose an API
|
|
78
|
+
|
|
79
|
+
| Workload | API | Notes |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| One complete JSON document | `decode_complete_json(data)` | Returns a regular Python value. |
|
|
82
|
+
| Repeated complete-document decoding | `make_tuned_complete_json_decoder(...)` | Calibrates against a representative sample and payload size. |
|
|
83
|
+
| JSON arriving in chunks | `StreamingJsonParser` | Strict resumable parser; inspect `ParseResult.status` and `.value` after each `feed()`. |
|
|
84
|
+
| Newline-delimited records | `decode_ndjson(data)` or `StreamingJsonParser(framing="ndjson")` | Call `finish()` to consume a final record without a newline. |
|
|
85
|
+
| Incomplete prefix; unfinished strings can be omitted | `decode_structural_partial_json(prefix)` | Structural finisher; not equivalent to strict incremental parsing. |
|
|
86
|
+
| A few fields from JSON or NDJSON | `make_tuned_json_path_extractor(..., framing="single" or "ndjson")` | Reuse the extractor for a stable workload. |
|
|
87
|
+
| Typed records | `make_ndjson_decoder(record_type=...)` | Optional typed decoding through `msgspec`. |
|
|
88
|
+
|
|
89
|
+
There is no single best backend for every input. The tuned factories can benchmark compatible backends once during setup when given a representative `sample` and `payload_size_hint`.
|
|
90
|
+
|
|
91
|
+
## Common operations
|
|
92
|
+
|
|
93
|
+
### Decode a complete document
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from streaming_json_parser import decode_complete_json
|
|
97
|
+
|
|
98
|
+
value = decode_complete_json(b'{"name":"example","ok":true}')
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For large read-only payloads, `decode_complete_json_view()` can return a view backed by `simdjson`; use it only when proxy/view semantics are suitable for your application.
|
|
102
|
+
|
|
103
|
+
### Read NDJSON records
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from streaming_json_parser import decode_ndjson
|
|
107
|
+
|
|
108
|
+
records = decode_ndjson(b'{"id":1}\n{"id":2}\n')
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The stateful parser also supports `framing="ndjson"` and `poll_many()` to drain complete records as they become available.
|
|
112
|
+
|
|
113
|
+
### Finish a structural partial value
|
|
114
|
+
|
|
115
|
+
Structural partial mode is useful when a caller has an incomplete prefix and needs completed objects or arrays from it. By default, it omits an unfinished trailing string; use `trailing_strings=True` or `partial_mode="structural_trailing_strings"` when that text must be retained. It may complete scalar prefixes differently from the strict parser.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from streaming_json_parser import decode_structural_partial_json
|
|
119
|
+
|
|
120
|
+
value = decode_structural_partial_json('{"items":[1,2')
|
|
121
|
+
assert value == {"items": [1, 2]}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Optional acceleration
|
|
125
|
+
|
|
126
|
+
Install the Rust extension separately when a compatible wheel is available:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python -m pip install streaming-json-parser-native
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The native package is optional. Backend-specific packages such as `msgspec`, `orjson`, and `simdjson` are also optional; APIs fall back to the Python implementation where applicable.
|
|
133
|
+
|
|
134
|
+
## Reproduce the benchmarks
|
|
135
|
+
|
|
136
|
+
The benchmark snapshot includes the date, source revision and dirty-state flag, Python and platform details, processor and architecture, installed benchmark-package versions, native-extension availability, payload sizes, record counts, and selected paths. The methodology records its clock, warm-up, sample count, and repetitions.
|
|
137
|
+
|
|
138
|
+
To run the benchmark with the optional alternatives installed, then regenerate all reports and SVGs:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
python -m pip install -e '.[test,benchmark,accelerated]'
|
|
142
|
+
python -m pip install 'streaming-json-parser-native==0.2.0'
|
|
143
|
+
make benchmark-artifacts
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The native package is separate from the Python extras. Install it to reproduce the snapshot's native benchmark rows; the snapshot records the exact optional package versions used.
|
|
147
|
+
|
|
148
|
+
To run the adapted whole-document benchmark against the public corpus, clone the benchmark data and pass its `data/` directory. This is separate from `make benchmark-artifacts` so the everyday benchmark does not download external files:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
git clone --depth 1 https://github.com/TkTech/json_benchmark.git /tmp/tktech-json-benchmark
|
|
152
|
+
make benchmark-community-corpus COMMUNITY_JSON_CORPUS_DIR=/tmp/tktech-json-benchmark/data
|
|
153
|
+
make verify-community-benchmark-artifacts
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
To verify generated Markdown, scorecard, charts, and the current benchmark results against the committed snapshot:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
make verify-benchmark-artifacts
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Verification checks generated files against the authoritative JSON snapshot and reruns the same benchmark slices. Timing checks allow shared machine slowdowns, but detect large per-implementation regressions. For meaningful comparisons, use the same dependency versions and a similar machine; compare the provenance fields in the snapshot.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
python -m pip install -e '.[test]'
|
|
168
|
+
python -m pytest -q
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
See [contributing guidance](CONTRIBUTING.md), the [changelog](CHANGELOG.md), the [current API scorecard](docs/current-api-scorecard.md), [open issues](https://github.com/aramisfacchinetti/streaming-json-parser/issues), and [manual GitHub settings follow-up](docs/github-settings-follow-up.md).
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "streaming-json-parser"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Aramis Facchinetti", email = "aramis.facchinetti16@gmail.com" },
|
|
10
10
|
]
|
|
@@ -33,7 +33,7 @@ keywords = ["streaming", "json", "parser", "llm", "large language model", "incre
|
|
|
33
33
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
test = [
|
|
36
|
-
"pytest>=
|
|
36
|
+
"pytest>=9.0.3",
|
|
37
37
|
"msgspec>=0.18",
|
|
38
38
|
"pydantic-core>=2.0",
|
|
39
39
|
"jiter>=0.12",
|
|
@@ -71,6 +71,8 @@ native-build = [
|
|
|
71
71
|
Homepage = "https://github.com/aramisfacchinetti/streaming-json-parser"
|
|
72
72
|
Repository = "https://github.com/aramisfacchinetti/streaming-json-parser"
|
|
73
73
|
Issues = "https://github.com/aramisfacchinetti/streaming-json-parser/issues"
|
|
74
|
+
Changelog = "https://github.com/aramisfacchinetti/streaming-json-parser/blob/main/CHANGELOG.md"
|
|
75
|
+
Contributing = "https://github.com/aramisfacchinetti/streaming-json-parser/blob/main/CONTRIBUTING.md"
|
|
74
76
|
|
|
75
77
|
[tool.setuptools.packages.find]
|
|
76
78
|
where = ["src"]
|
|
@@ -302,6 +302,32 @@ def _has_dense_json_escapes_text(payload: str) -> bool:
|
|
|
302
302
|
)
|
|
303
303
|
|
|
304
304
|
|
|
305
|
+
def _has_non_ascii_unicode_escape(payload: str | bytes | bytearray) -> bool:
|
|
306
|
+
"""Return whether a JSON string contains a ``\\u`` escape above ASCII."""
|
|
307
|
+
is_bytes = isinstance(payload, (bytes, bytearray))
|
|
308
|
+
backslash = ord("\\") if is_bytes else "\\"
|
|
309
|
+
marker = b"\\u" if is_bytes else r"\u"
|
|
310
|
+
index = 0
|
|
311
|
+
while True:
|
|
312
|
+
index = payload.find(marker, index)
|
|
313
|
+
if index < 0:
|
|
314
|
+
return False
|
|
315
|
+
preceding_backslashes = 0
|
|
316
|
+
cursor = index - 1
|
|
317
|
+
while cursor >= 0 and payload[cursor] == backslash:
|
|
318
|
+
preceding_backslashes += 1
|
|
319
|
+
cursor -= 1
|
|
320
|
+
if preceding_backslashes % 2 == 0 and index + 6 <= len(payload):
|
|
321
|
+
try:
|
|
322
|
+
codepoint = int(payload[index + 2:index + 6], 16)
|
|
323
|
+
except ValueError:
|
|
324
|
+
pass
|
|
325
|
+
else:
|
|
326
|
+
if codepoint > 0x7F:
|
|
327
|
+
return True
|
|
328
|
+
index += 2
|
|
329
|
+
|
|
330
|
+
|
|
305
331
|
def _looks_plain_ascii_text(payload: str) -> bool:
|
|
306
332
|
if len(payload) <= _NDJSON_ESCAPE_PROBE_BYTES:
|
|
307
333
|
return payload.isascii() and "\\" not in payload
|
|
@@ -1254,7 +1280,12 @@ def _select_complete_decoder(
|
|
|
1254
1280
|
if decoder is not None:
|
|
1255
1281
|
return decoder.decode
|
|
1256
1282
|
if b"\\" in payload and _has_dense_json_escapes(payload):
|
|
1257
|
-
if
|
|
1283
|
+
if (
|
|
1284
|
+
_backend_yyjson is not None
|
|
1285
|
+
and isinstance(payload, bytes)
|
|
1286
|
+
and payload.isascii()
|
|
1287
|
+
and not _has_non_ascii_unicode_escape(payload)
|
|
1288
|
+
):
|
|
1258
1289
|
return _backend_yyjson.loads
|
|
1259
1290
|
if _backend_orjson is not None:
|
|
1260
1291
|
return _backend_orjson.loads
|
|
@@ -1347,7 +1378,11 @@ def _select_complete_decoder(
|
|
|
1347
1378
|
return _GLOBAL_MSGSPEC_DECODER.decode
|
|
1348
1379
|
if _GLOBAL_SIMD_PARSER is not None:
|
|
1349
1380
|
return _decode_simdjson_bytes
|
|
1350
|
-
if
|
|
1381
|
+
if (
|
|
1382
|
+
_backend_yyjson is not None
|
|
1383
|
+
and payload.isascii()
|
|
1384
|
+
and not _has_non_ascii_unicode_escape(payload)
|
|
1385
|
+
):
|
|
1351
1386
|
return _backend_yyjson.loads
|
|
1352
1387
|
if _backend_orjson is not None:
|
|
1353
1388
|
return _backend_orjson.loads
|
|
@@ -1362,7 +1397,11 @@ def _select_complete_decoder_text(data: str, value_type: Any | None = None) -> A
|
|
|
1362
1397
|
if decoder is not None:
|
|
1363
1398
|
return decoder.decode
|
|
1364
1399
|
if "\\" in data and _has_dense_json_escapes_text(data):
|
|
1365
|
-
if
|
|
1400
|
+
if (
|
|
1401
|
+
_backend_yyjson is not None
|
|
1402
|
+
and data.isascii()
|
|
1403
|
+
and not _has_non_ascii_unicode_escape(data)
|
|
1404
|
+
):
|
|
1366
1405
|
return _backend_yyjson.loads
|
|
1367
1406
|
if _backend_orjson is not None:
|
|
1368
1407
|
return _backend_orjson.loads
|
|
@@ -1455,7 +1494,11 @@ def _select_complete_decoder_text(data: str, value_type: Any | None = None) -> A
|
|
|
1455
1494
|
return _GLOBAL_MSGSPEC_DECODER.decode
|
|
1456
1495
|
if _GLOBAL_SIMD_PARSER is not None:
|
|
1457
1496
|
return _decode_simdjson_bytes
|
|
1458
|
-
if
|
|
1497
|
+
if (
|
|
1498
|
+
_backend_yyjson is not None
|
|
1499
|
+
and data.isascii()
|
|
1500
|
+
and not _has_non_ascii_unicode_escape(data)
|
|
1501
|
+
):
|
|
1459
1502
|
return _backend_yyjson.loads
|
|
1460
1503
|
if _backend_orjson is not None:
|
|
1461
1504
|
return _backend_orjson.loads
|