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.
Files changed (22) hide show
  1. streaming_json_parser-0.2.1/PKG-INFO +232 -0
  2. streaming_json_parser-0.2.1/README.md +175 -0
  3. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/pyproject.toml +4 -2
  4. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser/high_performance_parser.py +47 -4
  5. streaming_json_parser-0.2.1/src/streaming_json_parser.egg-info/PKG-INFO +232 -0
  6. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser.egg-info/SOURCES.txt +2 -0
  7. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser.egg-info/requires.txt +1 -1
  8. streaming_json_parser-0.2.1/tests/test_benchmark_charts.py +52 -0
  9. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_benchmark_snapshot.py +79 -0
  10. streaming_json_parser-0.2.1/tests/test_community_benchmark.py +125 -0
  11. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_decode_helpers.py +41 -0
  12. streaming_json_parser-0.2.0/PKG-INFO +0 -214
  13. streaming_json_parser-0.2.0/README.md +0 -159
  14. streaming_json_parser-0.2.0/src/streaming_json_parser.egg-info/PKG-INFO +0 -214
  15. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/LICENSE +0 -0
  16. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/setup.cfg +0 -0
  17. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser/__init__.py +0 -0
  18. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser.egg-info/dependency_links.txt +0 -0
  19. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/src/streaming_json_parser.egg-info/top_level.txt +0 -0
  20. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_high_performance_parser.py +0 -0
  21. {streaming_json_parser-0.2.0 → streaming_json_parser-0.2.1}/tests/test_native_incremental.py +0 -0
  22. {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
+ [![CI](https://github.com/aramisfacchinetti/streaming-json-parser/actions/workflows/test.yml/badge.svg)](https://github.com/aramisfacchinetti/streaming-json-parser/actions/workflows/test.yml)
63
+ [![PyPI](https://img.shields.io/pypi/v/streaming-json-parser)](https://pypi.org/project/streaming-json-parser/)
64
+ [![Python versions](https://img.shields.io/pypi/pyversions/streaming-json-parser)](https://pypi.org/project/streaming-json-parser/)
65
+ [![License: MIT](https://img.shields.io/github/license/aramisfacchinetti/streaming-json-parser)](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
+ ![Diagram showing a JSON string split across two chunks: the first produces a PARTIAL value and the second completes it](docs/assets/streaming-flow.svg)
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
+ ![Horizontal chart of complete JSON decoding times for streaming-json-parser APIs and alternative decoders; total CPU time for 20 iterations, lower is better](docs/assets/benchmarks/complete-decoding.svg)
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
+ ![Horizontal chart of complete-document selective extraction times for streaming-json-parser APIs and alternative approaches; total CPU time for 200 iterations, lower is better](docs/assets/benchmarks/selective-extraction.svg)
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
+ ![Horizontal chart of NDJSON selective extraction times for streaming-json-parser APIs and alternative approaches; total CPU time for 25 iterations, lower is better](docs/assets/benchmarks/ndjson-selective-extraction.svg)
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
+ ![Grouped horizontal bars compare complete-load throughput on six public JSON corpus files across streaming-json-parser APIs and common Python decoder libraries](docs/assets/benchmarks/community-json-corpus-throughput.svg)
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
+ [![CI](https://github.com/aramisfacchinetti/streaming-json-parser/actions/workflows/test.yml/badge.svg)](https://github.com/aramisfacchinetti/streaming-json-parser/actions/workflows/test.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/streaming-json-parser)](https://pypi.org/project/streaming-json-parser/)
7
+ [![Python versions](https://img.shields.io/pypi/pyversions/streaming-json-parser)](https://pypi.org/project/streaming-json-parser/)
8
+ [![License: MIT](https://img.shields.io/github/license/aramisfacchinetti/streaming-json-parser)](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
+ ![Diagram showing a JSON string split across two chunks: the first produces a PARTIAL value and the second completes it](docs/assets/streaming-flow.svg)
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
+ ![Horizontal chart of complete JSON decoding times for streaming-json-parser APIs and alternative decoders; total CPU time for 20 iterations, lower is better](docs/assets/benchmarks/complete-decoding.svg)
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
+ ![Horizontal chart of complete-document selective extraction times for streaming-json-parser APIs and alternative approaches; total CPU time for 200 iterations, lower is better](docs/assets/benchmarks/selective-extraction.svg)
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
+ ![Horizontal chart of NDJSON selective extraction times for streaming-json-parser APIs and alternative approaches; total CPU time for 25 iterations, lower is better](docs/assets/benchmarks/ndjson-selective-extraction.svg)
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
+ ![Grouped horizontal bars compare complete-load throughput on six public JSON corpus files across streaming-json-parser APIs and common Python decoder libraries](docs/assets/benchmarks/community-json-corpus-throughput.svg)
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.0"
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>=8.3.5",
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 _backend_yyjson is not None and isinstance(payload, bytes) and payload.isascii():
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 _backend_yyjson is not None and payload.isascii():
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 _backend_yyjson is not None and data.isascii():
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 _backend_yyjson is not None and data.isascii():
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