streaming-json-parser 0.1.0__py3-none-any.whl
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/__init__.py +0 -0
- streaming_json_parser/iterative_state_machine.py +552 -0
- streaming_json_parser/streaming_json_parser.py +173 -0
- streaming_json_parser-0.1.0.dist-info/METADATA +156 -0
- streaming_json_parser-0.1.0.dist-info/RECORD +8 -0
- streaming_json_parser-0.1.0.dist-info/WHEEL +5 -0
- streaming_json_parser-0.1.0.dist-info/licenses/LICENSE +21 -0
- streaming_json_parser-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
# State constants for the iterative parser
|
|
6
|
+
S_INIT, S_OBJ_START, S_OBJ_KEY, S_OBJ_COLON, S_OBJ_VALUE, S_OBJ_COMMA, \
|
|
7
|
+
S_ARR_START, S_ARR_VALUE, S_ARR_COMMA, S_COMPLETE, S_ERROR = range(11)
|
|
8
|
+
|
|
9
|
+
class IterativeStateMachine:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
# Pre-compile regex for finding unquoted keys for efficiency
|
|
12
|
+
# Allows alphanumeric characters, _, -, . as part of the key
|
|
13
|
+
self.__key_pattern = re.compile(r"([a-zA-Z0-9_.-]+)")
|
|
14
|
+
|
|
15
|
+
def parse_iterative_partial(self, s: str) -> tuple[Optional[Union[dict[str, Any], list[Any]]], int]:
|
|
16
|
+
"""
|
|
17
|
+
An iterative state-machine based parser for potentially incomplete or
|
|
18
|
+
slightly non-standard JSON (unquoted keys, single quotes).
|
|
19
|
+
Attempts to parse one top-level object or array.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
s: The string buffer containing JSON data (or partial data).
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A tuple containing:
|
|
26
|
+
- The parsed object (dict) or array (list), or None if parsing fails early.
|
|
27
|
+
- The index in the string 's' up to which parsing consumed input.
|
|
28
|
+
This index might point to the end of a complete structure, or the
|
|
29
|
+
point where parsing stopped due to incomplete data or an error.
|
|
30
|
+
"""
|
|
31
|
+
state_stack = [S_INIT]
|
|
32
|
+
# Holds the nested dicts/lists currently being built
|
|
33
|
+
container_stack: list[Union[dict[str, Any], list[Any]]] = []
|
|
34
|
+
# Stores the key for the current object value being parsed
|
|
35
|
+
current_key: Optional[str] = None
|
|
36
|
+
# The root object or list being parsed
|
|
37
|
+
root: Optional[Union[dict[str, Any], list[Any]]] = None
|
|
38
|
+
i = 0 # Current parsing index in string 's'
|
|
39
|
+
len_s = len(s)
|
|
40
|
+
|
|
41
|
+
while i < len_s:
|
|
42
|
+
current_state = state_stack[-1]
|
|
43
|
+
char = s[i]
|
|
44
|
+
|
|
45
|
+
# Skip Whitespace efficiently
|
|
46
|
+
if char.isspace():
|
|
47
|
+
i += 1
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# --- State Machine Logic ---
|
|
52
|
+
|
|
53
|
+
# Initial state: Expect start of object '{' or array '['
|
|
54
|
+
if current_state == S_INIT:
|
|
55
|
+
if char == '{':
|
|
56
|
+
root = {}
|
|
57
|
+
container_stack.append(root)
|
|
58
|
+
state_stack[-1] = S_OBJ_START # Transition to object start state
|
|
59
|
+
i += 1
|
|
60
|
+
elif char == '[':
|
|
61
|
+
root = []
|
|
62
|
+
container_stack.append(root)
|
|
63
|
+
state_stack[-1] = S_ARR_START # Transition to array start state
|
|
64
|
+
i += 1
|
|
65
|
+
else:
|
|
66
|
+
# Error: Input doesn't start with '{' or '['
|
|
67
|
+
state_stack.append(S_ERROR)
|
|
68
|
+
break # Stop parsing
|
|
69
|
+
|
|
70
|
+
# === OBJECT STATES ===
|
|
71
|
+
|
|
72
|
+
# Inside object, after '{' or comma: Expect key or '}'
|
|
73
|
+
elif current_state == S_OBJ_START:
|
|
74
|
+
if char == '}':
|
|
75
|
+
# Empty object or end of object
|
|
76
|
+
if not container_stack:
|
|
77
|
+
# Should be impossible if state logic is correct
|
|
78
|
+
state_stack.append(S_ERROR)
|
|
79
|
+
break
|
|
80
|
+
state_stack.pop() # Pop S_OBJ_START state
|
|
81
|
+
container_stack.pop() # Pop the completed object
|
|
82
|
+
i += 1
|
|
83
|
+
if not state_stack:
|
|
84
|
+
# If stack is now empty, we finished the root object
|
|
85
|
+
state_stack.append(S_COMPLETE)
|
|
86
|
+
break
|
|
87
|
+
# Otherwise, we were in a nested object, continue parsing parent
|
|
88
|
+
else:
|
|
89
|
+
# Expecting an object key next
|
|
90
|
+
state_stack[-1] = S_OBJ_KEY
|
|
91
|
+
# Do not increment 'i' here, S_OBJ_KEY needs to process current char
|
|
92
|
+
|
|
93
|
+
# Expecting an object key (quoted or unquoted)
|
|
94
|
+
elif current_state == S_OBJ_KEY:
|
|
95
|
+
key = None
|
|
96
|
+
start_i = i
|
|
97
|
+
if char == '"':
|
|
98
|
+
key, i = self.__parse_partial_string(s, i)
|
|
99
|
+
elif char == "'": # Non-standard: single quotes
|
|
100
|
+
key, i = self.__parse_single_quoted_string(s, i)
|
|
101
|
+
else: # Non-standard: unquoted key
|
|
102
|
+
key, i = self.__parse_unquoted_key(s, i)
|
|
103
|
+
|
|
104
|
+
# Check if a key was successfully parsed and index advanced
|
|
105
|
+
if key is not None and i > start_i:
|
|
106
|
+
current_key = key
|
|
107
|
+
state_stack[-1] = S_OBJ_COLON # Transition: Expect colon next
|
|
108
|
+
else:
|
|
109
|
+
# Failed to parse a valid key at this position
|
|
110
|
+
state_stack.append(S_ERROR)
|
|
111
|
+
break # Stop parsing
|
|
112
|
+
|
|
113
|
+
# Expecting the colon ':' after an object key
|
|
114
|
+
elif current_state == S_OBJ_COLON:
|
|
115
|
+
if char == ':':
|
|
116
|
+
state_stack[-1] = S_OBJ_VALUE # Transition: Expect value next
|
|
117
|
+
i += 1
|
|
118
|
+
else:
|
|
119
|
+
# Error: Missing colon after key
|
|
120
|
+
state_stack.append(S_ERROR)
|
|
121
|
+
break # Stop parsing
|
|
122
|
+
|
|
123
|
+
# Expecting a value (primitive, object, or array) for the current key
|
|
124
|
+
elif current_state == S_OBJ_VALUE:
|
|
125
|
+
value_parsed = False
|
|
126
|
+
start_i = i
|
|
127
|
+
# Get the object this key-value pair belongs to
|
|
128
|
+
parent_obj = container_stack[-1]
|
|
129
|
+
|
|
130
|
+
# Basic check for state consistency
|
|
131
|
+
if not isinstance(parent_obj, dict) or current_key is None:
|
|
132
|
+
state_stack.append(S_ERROR)
|
|
133
|
+
break # Should not happen in correct state flow
|
|
134
|
+
|
|
135
|
+
# Check for nested object start
|
|
136
|
+
if char == '{':
|
|
137
|
+
new_obj = {}
|
|
138
|
+
parent_obj[current_key] = new_obj
|
|
139
|
+
# Transition parent state to expect comma/close brace
|
|
140
|
+
state_stack[-1] = S_OBJ_COMMA
|
|
141
|
+
# Push new object and its state onto stacks
|
|
142
|
+
container_stack.append(new_obj)
|
|
143
|
+
state_stack.append(S_OBJ_START)
|
|
144
|
+
i += 1
|
|
145
|
+
value_parsed = True
|
|
146
|
+
# Check for nested array start
|
|
147
|
+
elif char == '[':
|
|
148
|
+
new_arr = []
|
|
149
|
+
parent_obj[current_key] = new_arr
|
|
150
|
+
# Transition parent state to expect comma/close brace
|
|
151
|
+
state_stack[-1] = S_OBJ_COMMA
|
|
152
|
+
# Push new array and its state onto stacks
|
|
153
|
+
container_stack.append(new_arr)
|
|
154
|
+
state_stack.append(S_ARR_START)
|
|
155
|
+
i += 1
|
|
156
|
+
value_parsed = True
|
|
157
|
+
else:
|
|
158
|
+
# Attempt to parse a primitive value (string, number, literal)
|
|
159
|
+
value, next_i = self.__parse_primitive_value(s, i)
|
|
160
|
+
if next_i > i: # Check if parsing advanced the index
|
|
161
|
+
parent_obj[current_key] = value
|
|
162
|
+
state_stack[-1] = S_OBJ_COMMA # Expect comma/close brace next
|
|
163
|
+
i = next_i # Update index to after the primitive
|
|
164
|
+
value_parsed = True
|
|
165
|
+
# else: primitive parsing failed, handled below
|
|
166
|
+
|
|
167
|
+
if not value_parsed:
|
|
168
|
+
# Error: Could not parse a valid value after the colon
|
|
169
|
+
state_stack.append(S_ERROR)
|
|
170
|
+
break # Stop parsing
|
|
171
|
+
|
|
172
|
+
# After an object value: Expect comma ',' or closing brace '}'
|
|
173
|
+
elif current_state == S_OBJ_COMMA:
|
|
174
|
+
if char == ',':
|
|
175
|
+
state_stack[-1] = S_OBJ_KEY # Transition: Expect another key
|
|
176
|
+
i += 1
|
|
177
|
+
elif char == '}':
|
|
178
|
+
# End of the current object
|
|
179
|
+
if not container_stack:
|
|
180
|
+
state_stack.append(S_ERROR) # Error: Unbalanced braces
|
|
181
|
+
break
|
|
182
|
+
state_stack.pop() # Pop S_OBJ_COMMA state
|
|
183
|
+
container_stack.pop() # Pop the completed object
|
|
184
|
+
i += 1
|
|
185
|
+
if not state_stack:
|
|
186
|
+
# Finished the root object
|
|
187
|
+
state_stack.append(S_COMPLETE)
|
|
188
|
+
break
|
|
189
|
+
# Else, continue processing parent container
|
|
190
|
+
else:
|
|
191
|
+
# Error: Expected ',' or '}' after value
|
|
192
|
+
state_stack.append(S_ERROR)
|
|
193
|
+
break # Stop parsing
|
|
194
|
+
|
|
195
|
+
# === ARRAY STATES ===
|
|
196
|
+
|
|
197
|
+
# Inside array, after '[' or comma: Expect value or ']'
|
|
198
|
+
elif current_state == S_ARR_START:
|
|
199
|
+
if char == ']':
|
|
200
|
+
# Empty array or end of array
|
|
201
|
+
if not container_stack:
|
|
202
|
+
state_stack.append(S_ERROR) # Error: Unbalanced brackets
|
|
203
|
+
break
|
|
204
|
+
state_stack.pop() # Pop S_ARR_START state
|
|
205
|
+
container_stack.pop() # Pop the completed array
|
|
206
|
+
i += 1
|
|
207
|
+
if not state_stack:
|
|
208
|
+
# Finished the root array
|
|
209
|
+
state_stack.append(S_COMPLETE)
|
|
210
|
+
break
|
|
211
|
+
# Else, continue processing parent container
|
|
212
|
+
else:
|
|
213
|
+
# Expecting an array value next
|
|
214
|
+
state_stack[-1] = S_ARR_VALUE
|
|
215
|
+
# Do not increment 'i', S_ARR_VALUE needs to process current char
|
|
216
|
+
|
|
217
|
+
# Expecting an array value (primitive, object, or array)
|
|
218
|
+
elif current_state == S_ARR_VALUE:
|
|
219
|
+
value_parsed = False
|
|
220
|
+
start_i = i
|
|
221
|
+
# Get the array this value belongs to
|
|
222
|
+
parent_arr = container_stack[-1]
|
|
223
|
+
|
|
224
|
+
# Basic check for state consistency
|
|
225
|
+
if not isinstance(parent_arr, list):
|
|
226
|
+
state_stack.append(S_ERROR)
|
|
227
|
+
break # Should not happen
|
|
228
|
+
|
|
229
|
+
# Check for nested object start
|
|
230
|
+
if char == '{':
|
|
231
|
+
new_obj = {}
|
|
232
|
+
parent_arr.append(new_obj)
|
|
233
|
+
# Transition parent state to expect comma/close bracket
|
|
234
|
+
state_stack[-1] = S_ARR_COMMA
|
|
235
|
+
# Push new object and its state
|
|
236
|
+
container_stack.append(new_obj)
|
|
237
|
+
state_stack.append(S_OBJ_START)
|
|
238
|
+
i += 1
|
|
239
|
+
value_parsed = True
|
|
240
|
+
# Check for nested array start
|
|
241
|
+
elif char == '[':
|
|
242
|
+
new_arr = []
|
|
243
|
+
parent_arr.append(new_arr)
|
|
244
|
+
# Transition parent state to expect comma/close bracket
|
|
245
|
+
state_stack[-1] = S_ARR_COMMA
|
|
246
|
+
# Push new array and its state
|
|
247
|
+
container_stack.append(new_arr)
|
|
248
|
+
state_stack.append(S_ARR_START)
|
|
249
|
+
i += 1
|
|
250
|
+
value_parsed = True
|
|
251
|
+
else:
|
|
252
|
+
# Attempt to parse a primitive value
|
|
253
|
+
value, next_i = self.__parse_primitive_value(s, i)
|
|
254
|
+
if next_i > i: # Check if parsing advanced the index
|
|
255
|
+
parent_arr.append(value)
|
|
256
|
+
state_stack[-1] = S_ARR_COMMA # Expect comma/close bracket next
|
|
257
|
+
i = next_i # Update index
|
|
258
|
+
value_parsed = True
|
|
259
|
+
# else: primitive parsing failed, handled below
|
|
260
|
+
|
|
261
|
+
if not value_parsed:
|
|
262
|
+
# Error: Could not parse a valid value in array
|
|
263
|
+
state_stack.append(S_ERROR)
|
|
264
|
+
break # Stop parsing
|
|
265
|
+
|
|
266
|
+
# After an array value: Expect comma ',' or closing bracket ']'
|
|
267
|
+
elif current_state == S_ARR_COMMA:
|
|
268
|
+
if char == ',':
|
|
269
|
+
state_stack[-1] = S_ARR_VALUE # Transition: Expect another value
|
|
270
|
+
i += 1
|
|
271
|
+
elif char == ']':
|
|
272
|
+
# End of the current array
|
|
273
|
+
if not container_stack:
|
|
274
|
+
state_stack.append(S_ERROR) # Error: Unbalanced brackets
|
|
275
|
+
break
|
|
276
|
+
state_stack.pop() # Pop S_ARR_COMMA state
|
|
277
|
+
container_stack.pop() # Pop the completed array
|
|
278
|
+
i += 1
|
|
279
|
+
if not state_stack:
|
|
280
|
+
# Finished the root array
|
|
281
|
+
state_stack.append(S_COMPLETE)
|
|
282
|
+
break
|
|
283
|
+
# Else, continue processing parent container
|
|
284
|
+
else:
|
|
285
|
+
# Error: Expected ',' or ']' after array element
|
|
286
|
+
state_stack.append(S_ERROR)
|
|
287
|
+
break # Stop parsing
|
|
288
|
+
|
|
289
|
+
# === END/ERROR STATES ===
|
|
290
|
+
|
|
291
|
+
# Parsing completed successfully for the root element
|
|
292
|
+
elif current_state == S_COMPLETE:
|
|
293
|
+
break # Exit main loop
|
|
294
|
+
|
|
295
|
+
# An error occurred during parsing
|
|
296
|
+
elif current_state == S_ERROR:
|
|
297
|
+
break # Exit main loop
|
|
298
|
+
|
|
299
|
+
# Catchall for unknown states (should not happen)
|
|
300
|
+
else:
|
|
301
|
+
print(f"Warning: Encountered unknown parser state {current_state}")
|
|
302
|
+
state_stack.append(S_ERROR)
|
|
303
|
+
break # Stop parsing
|
|
304
|
+
|
|
305
|
+
except IndexError:
|
|
306
|
+
# Catch errors accessing state_stack or container_stack if they become empty unexpectedly
|
|
307
|
+
print(f"Error: Stack underflow at index {i}, state {current_state}. Likely malformed JSON.")
|
|
308
|
+
state_stack.append(S_ERROR)
|
|
309
|
+
break
|
|
310
|
+
except Exception as inner_ex:
|
|
311
|
+
# Catch any other unexpected errors during state processing
|
|
312
|
+
print(f"Error during iterative parse step: {inner_ex} at index {i}, state {current_state}")
|
|
313
|
+
state_stack.append(S_ERROR)
|
|
314
|
+
break # Stop parsing on unexpected exceptions
|
|
315
|
+
|
|
316
|
+
# Determine the final state and return result
|
|
317
|
+
final_state = state_stack[-1]
|
|
318
|
+
|
|
319
|
+
if final_state == S_COMPLETE:
|
|
320
|
+
# Parsed a complete object/array successfully
|
|
321
|
+
return root, i
|
|
322
|
+
elif final_state == S_ERROR:
|
|
323
|
+
# Parsing stopped due to an error. Return whatever was parsed
|
|
324
|
+
# up to the error point (might be None or partial structure)
|
|
325
|
+
# 'i' will be the index where the error occurred.
|
|
326
|
+
return root, i
|
|
327
|
+
else:
|
|
328
|
+
# Loop finished because end of input string 's' was reached,
|
|
329
|
+
# but the JSON structure wasn't 'complete' (e.g., missing closing brace).
|
|
330
|
+
# Return the partially parsed structure and the final index 'i'.
|
|
331
|
+
return root, i
|
|
332
|
+
|
|
333
|
+
def __parse_primitive_value(self, s: str, i: int) -> tuple[Any, int]:
|
|
334
|
+
"""
|
|
335
|
+
Attempts to parse any primitive JSON value (string, number, literal)
|
|
336
|
+
starting at s[i]. Also handles non-standard single quotes.
|
|
337
|
+
Returns the parsed value and the index after it, or (None, i) on failure.
|
|
338
|
+
"""
|
|
339
|
+
if i >= len(s):
|
|
340
|
+
# Cannot parse if index is out of bounds
|
|
341
|
+
return None, i
|
|
342
|
+
|
|
343
|
+
char = s[i]
|
|
344
|
+
|
|
345
|
+
# Check for string start (double quote)
|
|
346
|
+
if char == '"':
|
|
347
|
+
return self.__parse_partial_string(s, i)
|
|
348
|
+
|
|
349
|
+
# Check for non-standard string start (single quote)
|
|
350
|
+
if char == "'":
|
|
351
|
+
return self.__parse_single_quoted_string(s, i)
|
|
352
|
+
|
|
353
|
+
# Check for number start (digit or minus sign)
|
|
354
|
+
if char in "-0123456789":
|
|
355
|
+
return self.__parse_partial_number(s, i)
|
|
356
|
+
|
|
357
|
+
# Check for literals (true, false, null)
|
|
358
|
+
val, next_i = self.__parse_partial_literal(s, i)
|
|
359
|
+
if next_i > i:
|
|
360
|
+
# A literal was successfully parsed
|
|
361
|
+
return val, next_i
|
|
362
|
+
|
|
363
|
+
# If none of the above matched, it's not a recognizable primitive start
|
|
364
|
+
return None, i # Indicate failure by returning original index
|
|
365
|
+
|
|
366
|
+
def __parse_unquoted_key(self, s: str, i: int) -> tuple[Optional[str], int]:
|
|
367
|
+
"""
|
|
368
|
+
Attempts to parse an unquoted object key starting at s[i]. Non-standard.
|
|
369
|
+
Uses the pre-compiled regex `_key_pattern`.
|
|
370
|
+
Returns the key string and the index after it, or (None, i) if no match.
|
|
371
|
+
"""
|
|
372
|
+
match = self.__key_pattern.match(s, i)
|
|
373
|
+
if match:
|
|
374
|
+
# Key must not be followed immediately by ":" without whitespace,
|
|
375
|
+
# handle ':' separation in the main loop.
|
|
376
|
+
key = match.group(1)
|
|
377
|
+
return key, match.end()
|
|
378
|
+
else:
|
|
379
|
+
# No match for the unquoted key pattern
|
|
380
|
+
return None, i
|
|
381
|
+
|
|
382
|
+
def __parse_single_quoted_string(self, s: str, i: int) -> tuple[str, int]:
|
|
383
|
+
"""
|
|
384
|
+
Parses a single-quoted string starting at s[i]. Non-standard JSON.
|
|
385
|
+
Assumes s[i] == "'". Handles limited escapes (\' and \\).
|
|
386
|
+
Returns the parsed string content and the index after the closing quote,
|
|
387
|
+
or the end of the string if unterminated.
|
|
388
|
+
"""
|
|
389
|
+
i += 1 # skip opening '
|
|
390
|
+
result = []
|
|
391
|
+
escape = False
|
|
392
|
+
|
|
393
|
+
while i < len(s):
|
|
394
|
+
ch = s[i]
|
|
395
|
+
|
|
396
|
+
if escape:
|
|
397
|
+
if ch == "'":
|
|
398
|
+
result.append("'")
|
|
399
|
+
elif ch == '\\':
|
|
400
|
+
result.append('\\')
|
|
401
|
+
else:
|
|
402
|
+
# Pass through other characters following a backslash
|
|
403
|
+
result.append('\\')
|
|
404
|
+
result.append(ch)
|
|
405
|
+
escape = False
|
|
406
|
+
elif ch == "\\":
|
|
407
|
+
escape = True
|
|
408
|
+
elif ch == "'":
|
|
409
|
+
# End of string found
|
|
410
|
+
return "".join(result), i + 1
|
|
411
|
+
else:
|
|
412
|
+
result.append(ch)
|
|
413
|
+
|
|
414
|
+
i += 1
|
|
415
|
+
|
|
416
|
+
# Unterminated single-quoted string
|
|
417
|
+
return "".join(result), i
|
|
418
|
+
|
|
419
|
+
def __parse_partial_number(self, s: str, i: int) -> tuple[Any, int]:
|
|
420
|
+
"""
|
|
421
|
+
Parses a number (int or float) starting at s[i].
|
|
422
|
+
Uses regex for robust matching of JSON number format.
|
|
423
|
+
Returns the parsed number (int or float) and the index after the number,
|
|
424
|
+
or (None, i) if no valid number start is found.
|
|
425
|
+
"""
|
|
426
|
+
# Regex for standard JSON numbers (integer and floating point)
|
|
427
|
+
# Allows leading minus, optional fractional part, optional exponent
|
|
428
|
+
num_match = re.match(r"-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?", s[i:])
|
|
429
|
+
|
|
430
|
+
if num_match:
|
|
431
|
+
num_str = num_match.group(0)
|
|
432
|
+
end_i = i + len(num_str)
|
|
433
|
+
try:
|
|
434
|
+
# Use json.loads on the matched string for reliable conversion
|
|
435
|
+
# This correctly handles int vs float detection.
|
|
436
|
+
return json.loads(num_str), end_i
|
|
437
|
+
except json.JSONDecodeError:
|
|
438
|
+
# This should theoretically not happen if the regex is correct,
|
|
439
|
+
# but acts as a safeguard. Return the raw string maybe?
|
|
440
|
+
# Or indicate failure. Let's return failure indicator.
|
|
441
|
+
return None, i # Indicate parsing failed despite regex match
|
|
442
|
+
else:
|
|
443
|
+
# No valid number pattern matched at the current position
|
|
444
|
+
return None, i
|
|
445
|
+
|
|
446
|
+
def __parse_partial_literal(self, s: str, i: int) -> tuple[Any, int]:
|
|
447
|
+
"""
|
|
448
|
+
Parses JSON literals (true, false, null) starting at s[i].
|
|
449
|
+
Checks for word boundaries to avoid partial matches (e.g., 'trueish').
|
|
450
|
+
Returns the literal value (True, False, None) and the index after it,
|
|
451
|
+
or (None, i) if no literal is matched.
|
|
452
|
+
"""
|
|
453
|
+
len_s = len(s)
|
|
454
|
+
|
|
455
|
+
# Check for 'true'
|
|
456
|
+
if s.startswith("true", i):
|
|
457
|
+
end_i = i + 4
|
|
458
|
+
# Check if it's the end of the string or followed by a non-alphanumeric char
|
|
459
|
+
if end_i == len_s or not s[end_i].isalnum():
|
|
460
|
+
return True, end_i
|
|
461
|
+
|
|
462
|
+
# Check for 'false'
|
|
463
|
+
if s.startswith("false", i):
|
|
464
|
+
end_i = i + 5
|
|
465
|
+
# Check for word boundary
|
|
466
|
+
if end_i == len_s or not s[end_i].isalnum():
|
|
467
|
+
return False, end_i
|
|
468
|
+
|
|
469
|
+
# Check for 'null'
|
|
470
|
+
if s.startswith("null", i):
|
|
471
|
+
end_i = i + 4
|
|
472
|
+
# Check for word boundary
|
|
473
|
+
if end_i == len_s or not s[end_i].isalnum():
|
|
474
|
+
return None, end_i
|
|
475
|
+
|
|
476
|
+
# No literal matched at this position
|
|
477
|
+
return None, i
|
|
478
|
+
|
|
479
|
+
def __parse_partial_string(self, s: str, i: int) -> tuple[str, int]:
|
|
480
|
+
"""
|
|
481
|
+
Parses a double-quoted string starting at s[i].
|
|
482
|
+
Assumes s[i] == '"'. Handles standard JSON escapes.
|
|
483
|
+
Returns the parsed string content and the index after the closing quote,
|
|
484
|
+
or the end of the string if unterminated.
|
|
485
|
+
"""
|
|
486
|
+
i += 1 # skip opening "
|
|
487
|
+
result = []
|
|
488
|
+
escape = False
|
|
489
|
+
|
|
490
|
+
while i < len(s):
|
|
491
|
+
ch = s[i]
|
|
492
|
+
|
|
493
|
+
# Handle escape sequences
|
|
494
|
+
if escape:
|
|
495
|
+
if ch == 'b':
|
|
496
|
+
result.append('\b')
|
|
497
|
+
elif ch == 'f':
|
|
498
|
+
result.append('\f')
|
|
499
|
+
elif ch == 'n':
|
|
500
|
+
result.append('\n')
|
|
501
|
+
elif ch == 'r':
|
|
502
|
+
result.append('\r')
|
|
503
|
+
elif ch == 't':
|
|
504
|
+
result.append('\t')
|
|
505
|
+
elif ch == '"':
|
|
506
|
+
result.append('"')
|
|
507
|
+
elif ch == '\\':
|
|
508
|
+
result.append('\\')
|
|
509
|
+
elif ch == '/':
|
|
510
|
+
result.append('/') # Allowed escape, often seen
|
|
511
|
+
elif ch == 'u':
|
|
512
|
+
# Unicode escape (basic handling)
|
|
513
|
+
if i + 4 < len(s):
|
|
514
|
+
try:
|
|
515
|
+
hex_code = s[i + 1 : i + 5]
|
|
516
|
+
result.append(chr(int(hex_code, 16)))
|
|
517
|
+
i += 4 # Skip hex digits
|
|
518
|
+
except ValueError:
|
|
519
|
+
# Invalid hex code, keep the original sequence
|
|
520
|
+
result.append('\\u')
|
|
521
|
+
result.append(s[i + 1 : i + 5])
|
|
522
|
+
i += 4
|
|
523
|
+
else:
|
|
524
|
+
# Incomplete escape sequence at end of buffer
|
|
525
|
+
result.append('\\u')
|
|
526
|
+
# Append remaining characters if any
|
|
527
|
+
if i + 1 < len(s):
|
|
528
|
+
result.append(s[i + 1 :])
|
|
529
|
+
i = len(s) # Move index to end
|
|
530
|
+
break # Exit loop as escape is incomplete
|
|
531
|
+
else:
|
|
532
|
+
# Pass through unknown escape sequences
|
|
533
|
+
result.append('\\')
|
|
534
|
+
result.append(ch)
|
|
535
|
+
escape = False
|
|
536
|
+
elif ch == "\\":
|
|
537
|
+
escape = True
|
|
538
|
+
elif ch == '"':
|
|
539
|
+
# End of string found
|
|
540
|
+
return "".join(result), i + 1
|
|
541
|
+
else:
|
|
542
|
+
# Handle control characters (U+0000 to U+001F)
|
|
543
|
+
# Escape them to \uXXXX format if not already escaped
|
|
544
|
+
if 0x00 <= ord(ch) <= 0x1F:
|
|
545
|
+
result.append(f'\\u{ord(ch):04x}')
|
|
546
|
+
result.append(ch)
|
|
547
|
+
|
|
548
|
+
i += 1
|
|
549
|
+
|
|
550
|
+
# If loop finishes without finding closing quote, string is unterminated.
|
|
551
|
+
# Return the partial result found so far.
|
|
552
|
+
return "".join(result), i
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from src.streaming_json_parser.iterative_state_machine import \
|
|
6
|
+
IterativeStateMachine
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StreamingJsonParser:
|
|
10
|
+
"""
|
|
11
|
+
A parser designed to handle potentially incomplete or slightly malformed
|
|
12
|
+
JSON streams, attempting to extract valid JSON objects.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.__buffer: str = ""
|
|
16
|
+
|
|
17
|
+
def consume(self, data: str) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Adds new data chunks to the internal buffer after escaping invalid chars.
|
|
20
|
+
"""
|
|
21
|
+
# Escape control characters before adding to buffer
|
|
22
|
+
# This helps prevent issues if these chars appear outside strings
|
|
23
|
+
if not self.__is_string(data):
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
escaped_data = self.__escape_invalid_control_chars(data)
|
|
27
|
+
if escaped_data:
|
|
28
|
+
self.__buffer += escaped_data
|
|
29
|
+
|
|
30
|
+
def get(self) -> dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Attempts to parse and return the first complete JSON object found
|
|
33
|
+
in the buffer. Removes the parsed object (and preceding non-JSON data)
|
|
34
|
+
from the buffer. Returns an empty dict if no complete object is found.
|
|
35
|
+
"""
|
|
36
|
+
# --- Initial buffer cleanup ---
|
|
37
|
+
# Remove potential BOM (Byte Order Mark) and leading whitespace
|
|
38
|
+
self.__buffer = self.__buffer.lstrip("\ufeff").lstrip()
|
|
39
|
+
|
|
40
|
+
# Find the first opening brace '{' which signifies a potential JSON object start
|
|
41
|
+
start_index = self.__buffer.find("{")
|
|
42
|
+
if start_index == -1:
|
|
43
|
+
# No object start found, clear buffer if it contains only whitespace/junk
|
|
44
|
+
if not self.__buffer.strip():
|
|
45
|
+
self.__buffer = ""
|
|
46
|
+
# Otherwise, keep buffer as it might be incomplete start of something else
|
|
47
|
+
return {}
|
|
48
|
+
elif start_index > 0:
|
|
49
|
+
# Discard anything before the first '{'
|
|
50
|
+
self.__buffer = self.__buffer[start_index:]
|
|
51
|
+
|
|
52
|
+
# If buffer became empty after stripping/finding '{', return empty
|
|
53
|
+
if not self.__buffer:
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
# --- Attempt 1: Use standard json.raw_decode for well-formed JSON ---
|
|
57
|
+
# This is efficient for standard JSON.
|
|
58
|
+
try:
|
|
59
|
+
decoder = json.JSONDecoder()
|
|
60
|
+
# raw_decode parses one JSON value and returns it and the index where it stopped
|
|
61
|
+
obj, idx = decoder.raw_decode(self.__buffer)
|
|
62
|
+
|
|
63
|
+
# Check if the decoded item is a dictionary (object)
|
|
64
|
+
if isinstance(obj, dict):
|
|
65
|
+
# Successfully parsed a standard JSON object
|
|
66
|
+
self.__buffer = self.__buffer[idx:] # Remove parsed part from buffer
|
|
67
|
+
self.__clean_buffer_after_parse() # Clean up potential leading junk
|
|
68
|
+
return obj
|
|
69
|
+
else:
|
|
70
|
+
# Parsed something, but it wasn't an object (e.g., list, primitive)
|
|
71
|
+
# Discard the parsed part and try again (or let partial parser handle it)
|
|
72
|
+
self.__buffer = self.__buffer[idx:]
|
|
73
|
+
# Fall through to the partial parser attempt
|
|
74
|
+
except json.JSONDecodeError:
|
|
75
|
+
# raw_decode failed, likely due to incomplete or malformed JSON.
|
|
76
|
+
# Proceed to the more lenient iterative partial parser.
|
|
77
|
+
pass
|
|
78
|
+
except Exception:
|
|
79
|
+
# Catch other potential errors during raw_decode
|
|
80
|
+
# print(f"Unexpected error during raw_decode: {e}") # Optional logging
|
|
81
|
+
pass # Fall through to partial parser
|
|
82
|
+
|
|
83
|
+
# --- Attempt 2: Use the Iterative Partial Parser ---
|
|
84
|
+
# This handles incomplete data, unquoted keys, single quotes etc.
|
|
85
|
+
self.__buffer = self.__buffer.lstrip() # Ensure no leading whitespace
|
|
86
|
+
|
|
87
|
+
# Re-check if buffer starts with '{' after potential modification from attempt 1
|
|
88
|
+
if not self.__buffer.startswith('{'):
|
|
89
|
+
start_index = self.__buffer.find("{")
|
|
90
|
+
if start_index == -1:
|
|
91
|
+
# No '{' found at all anymore
|
|
92
|
+
self.__buffer = ""
|
|
93
|
+
return {}
|
|
94
|
+
else:
|
|
95
|
+
# Discard leading content if any before '{'
|
|
96
|
+
self.__buffer = self.__buffer[start_index:]
|
|
97
|
+
|
|
98
|
+
# Re-check if buffer is empty
|
|
99
|
+
if not self.__buffer:
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
# Run the iterative state-machine parser
|
|
103
|
+
iterative_state_machine = IterativeStateMachine()
|
|
104
|
+
parsed_obj, consumed_idx = iterative_state_machine.parse_iterative_partial(self.__buffer)
|
|
105
|
+
|
|
106
|
+
# Update buffer based on how much was consumed
|
|
107
|
+
if 0 <= consumed_idx <= len(self.__buffer):
|
|
108
|
+
# Successfully parsed or partially parsed, remove consumed part
|
|
109
|
+
self.__buffer = self.__buffer[consumed_idx:]
|
|
110
|
+
else:
|
|
111
|
+
# Indicates an error or unexpected state, clear buffer to be safe
|
|
112
|
+
self.__buffer = ""
|
|
113
|
+
|
|
114
|
+
# Clean buffer again after partial parse attempt
|
|
115
|
+
self.__clean_buffer_after_parse()
|
|
116
|
+
|
|
117
|
+
# Return the parsed object only if it's a dictionary
|
|
118
|
+
if isinstance(parsed_obj, dict):
|
|
119
|
+
return parsed_obj
|
|
120
|
+
else:
|
|
121
|
+
# The partial parser might return lists or None in some edge cases/errors
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
def __escape_invalid_control_chars(self, s: str) -> str:
|
|
125
|
+
"""
|
|
126
|
+
Escapes control characters (U+0000 to U+001F) that are invalid in JSON
|
|
127
|
+
unless escaped. Standard escapes like \n, \t are preserved.
|
|
128
|
+
"""
|
|
129
|
+
# Mapping for standard JSON escapes within the control character range
|
|
130
|
+
escape_map = {
|
|
131
|
+
"\b": "\\b", # Backspace (U+0008)
|
|
132
|
+
"\f": "\\f", # Form feed (U+000C)
|
|
133
|
+
"\n": "\\n", # Line feed (U+000A)
|
|
134
|
+
"\r": "\\r", # Carriage return (U+000D)
|
|
135
|
+
"\t": "\\t" # Horizontal tab (U+0009)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Replacement function for re.sub
|
|
139
|
+
def replace(match: re.Match[str]) -> str:
|
|
140
|
+
ch = match.group(0)
|
|
141
|
+
# Use standard escape if available, otherwise use \uXXXX format
|
|
142
|
+
return escape_map.get(ch, f"\\u{ord(ch):04x}")
|
|
143
|
+
|
|
144
|
+
# Regex to find all characters from U+0000 to U+001F
|
|
145
|
+
return re.sub(r"[\x00-\x1F]", replace, s)
|
|
146
|
+
|
|
147
|
+
def __clean_buffer_after_parse(self):
|
|
148
|
+
"""
|
|
149
|
+
Helper to clean the buffer after a successful parse.
|
|
150
|
+
Removes leading whitespace and searches for the next potential object start.
|
|
151
|
+
If no '{' is found, clears the buffer if it only contains whitespace.
|
|
152
|
+
"""
|
|
153
|
+
self.__buffer = self.__buffer.lstrip()
|
|
154
|
+
next_obj_start = self.__buffer.find("{")
|
|
155
|
+
|
|
156
|
+
if next_obj_start > 0:
|
|
157
|
+
# Found another potential object start, discard text before it
|
|
158
|
+
self.__buffer = self.__buffer[next_obj_start:]
|
|
159
|
+
elif next_obj_start == -1:
|
|
160
|
+
# No more '{' found. If buffer is just whitespace, clear it.
|
|
161
|
+
if not self.__buffer.strip():
|
|
162
|
+
self.__buffer = ""
|
|
163
|
+
# Otherwise, keep the buffer content (might be start of next partial object)
|
|
164
|
+
|
|
165
|
+
def __is_string(self, data: Any) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Helper to check if the provided data is a string.
|
|
168
|
+
Args:
|
|
169
|
+
data: The data to check.
|
|
170
|
+
Returns:
|
|
171
|
+
True if data is a string, False otherwise.
|
|
172
|
+
"""
|
|
173
|
+
return isinstance(data, str)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: streaming-json-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A streaming JSON parser that processes JSON data incrementally, handling partial states. Useful for incrementally parsing partial responses from streaming outputs of Large Language Models (LLMs).
|
|
5
|
+
Author-email: Aramis Facchinetti <aramis.facchinetti16@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aramisfacchinetti/streaming-json-parser
|
|
8
|
+
Project-URL: Repository, https://github.com/aramisfacchinetti/streaming-json-parser
|
|
9
|
+
Keywords: streaming,json,parser,llm,large language model,incremental parsing
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Text Processing
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=8.3.5; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Streaming JSON Parser
|
|
30
|
+
|
|
31
|
+
## Objective
|
|
32
|
+
|
|
33
|
+
This Python module implements a streaming JSON parser designed to process JSON data incrementally. The primary goal is to handle potentially incomplete JSON data streams, such as those produced by Large Language Models (LLMs), and return the current state of the parsed object at any time.
|
|
34
|
+
|
|
35
|
+
## Requirements Subset
|
|
36
|
+
|
|
37
|
+
The parser is specifically designed for a subset of JSON where:
|
|
38
|
+
|
|
39
|
+
- Values consist solely of **strings** and **objects**.
|
|
40
|
+
- **Escape sequences** in strings are not expected (though the implementation handles them).
|
|
41
|
+
- **Duplicate keys** in objects are not expected (though the implementation may tolerate them, typically keeping the last value).
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Incremental Parsing:** Consumes JSON data in chunks via the `consume()` method.
|
|
46
|
+
- **Partial State Retrieval:** The `get()` method returns the currently parsed JSON object state, even if the input stream is incomplete.
|
|
47
|
+
- **Partial String Values:** Returns partial string values as they are received (e.g., `{"key": "val` is valid partial state).
|
|
48
|
+
- **Key Handling:** Keys are only included in the returned object once their value type (string or object start) is identified.
|
|
49
|
+
- **Robustness:** Attempts to parse standard JSON efficiently and falls back to a more lenient state-machine parser for incomplete or slightly non-standard input.
|
|
50
|
+
- **Non-Standard JSON:** Tolerates some non-standard features like unquoted keys and single-quoted strings.
|
|
51
|
+
- **Error Handling:** Attempts to recover from invalid characters or find the first valid JSON object within the buffer.
|
|
52
|
+
- **Support for Primitives & Arrays:** Although the requirements focused on strings and objects, the implementation also handles numbers, booleans, null, and arrays as values within objects.
|
|
53
|
+
|
|
54
|
+
## Implementation Approach
|
|
55
|
+
|
|
56
|
+
1. **Buffering:** The `consume()` method appends incoming data chunks to an internal string buffer after escaping potentially invalid control characters.
|
|
57
|
+
2. **Parsing (`get()`):**
|
|
58
|
+
- The buffer is first cleaned by removing leading whitespace and any characters before the first `{`.
|
|
59
|
+
- It attempts parsing using `json.raw_decode` for speed and standard compliance. If a dictionary is successfully decoded, it's returned, and the consumed portion is removed from the buffer.
|
|
60
|
+
- If `raw_decode` fails (due to incomplete data, syntax errors, or non-standard features), it falls back to the `IterativeStateMachine`.
|
|
61
|
+
- The `IterativeStateMachine` parses the buffer character by character, maintaining state to handle nested structures, different value types (including non-standard ones like unquoted keys), and partial inputs.
|
|
62
|
+
- The `get()` method returns the dictionary parsed by either method and updates the buffer, removing the parsed object and any leading garbage before the _next_ potential object. If no complete object can be parsed, an empty dictionary is returned.
|
|
63
|
+
|
|
64
|
+
## Assumptions and Extensions
|
|
65
|
+
|
|
66
|
+
The implementation makes the following assumptions or extends the requirements:
|
|
67
|
+
|
|
68
|
+
1. **Handling of Additional Primitive Types:** Supports numbers (int, float), booleans (`true`, `false`), and `null` as values, beyond the specified strings and objects.
|
|
69
|
+
2. **Handling of Arrays:** Supports JSON arrays (`[...]`) as values within objects and can parse them, although `get()` only returns top-level _objects_ (`dict`).
|
|
70
|
+
3. **Non-Standard JSON Support:** Tolerates and parses:
|
|
71
|
+
- Unquoted object keys (e.g., `{key: "value"}`).
|
|
72
|
+
- Single-quoted strings (e.g., `{'key': 'value'}`).
|
|
73
|
+
4. **Escape Sequence Handling:** Actively handles standard JSON escape sequences (e.g., `\n`, `\"`) and Unicode escapes (`\uXXXX`) within strings, although they were "not expected".
|
|
74
|
+
5. **Control Character Handling:** Escapes invalid JSON control characters (U+0000 to U+001F) found _outside_ of strings in the input buffer using `\uXXXX` format during `consume`.
|
|
75
|
+
6. **Error Recovery/Robustness:** Discards leading non-JSON data before the first `{` and attempts to parse the first valid object found. Handles multiple objects in the buffer sequentially across `get()` calls.
|
|
76
|
+
7. **Duplicate Keys:** Does not explicitly prevent duplicate keys; standard Python dictionary behavior (last key wins) likely applies.
|
|
77
|
+
8. **Efficiency Strategy:** Uses `json.raw_decode` first, falling back to a custom parser only when necessary.
|
|
78
|
+
9. **Input Type:** `consume` expects string input; other types are ignored.
|
|
79
|
+
|
|
80
|
+
## Algorithmic Complexity
|
|
81
|
+
|
|
82
|
+
The efficiency of the `StreamingJsonParser` depends on the method being called and the nature of the input data stream.
|
|
83
|
+
|
|
84
|
+
- **`consume(buffer: str)`:**
|
|
85
|
+
|
|
86
|
+
- **Time Complexity:** Primarily involves appending the new `buffer` (length `k`) to the internal buffer and performing basic character escaping. This is typically **O(k)**. String concatenation in Python can sometimes be O(N+k) where N is the current buffer size, but often optimized closer to O(k) amortized.
|
|
87
|
+
- **Space Complexity:** Increases the internal buffer size by O(k).
|
|
88
|
+
|
|
89
|
+
- **`get()`:**
|
|
90
|
+
|
|
91
|
+
- **Time Complexity:**
|
|
92
|
+
- **Fast Path (`json.raw_decode`):** If the buffer starts with a complete, standard JSON object of size `P`, Python's built-in decoder is used. This is generally efficient, expected to be around **O(P)**.
|
|
93
|
+
- **Fallback Path (`IterativeStateMachine`):** If `raw_decode` fails (due to incomplete data or non-standard syntax), the custom state machine parses the buffer character by character. In the worst case, it might need to scan a significant portion of the buffer (size `B'`). The complexity is dominated by this scan and subsequent buffer slicing, making it roughly **O(B')**.
|
|
94
|
+
- **Overall:** The complexity varies. It's close to O(P) when complete objects are readily available and standard, and approaches O(B') when parsing incomplete or non-standard streams requires the iterative fallback.
|
|
95
|
+
- **Space Complexity:** Does not inherently allocate significant additional space beyond the internal representation of the parsed object being returned. The main space usage comes from the internal buffer managed by `consume`.
|
|
96
|
+
|
|
97
|
+
- **Overall Space Complexity:** The primary factor is the internal buffer. In the worst case (e.g., a very large stream is consumed without any complete objects being parsed and removed by `get()`), the space complexity can be **O(T)**, where T is the total size of the streamed data received so far. In typical usage where `get()` successfully parses and removes objects, the buffer size stays manageable.
|
|
98
|
+
|
|
99
|
+
## Usage
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# Import the class
|
|
103
|
+
from streaming_json_parser import StreamingJsonParser
|
|
104
|
+
|
|
105
|
+
# Initialize the parser
|
|
106
|
+
parser = StreamingJsonParser()
|
|
107
|
+
|
|
108
|
+
# Consume JSON data chunks
|
|
109
|
+
parser.consume('{"name": "Example", "data": {"val') # Partial object value
|
|
110
|
+
parser.consume('ue": "stream"}') # Complete the object
|
|
111
|
+
|
|
112
|
+
# Get the current state of the parsed object
|
|
113
|
+
# This will return the first complete object found.
|
|
114
|
+
current_object = parser.get()
|
|
115
|
+
print(current_object)
|
|
116
|
+
# Output: {'name': 'Example', 'data': {'value': 'stream'}}
|
|
117
|
+
|
|
118
|
+
# The buffer is cleared/updated after get(), ready for the next object
|
|
119
|
+
parser.consume('{"next": "object"}')
|
|
120
|
+
next_object = parser.get()
|
|
121
|
+
print(next_object)
|
|
122
|
+
# Output: {'next': 'object'}
|
|
123
|
+
|
|
124
|
+
# Example with partial string value
|
|
125
|
+
parser = StreamingJsonParser()
|
|
126
|
+
parser.consume('{"key": "partial string')
|
|
127
|
+
partial_state = parser.get()
|
|
128
|
+
print(partial_state)
|
|
129
|
+
# Output: {'key': 'partial string'}
|
|
130
|
+
|
|
131
|
+
parser.consume(' complete"}')
|
|
132
|
+
complete_state = parser.get()
|
|
133
|
+
print(complete_state)
|
|
134
|
+
# Output: {'key': 'partial string complete'}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Setup
|
|
138
|
+
|
|
139
|
+
To use this parser and run the tests, you need to install the dependencies:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pip install -r requirements.txt
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `requirements.txt` file includes:
|
|
146
|
+
|
|
147
|
+
- `pytest`
|
|
148
|
+
- `pytest-cov`
|
|
149
|
+
|
|
150
|
+
## Testing
|
|
151
|
+
|
|
152
|
+
Unit tests are provided in `test_streaming_json_parser.py`. You can run them using `pytest`:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pytest
|
|
156
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
streaming_json_parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
streaming_json_parser/iterative_state_machine.py,sha256=LHM2tMSYwQ26snwXWiLItj5zp0ds9c5gxY2_TVt5EMQ,24555
|
|
3
|
+
streaming_json_parser/streaming_json_parser.py,sha256=N2QJUeRU6V2mQklL8hwS5rOuE9TT3QwzwNxR1SpDY4M,7220
|
|
4
|
+
streaming_json_parser-0.1.0.dist-info/licenses/LICENSE,sha256=LCzjgEJx8tM9dXg1qVkOuj9AdZgqNYdh2q0GGWqW9s0,1075
|
|
5
|
+
streaming_json_parser-0.1.0.dist-info/METADATA,sha256=Kc7ePMzoMmMI3kb8XCkG1vd39COE1ZbuSoVZPa6g0Ic,9208
|
|
6
|
+
streaming_json_parser-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
7
|
+
streaming_json_parser-0.1.0.dist-info/top_level.txt,sha256=esGYTSNfbg07ZazVHscWNod09hjodlQ8TNYP4sAKYh4,22
|
|
8
|
+
streaming_json_parser-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aramis Facchinetti
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
streaming_json_parser
|