jsonsax 0.1.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.
@@ -0,0 +1,15 @@
1
+ # Build artifacts
2
+ build/
3
+ dist/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.py[cod]
7
+
8
+ # Tooling caches
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
jsonsax-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chandrapenugonda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
jsonsax-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,399 @@
1
+ Metadata-Version: 2.4
2
+ Name: jsonsax
3
+ Version: 0.1.0
4
+ Summary: A lightweight, dependency-free streaming (SAX-style) JSON parser.
5
+ Project-URL: Homepage, https://github.com/chandrapenugonda/jsonsax
6
+ Project-URL: Repository, https://github.com/chandrapenugonda/jsonsax
7
+ Project-URL: Issues, https://github.com/chandrapenugonda/jsonsax/issues
8
+ Author: chandrapenugonda
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: incremental,json,llm,parser,sax,streaming
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Text Processing :: Markup
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.8; extra == 'dev'
28
+ Requires-Dist: pylint>=3; extra == 'dev'
29
+ Requires-Dist: pytest>=7; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # jsonsax
33
+
34
+ **Read JSON while it is still arriving — don't wait for the whole thing.**
35
+
36
+ Imagine someone is reading you a long story out loud, one word at a time. You
37
+ don't wait for them to finish the whole book before you start listening — you
38
+ react to each part as you hear it. `jsonsax` does that for JSON.
39
+
40
+ Normally a computer waits for the *entire* JSON to show up, then reads it.
41
+ `jsonsax` is different: you hand it little pieces as they arrive, and it taps
42
+ you on the shoulder and says *"hey, I just found a name!"*, *"hey, here's a
43
+ number!"* — right away, piece by piece.
44
+
45
+ This style of reading-as-you-go is called a **streaming** (or **SAX-style**)
46
+ parser. (XML has had one for years; this is the same idea for JSON.)
47
+
48
+ ### Why would you want that?
49
+
50
+ - 🤖 **Talking to an AI** — chatbots send their answer one word at a time. With
51
+ `jsonsax` you can start using the first part of the answer before the rest
52
+ has even arrived.
53
+ - 🐘 **Huge files** — a JSON file too big to fit in memory? Read it in small
54
+ sips instead of swallowing it whole.
55
+ - ⚡ **Show things sooner** — display the title of an article the instant it
56
+ appears, without waiting for the whole article.
57
+
58
+ ---
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ pip install jsonsax
64
+ ```
65
+
66
+ That's it. No other stuff gets installed — `jsonsax` has **zero dependencies**.
67
+
68
+ ---
69
+
70
+ ## The tiniest example
71
+
72
+ ```python
73
+ from jsonsax import parse
74
+
75
+ parse('{"name": "Bo", "age": 5}', value=lambda path, val: print(path, "=", val))
76
+ ```
77
+
78
+ Output:
79
+
80
+ ```
81
+ $.name = Bo
82
+ $.age = 5
83
+ ```
84
+
85
+ `$` means "the start". `$.name` means "the `name` part". Think of it as an
86
+ address that tells you **where** in the JSON you are.
87
+
88
+ ---
89
+
90
+ ## Feeding it bit by bit (the fun part)
91
+
92
+ Real streams don't arrive all at once. Watch what happens when the JSON shows
93
+ up in messy little chunks — even cut in the middle of a word:
94
+
95
+ ```python
96
+ from jsonsax import Parser
97
+
98
+ parser = Parser()
99
+ parser.on("value", lambda path, val: print("found:", path, "=", val))
100
+
101
+ chunks = ['{"tit', 'le": "R', 'AG", "sco', 're": 9.5}']
102
+ for chunk in chunks:
103
+ parser.feed(chunk) # hand over one piece at a time
104
+ parser.close() # tell it "okay, that's everything"
105
+ ```
106
+
107
+ Output:
108
+
109
+ ```
110
+ found: $.title = RAG
111
+ found: $.score = 9.5
112
+ ```
113
+
114
+ Even though `"title"` got chopped into `"tit"` + `"le"`, `jsonsax` patiently
115
+ stitched it back together. 🧩
116
+
117
+ ---
118
+
119
+ ## Listening for different things ("events")
120
+
121
+ You tell `jsonsax` what you care about with `parser.on(...)`. Each time it sees
122
+ that kind of thing, it calls your little function (a *callback*).
123
+
124
+ ```python
125
+ from jsonsax import Parser
126
+
127
+ parser = Parser()
128
+ parser.on("start_object", lambda path: print(path, "{ ... an object starts"))
129
+ parser.on("end_object", lambda path: print(path, "} ... an object ends"))
130
+ parser.on("start_array", lambda path: print(path, "[ ... a list starts"))
131
+ parser.on("end_array", lambda path: print(path, "] ... a list ends"))
132
+ parser.on("key", lambda path, key: print(path, "key:", key))
133
+ parser.on("value", lambda path, val: print(path, "value:", repr(val)))
134
+
135
+ parse_me = '{"pets": ["cat", "dog"], "happy": true}'
136
+ for ch in parse_me:
137
+ parser.feed(ch)
138
+ parser.close()
139
+ ```
140
+
141
+ Output:
142
+
143
+ ```
144
+ $ { ... an object starts
145
+ $.pets key: pets
146
+ $.pets [ ... a list starts
147
+ $.pets[0] value: 'cat'
148
+ $.pets[1] value: 'dog'
149
+ $.pets ] ... a list ends
150
+ $.happy key: happy
151
+ $.happy value: True
152
+ $ } ... an object ends
153
+ ```
154
+
155
+ See how `$.pets[0]` and `$.pets[1]` count the items in the list, just like
156
+ "first pet" and "second pet"?
157
+
158
+ ---
159
+
160
+ ## The events you can listen for
161
+
162
+ | Event | You get… | Happens when it sees… |
163
+ | -------------- | --------------- | -------------------------------------- |
164
+ | `start_object` | `path` | a `{` — an object is starting |
165
+ | `end_object` | `path` | a `}` — an object is finished |
166
+ | `start_array` | `path` | a `[` — a list is starting |
167
+ | `end_array` | `path` | a `]` — a list is finished |
168
+ | `key` | `path, key` | a label inside an object (like `name`) |
169
+ | `value` | `path, value` | a real value: text, number, true/false/null |
170
+
171
+ A `value` can be a `str`, an `int`, a `float`, `True`, `False`, or `None`
172
+ (JSON's `null` becomes Python's `None`).
173
+
174
+ ---
175
+
176
+ ## More examples (little recipes)
177
+
178
+ ### 1. Grab just one field, ignore everything else
179
+
180
+ Only want the title? Only listen for it:
181
+
182
+ ```python
183
+ from jsonsax import Parser
184
+
185
+ def on_value(path, val):
186
+ if path == "$.title":
187
+ print("The title is:", val)
188
+
189
+ p = Parser()
190
+ p.on("value", on_value)
191
+ p.feed('{"title": "Hello", "body": "long boring text..."}')
192
+ p.close()
193
+ # The title is: Hello
194
+ ```
195
+
196
+ ### 2. Build a normal dictionary as you go
197
+
198
+ ```python
199
+ from jsonsax import Parser
200
+
201
+ data = {}
202
+ p = Parser()
203
+ p.on("value", lambda path, val: data.__setitem__(path, val))
204
+ p.feed('{"a": 1, "b": 2, "c": 3}')
205
+ p.close()
206
+ print(data)
207
+ # {'$.a': 1, '$.b': 2, '$.c': 3}
208
+ ```
209
+
210
+ ### 3. Count the items in a list
211
+
212
+ ```python
213
+ from jsonsax import Parser
214
+
215
+ count = 0
216
+ def bump(path, val):
217
+ global count
218
+ count += 1
219
+
220
+ p = Parser()
221
+ p.on("value", bump)
222
+ p.feed('[10, 20, 30, 40, 50]')
223
+ p.close()
224
+ print("items:", count) # items: 5
225
+ ```
226
+
227
+ ### 4. Deeply nested stuff is no problem
228
+
229
+ ```python
230
+ from jsonsax import parse
231
+
232
+ parse(
233
+ '{"user": {"name": "Mia", "tags": ["a", "b"]}}',
234
+ value=lambda path, val: print(path, "=", val),
235
+ )
236
+ # $.user.name = Mia
237
+ # $.user.tags[0] = a
238
+ # $.user.tags[1] = b
239
+ ```
240
+
241
+ ### 5. All the value types at once
242
+
243
+ ```python
244
+ from jsonsax import parse
245
+
246
+ parse(
247
+ '{"text": "hi", "whole": 42, "decimal": 3.14, "yes": true, "no": false, "nothing": null}',
248
+ value=lambda path, val: print(f"{path:14} -> {val!r}"),
249
+ )
250
+ # $.text -> 'hi'
251
+ # $.whole -> 42
252
+ # $.decimal -> 3.14
253
+ # $.yes -> True
254
+ # $.no -> False
255
+ # $.nothing -> None
256
+ ```
257
+
258
+ ### 6. Reacting to an AI that types its answer slowly
259
+
260
+ This is the big one. Pretend an AI sends its reply word-by-word:
261
+
262
+ ```python
263
+ from jsonsax import Parser
264
+
265
+ # These pieces would normally come from the AI, one at a time.
266
+ ai_stream = ['{"head', 'line": "Big New', 's!", "summary": "It happened today."}']
267
+
268
+ p = Parser()
269
+ p.on("value", lambda path, val: print(f"[{path}] arrived: {val}"))
270
+
271
+ for piece in ai_stream:
272
+ p.feed(piece) # the moment a field finishes, you hear about it
273
+ p.close()
274
+ # [$.headline] arrived: Big News!
275
+ # [$.summary] arrived: It happened today.
276
+ ```
277
+
278
+ ### 7. Chain your setup in one breath
279
+
280
+ `on(...)` hands you the parser back, so you can line them up:
281
+
282
+ ```python
283
+ from jsonsax import Parser
284
+
285
+ p = (
286
+ Parser()
287
+ .on("key", lambda path, k: print("key", k))
288
+ .on("value", lambda path, v: print("value", v))
289
+ )
290
+ p.feed('{"x": 1}')
291
+ p.close()
292
+ ```
293
+
294
+ ---
295
+
296
+ ## When the JSON is broken
297
+
298
+ If the JSON is messy or unfinished, `jsonsax` tells you by raising a
299
+ `ParseError` (which is just a special kind of Python `ValueError`). It is
300
+ **strict** on purpose — better to shout early than to quietly hand you wrong data.
301
+
302
+ ```python
303
+ from jsonsax import parse, ParseError
304
+
305
+ broken_examples = [
306
+ '{"a": 1,}', # extra comma at the end
307
+ '[1, 2', # forgot to close the list
308
+ '{"a" 1}', # missing the ':' between key and value
309
+ '"never ends', # string with no closing quote
310
+ 'true false', # two things glued together
311
+ ]
312
+
313
+ for bad in broken_examples:
314
+ try:
315
+ parse(bad)
316
+ except ParseError as error:
317
+ print("rejected:", bad, "->", error)
318
+ ```
319
+
320
+ Output (your wording may vary slightly):
321
+
322
+ ```
323
+ rejected: {"a": 1,} -> Unexpected '}'.
324
+ rejected: [1, 2 -> Unexpected end of input: unclosed container.
325
+ rejected: {"a" 1} -> Unexpected value (parser state: obj_colon).
326
+ rejected: "never ends -> Unexpected end of input: unterminated string.
327
+ rejected: true false -> Unexpected value (parser state: done).
328
+ ```
329
+
330
+ > **Always call `parser.close()` at the end.** That's the moment `jsonsax`
331
+ > double-checks that the JSON was actually complete. Forgetting it means you
332
+ > might miss the "you're missing the last `}`!" warning.
333
+
334
+ ---
335
+
336
+ ## Run it from the terminal (no code needed)
337
+
338
+ You can pipe JSON straight into `jsonsax` to watch the events scroll by:
339
+
340
+ ```bash
341
+ echo '{"x": [1, 2, true]}' | python -m jsonsax
342
+ ```
343
+
344
+ Output:
345
+
346
+ ```
347
+ $ {
348
+ $.x key='x'
349
+ $.x [
350
+ $.x[0] value=1
351
+ $.x[1] value=2
352
+ $.x[2] value=True
353
+ $.x ]
354
+ $ }
355
+ ```
356
+
357
+ ---
358
+
359
+ ## The whole toolbox (quick reference)
360
+
361
+ ```python
362
+ from jsonsax import Parser, parse, ParseError, EVENTS
363
+
364
+ parser = Parser() # make a new reader
365
+ parser.on(event, callback) # "when you see <event>, call <callback>" (returns parser)
366
+ parser.feed(chunk) # give it the next piece of text
367
+ parser.close() # "that's all" — checks the JSON was complete
368
+ parser.closed # True after a successful close()
369
+
370
+ parse(text, **handlers) # shortcut: feed + close in one line
371
+ ParseError # raised when the JSON is broken (a ValueError)
372
+ EVENTS # the tuple of all valid event names
373
+ ```
374
+
375
+ Good to know:
376
+
377
+ - **Truly incremental** — chunks can split *anywhere*, even in the middle of a
378
+ word, a number, or a `\uXXXX` escape.
379
+ - **Strict** — rejects trailing commas, missing colons, leftover junk, and
380
+ unfinished strings or brackets.
381
+ - **Typed** — ships with `py.typed`, so type checkers understand it.
382
+ - **Tiny & dependency-free**, works on **Python 3.9+**.
383
+
384
+ ---
385
+
386
+ ## For developers (working on jsonsax itself)
387
+
388
+ ```bash
389
+ pip install -e ".[dev]"
390
+ pytest # run the tests
391
+ pylint src/jsonsax # check the style
392
+ mypy # check the types
393
+ ```
394
+
395
+ ---
396
+
397
+ ## License
398
+
399
+ MIT — free to use, change, and share.