jmux 0.0.1__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.
- jmux/__init__.py +0 -0
- jmux/awaitable.py +288 -0
- jmux/decoder.py +66 -0
- jmux/demux.py +869 -0
- jmux/error.py +141 -0
- jmux/helpers.py +60 -0
- jmux/pda.py +32 -0
- jmux/types.py +57 -0
- jmux-0.0.1.dist-info/METADATA +364 -0
- jmux-0.0.1.dist-info/RECORD +13 -0
- jmux-0.0.1.dist-info/WHEEL +5 -0
- jmux-0.0.1.dist-info/licenses/LICENSE +24 -0
- jmux-0.0.1.dist-info/top_level.txt +1 -0
jmux/demux.py
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from types import NoneType
|
|
4
|
+
from typing import (
|
|
5
|
+
Optional,
|
|
6
|
+
Set,
|
|
7
|
+
Type,
|
|
8
|
+
get_args,
|
|
9
|
+
get_origin,
|
|
10
|
+
get_type_hints,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from jmux.awaitable import AwaitableValue, IAsyncSink, SinkType, StreamableValues
|
|
16
|
+
from jmux.decoder import IDecoder, StringEscapeDecoder
|
|
17
|
+
from jmux.error import (
|
|
18
|
+
EmptyKeyError,
|
|
19
|
+
ForbiddenTypeHintsError,
|
|
20
|
+
MissingAttributeError,
|
|
21
|
+
NoCurrentSinkError,
|
|
22
|
+
NotAllObjectPropertiesSetError,
|
|
23
|
+
NothingEmittedError,
|
|
24
|
+
ObjectAlreadyClosedError,
|
|
25
|
+
ObjectMissmatchedError,
|
|
26
|
+
ParsePrimitiveError,
|
|
27
|
+
TypeEmitError,
|
|
28
|
+
UnexpectedAttributeTypeError,
|
|
29
|
+
UnexpectedCharacterError,
|
|
30
|
+
UnexpectedStateError,
|
|
31
|
+
)
|
|
32
|
+
from jmux.helpers import (
|
|
33
|
+
extract_types_from_generic_alias,
|
|
34
|
+
get_main_type,
|
|
35
|
+
is_json_whitespace,
|
|
36
|
+
)
|
|
37
|
+
from jmux.pda import PushDownAutomata
|
|
38
|
+
from jmux.types import (
|
|
39
|
+
ARRAY_CLOSE,
|
|
40
|
+
ARRAY_OPEN,
|
|
41
|
+
BOOLEAN_ALLOWED,
|
|
42
|
+
BOOLEAN_OPEN,
|
|
43
|
+
COLON,
|
|
44
|
+
COMMA,
|
|
45
|
+
FLOAT_ALLOWED,
|
|
46
|
+
INTERGER_ALLOWED,
|
|
47
|
+
JSON_FALSE,
|
|
48
|
+
JSON_NULL,
|
|
49
|
+
JSON_TRUE,
|
|
50
|
+
NULL_ALLOWED,
|
|
51
|
+
NULL_OPEN,
|
|
52
|
+
NUMBER_OPEN,
|
|
53
|
+
OBJECT_CLOSE,
|
|
54
|
+
OBJECT_OPEN,
|
|
55
|
+
PRIMITIVE_STATES,
|
|
56
|
+
QUOTE,
|
|
57
|
+
)
|
|
58
|
+
from jmux.types import Mode as M
|
|
59
|
+
from jmux.types import State as S
|
|
60
|
+
|
|
61
|
+
type Primitive = int | float | str | bool | None
|
|
62
|
+
type Emittable = Primitive | "JMux" | Enum
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Sink[T: Emittable]:
|
|
66
|
+
def __init__(self, delegate: "JMux"):
|
|
67
|
+
self._current_key: Optional[str] = None
|
|
68
|
+
self._current_sink: Optional[IAsyncSink[T]] = None
|
|
69
|
+
self._delegate: "JMux" = delegate
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def current_sink_type(self) -> SinkType:
|
|
73
|
+
if self._current_sink is None:
|
|
74
|
+
raise NoCurrentSinkError()
|
|
75
|
+
return self._current_sink.get_sink_type()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def current_underlying_generics(self) -> Set[Type[T]]:
|
|
79
|
+
if self._current_sink is None:
|
|
80
|
+
raise NoCurrentSinkError()
|
|
81
|
+
return self._current_sink.get_underlying_generics()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def current_underlying_main_generic(self) -> Type[T]:
|
|
85
|
+
if self._current_sink is None:
|
|
86
|
+
raise NoCurrentSinkError()
|
|
87
|
+
return self._current_sink.get_underlying_main_generic()
|
|
88
|
+
|
|
89
|
+
def set_current(self, attr_name: str) -> None:
|
|
90
|
+
if not hasattr(self._delegate, attr_name):
|
|
91
|
+
raise MissingAttributeError(
|
|
92
|
+
object_name=self._delegate.__class__.__name__,
|
|
93
|
+
attribute=attr_name,
|
|
94
|
+
)
|
|
95
|
+
sink = getattr(self._delegate, attr_name)
|
|
96
|
+
if not isinstance(sink, IAsyncSink):
|
|
97
|
+
raise UnexpectedAttributeTypeError(
|
|
98
|
+
attribute=attr_name,
|
|
99
|
+
object_name=type(sink).__name__,
|
|
100
|
+
expected_type="IAsyncSink",
|
|
101
|
+
)
|
|
102
|
+
self._current_key = attr_name
|
|
103
|
+
self._current_sink = sink
|
|
104
|
+
|
|
105
|
+
async def emit(self, val: T) -> None:
|
|
106
|
+
if self._current_sink is None:
|
|
107
|
+
raise NoCurrentSinkError()
|
|
108
|
+
generics = self._current_sink.get_underlying_generics()
|
|
109
|
+
if not any(
|
|
110
|
+
isinstance(val, underlying_generic) for underlying_generic in generics
|
|
111
|
+
):
|
|
112
|
+
raise TypeEmitError(
|
|
113
|
+
expected_type=f"{generics}",
|
|
114
|
+
actual_type=f"{type(val).__name__}",
|
|
115
|
+
)
|
|
116
|
+
await self._current_sink.put(val)
|
|
117
|
+
|
|
118
|
+
async def close(self) -> None:
|
|
119
|
+
if self._current_sink is None:
|
|
120
|
+
raise NoCurrentSinkError()
|
|
121
|
+
await self._current_sink.close()
|
|
122
|
+
|
|
123
|
+
async def ensure_closed(self) -> None:
|
|
124
|
+
if self._current_sink is None:
|
|
125
|
+
raise NoCurrentSinkError()
|
|
126
|
+
await self._current_sink.ensure_closed()
|
|
127
|
+
|
|
128
|
+
async def create_and_emit_nested(self) -> None:
|
|
129
|
+
if self._current_sink is None:
|
|
130
|
+
raise NoCurrentSinkError()
|
|
131
|
+
NestedJmux = self._current_sink.get_underlying_main_generic()
|
|
132
|
+
if not issubclass(NestedJmux, JMux):
|
|
133
|
+
raise TypeEmitError(
|
|
134
|
+
expected_type="JMux",
|
|
135
|
+
actual_type=f"{NestedJmux.__name__}",
|
|
136
|
+
)
|
|
137
|
+
nested = NestedJmux()
|
|
138
|
+
await self.emit(nested)
|
|
139
|
+
|
|
140
|
+
async def forward_char(self, ch: str) -> None:
|
|
141
|
+
if self._current_sink is None:
|
|
142
|
+
raise NoCurrentSinkError()
|
|
143
|
+
maybe_jmux = self._current_sink.get_current()
|
|
144
|
+
if not isinstance(maybe_jmux, JMux):
|
|
145
|
+
raise TypeEmitError(
|
|
146
|
+
expected_type="JMux",
|
|
147
|
+
actual_type=f"{type(maybe_jmux).__name__}",
|
|
148
|
+
)
|
|
149
|
+
await maybe_jmux.feed_char(ch)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class JMux(ABC):
|
|
153
|
+
"""
|
|
154
|
+
JMux is an abstract base class for creating JSON demultiplexers.
|
|
155
|
+
It parses a JSON stream and demultiplexes it into different sinks,
|
|
156
|
+
which can be either AwaitableValue or StreamableValues.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self):
|
|
160
|
+
self._instantiate_attributes()
|
|
161
|
+
self._pda: PushDownAutomata[M, S] = PushDownAutomata[M, S](S.START)
|
|
162
|
+
self._decoder: IDecoder = StringEscapeDecoder()
|
|
163
|
+
self._sink = Sink[Emittable](self)
|
|
164
|
+
|
|
165
|
+
def _instantiate_attributes(self) -> None:
|
|
166
|
+
type_hints = get_type_hints(self.__class__)
|
|
167
|
+
for attr_name, type_alias in type_hints.items():
|
|
168
|
+
TargetType = get_origin(type_alias)
|
|
169
|
+
type_alias_args = get_args(type_alias)
|
|
170
|
+
if len(type_alias_args) != 1:
|
|
171
|
+
raise TypeError(f"Generic type {type_alias} must be fully specified")
|
|
172
|
+
TargetGenericType = type_alias_args[0]
|
|
173
|
+
target_instance = TargetType[TargetGenericType]()
|
|
174
|
+
if not issubclass(TargetType, IAsyncSink):
|
|
175
|
+
raise TypeError(
|
|
176
|
+
f"Attribute '{attr_name}' must conform to protocol IAsyncSink, "
|
|
177
|
+
f"got {TargetType}."
|
|
178
|
+
)
|
|
179
|
+
setattr(self, attr_name, target_instance)
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def assert_conforms_to(cls, pydantic_model: Type[BaseModel]) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Asserts that the JMux class conforms to a given Pydantic model.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
pydantic_model: The Pydantic model to compare against.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
MissingAttributeError: If an attribute is missing in the Pydantic model.
|
|
191
|
+
ForbiddenTypeHintsError: If a type hint is not allowed.
|
|
192
|
+
ObjectMissmatchedError: If the JMux class does not match the Pydantic model.
|
|
193
|
+
"""
|
|
194
|
+
for attr_name, type_alias in get_type_hints(cls).items():
|
|
195
|
+
jmux_main_type_set, jmux_subtype_set = extract_types_from_generic_alias(
|
|
196
|
+
type_alias
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
MaybePydanticType = get_type_hints(pydantic_model).get(attr_name, None)
|
|
200
|
+
if MaybePydanticType is None:
|
|
201
|
+
if NoneType in jmux_subtype_set:
|
|
202
|
+
continue
|
|
203
|
+
else:
|
|
204
|
+
raise MissingAttributeError(
|
|
205
|
+
object_name=pydantic_model.__name__,
|
|
206
|
+
attribute=attr_name,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
pydantic_main_type_set, pydantic_subtype_set = (
|
|
210
|
+
extract_types_from_generic_alias(MaybePydanticType)
|
|
211
|
+
)
|
|
212
|
+
cls._assert_only_allowed_types(
|
|
213
|
+
jmux_main_type_set,
|
|
214
|
+
jmux_subtype_set,
|
|
215
|
+
pydantic_main_type_set,
|
|
216
|
+
pydantic_subtype_set,
|
|
217
|
+
)
|
|
218
|
+
if (
|
|
219
|
+
pydantic_wrong := len(pydantic_main_type_set) != 1
|
|
220
|
+
and len(pydantic_subtype_set) > 0
|
|
221
|
+
) or len(jmux_main_type_set) != 1:
|
|
222
|
+
wrong_obj = "pydantic" if pydantic_wrong else "JMux"
|
|
223
|
+
wrong_set = (
|
|
224
|
+
jmux_main_type_set if pydantic_wrong else pydantic_main_type_set
|
|
225
|
+
)
|
|
226
|
+
raise ForbiddenTypeHintsError(
|
|
227
|
+
message=(f"Forbidden typing received on {wrong_obj}: {wrong_set}"),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if StreamableValues in jmux_main_type_set:
|
|
231
|
+
cls._assert_is_allowed_streamable_values(
|
|
232
|
+
jmux_subtype_set,
|
|
233
|
+
pydantic_subtype_set,
|
|
234
|
+
pydantic_main_type_set,
|
|
235
|
+
pydantic_model,
|
|
236
|
+
attr_name,
|
|
237
|
+
)
|
|
238
|
+
elif AwaitableValue in jmux_main_type_set:
|
|
239
|
+
cls._assert_is_allowed_awaitable_value(
|
|
240
|
+
jmux_subtype_set,
|
|
241
|
+
pydantic_subtype_set,
|
|
242
|
+
pydantic_main_type_set,
|
|
243
|
+
pydantic_model,
|
|
244
|
+
attr_name,
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
raise ObjectMissmatchedError(
|
|
248
|
+
jmux_model=cls.__name__,
|
|
249
|
+
pydantic_model=pydantic_model.__name__,
|
|
250
|
+
attribute=attr_name,
|
|
251
|
+
message="Unexpected main type on JMux",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def _assert_is_allowed_streamable_values(
|
|
256
|
+
cls,
|
|
257
|
+
jmux_subtype_set: Set[Type],
|
|
258
|
+
pydantic_subtype_set: Set[Type],
|
|
259
|
+
pydantic_main_type_set: Set[Type],
|
|
260
|
+
pydantic_model: Type[BaseModel],
|
|
261
|
+
attr_name: str,
|
|
262
|
+
):
|
|
263
|
+
if len(jmux_subtype_set) != 1:
|
|
264
|
+
raise ForbiddenTypeHintsError(
|
|
265
|
+
"StreamableValues must have exactly one underlying type, "
|
|
266
|
+
f"got {jmux_subtype_set}."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if list in pydantic_main_type_set:
|
|
270
|
+
NonNoneType = get_main_type(jmux_subtype_set)
|
|
271
|
+
PydanticNonNoneType = get_main_type(pydantic_subtype_set)
|
|
272
|
+
if issubclass(NonNoneType, JMux):
|
|
273
|
+
NonNoneType.assert_conforms_to(PydanticNonNoneType)
|
|
274
|
+
return
|
|
275
|
+
if jmux_subtype_set != pydantic_subtype_set:
|
|
276
|
+
raise ObjectMissmatchedError(
|
|
277
|
+
jmux_model=cls.__name__,
|
|
278
|
+
pydantic_model=pydantic_model.__name__,
|
|
279
|
+
attribute=attr_name,
|
|
280
|
+
message=(
|
|
281
|
+
"StreamableValues of type list with subtype "
|
|
282
|
+
f"{jmux_subtype_set} does not match pydantic model "
|
|
283
|
+
f"type: {pydantic_subtype_set}"
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
elif str in pydantic_main_type_set:
|
|
287
|
+
if jmux_subtype_set != pydantic_main_type_set:
|
|
288
|
+
raise ObjectMissmatchedError(
|
|
289
|
+
jmux_model=cls.__name__,
|
|
290
|
+
pydantic_model=pydantic_model.__name__,
|
|
291
|
+
attribute=attr_name,
|
|
292
|
+
message=(
|
|
293
|
+
"StreamableValues of type string does not match pydantic "
|
|
294
|
+
f"model type: {pydantic_main_type_set}"
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
raise ForbiddenTypeHintsError(
|
|
299
|
+
message="StreamableValues must be initialized with a sequence type."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def _assert_is_allowed_awaitable_value(
|
|
304
|
+
cls,
|
|
305
|
+
jmux_subtype_set: Set[Type],
|
|
306
|
+
pydantic_subtype_set: Set[Type],
|
|
307
|
+
pydantic_main_type_set: Set[Type],
|
|
308
|
+
pydantic_model: Type[BaseModel],
|
|
309
|
+
attr_name: str,
|
|
310
|
+
):
|
|
311
|
+
if len(pydantic_subtype_set) > 0:
|
|
312
|
+
raise ForbiddenTypeHintsError(
|
|
313
|
+
message="Pydantic model cannot have subtype for AwaitableValue."
|
|
314
|
+
)
|
|
315
|
+
NonNoneType = get_main_type(jmux_subtype_set)
|
|
316
|
+
PydanticNonNoneType = get_main_type(pydantic_main_type_set)
|
|
317
|
+
if issubclass(NonNoneType, JMux):
|
|
318
|
+
NonNoneType.assert_conforms_to(PydanticNonNoneType)
|
|
319
|
+
return
|
|
320
|
+
if not jmux_subtype_set == pydantic_main_type_set:
|
|
321
|
+
raise ObjectMissmatchedError(
|
|
322
|
+
jmux_model=cls.__name__,
|
|
323
|
+
pydantic_model=pydantic_model.__name__,
|
|
324
|
+
attribute=attr_name,
|
|
325
|
+
message=(
|
|
326
|
+
f"AwaitableValue with type {jmux_subtype_set} does not match "
|
|
327
|
+
f"pydantic model type: {pydantic_main_type_set}"
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def _assert_only_allowed_types(
|
|
333
|
+
cls,
|
|
334
|
+
jmux_main_type_set: Set[Type],
|
|
335
|
+
jmux_subtype_set: Set[Type],
|
|
336
|
+
pydantic_main_type_set: Set[Type],
|
|
337
|
+
pydantic_subtype_set: Set[Type],
|
|
338
|
+
) -> None:
|
|
339
|
+
if not all(t in (AwaitableValue, StreamableValues) for t in jmux_main_type_set):
|
|
340
|
+
raise ForbiddenTypeHintsError(
|
|
341
|
+
message=(
|
|
342
|
+
"JMux must have either AwaitableValue or StreamableValues as "
|
|
343
|
+
f"main type, got {jmux_main_type_set}."
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if not any(
|
|
348
|
+
issubclass(elem, t)
|
|
349
|
+
for t in (int, float, str, bool, NoneType, JMux, Enum)
|
|
350
|
+
for elem in jmux_subtype_set
|
|
351
|
+
):
|
|
352
|
+
raise ForbiddenTypeHintsError(
|
|
353
|
+
message=(
|
|
354
|
+
"JMux sub type must be one of the emittable types: "
|
|
355
|
+
f"{jmux_subtype_set}."
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if len(pydantic_subtype_set) > 0 and not any(
|
|
360
|
+
issubclass(elem, t)
|
|
361
|
+
for t in (int, float, str, bool, NoneType, BaseModel, Enum)
|
|
362
|
+
for elem in pydantic_subtype_set
|
|
363
|
+
):
|
|
364
|
+
raise ForbiddenTypeHintsError(
|
|
365
|
+
message=(
|
|
366
|
+
"Pydantic sub type must be one of the primitive, enum or "
|
|
367
|
+
f"BaseModel, got: {pydantic_subtype_set}."
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if not any(
|
|
372
|
+
issubclass(elem, t)
|
|
373
|
+
for t in (int, float, str, bool, list, NoneType, BaseModel, Enum)
|
|
374
|
+
for elem in pydantic_main_type_set
|
|
375
|
+
):
|
|
376
|
+
raise ForbiddenTypeHintsError(
|
|
377
|
+
message=(
|
|
378
|
+
"Pydantic main type must be one of the primitive, enum, list "
|
|
379
|
+
f"or BaseModel, got {pydantic_main_type_set}."
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
async def feed_chunks(self, chunks: str) -> None:
|
|
384
|
+
"""
|
|
385
|
+
Feeds a string of characters to the JMux parser.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
chunks: The string of characters to feed.
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
UnexpectedCharacterError: If an unexpected character is encountered.
|
|
392
|
+
ObjectAlreadyClosedError: If the JMux object is already closed.
|
|
393
|
+
UnexpectedStateError: If the parser is in an unexpected state.
|
|
394
|
+
EmptyKeyError: If an empty key is encountered in a JSON object.
|
|
395
|
+
"""
|
|
396
|
+
for ch in chunks:
|
|
397
|
+
await self.feed_char(ch)
|
|
398
|
+
|
|
399
|
+
async def feed_char(self, ch: str) -> None:
|
|
400
|
+
"""
|
|
401
|
+
Feeds a character to the JMux parser.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
ch: The character to feed.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
UnexpectedCharacterError: If an unexpected character is encountered.
|
|
408
|
+
ObjectAlreadyClosedError: If the JMux object is already closed.
|
|
409
|
+
UnexpectedStateError: If the parser is in an unexpected state.
|
|
410
|
+
EmptyKeyError: If an empty key is encountered in a JSON object.
|
|
411
|
+
"""
|
|
412
|
+
if len(ch) != 1:
|
|
413
|
+
raise UnexpectedCharacterError(
|
|
414
|
+
character=ch,
|
|
415
|
+
pda_stack=self._pda.stack,
|
|
416
|
+
pda_state=self._pda.state,
|
|
417
|
+
message="Only single characters are allowed to be fed to JMux.",
|
|
418
|
+
)
|
|
419
|
+
match self._pda.top:
|
|
420
|
+
# CONTEXT: Start
|
|
421
|
+
case None:
|
|
422
|
+
match self._pda.state:
|
|
423
|
+
case S.START:
|
|
424
|
+
if ch in OBJECT_OPEN:
|
|
425
|
+
self._pda.push(M.ROOT)
|
|
426
|
+
self._pda.set_state(S.EXPECT_KEY)
|
|
427
|
+
else:
|
|
428
|
+
raise UnexpectedCharacterError(
|
|
429
|
+
ch,
|
|
430
|
+
self._pda.stack,
|
|
431
|
+
self._pda.state,
|
|
432
|
+
"JSON must start with '{' character.",
|
|
433
|
+
)
|
|
434
|
+
case S.END:
|
|
435
|
+
raise ObjectAlreadyClosedError(
|
|
436
|
+
object_name=self.__class__.__name__,
|
|
437
|
+
message=(
|
|
438
|
+
"Cannot feed more characters to closed JMux "
|
|
439
|
+
f"object, got '{ch}'"
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
case _:
|
|
443
|
+
raise UnexpectedStateError(
|
|
444
|
+
self._pda.stack,
|
|
445
|
+
self._pda.state,
|
|
446
|
+
message=(
|
|
447
|
+
"Only START and END states are allowed in the root "
|
|
448
|
+
"context."
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# CONTEXT: Root
|
|
453
|
+
case M.ROOT:
|
|
454
|
+
match self._pda.state:
|
|
455
|
+
case S.EXPECT_KEY:
|
|
456
|
+
if is_json_whitespace(ch):
|
|
457
|
+
pass
|
|
458
|
+
elif ch == '"':
|
|
459
|
+
self._pda.set_state(S.PARSING_KEY)
|
|
460
|
+
self._decoder.reset()
|
|
461
|
+
else:
|
|
462
|
+
raise UnexpectedCharacterError(
|
|
463
|
+
ch,
|
|
464
|
+
self._pda.stack,
|
|
465
|
+
self._pda.state,
|
|
466
|
+
"Char needs to be '\"' or JSON whitespaces",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
case S.PARSING_KEY:
|
|
470
|
+
if self._decoder.is_terminating_quote(ch):
|
|
471
|
+
buffer = self._decoder.buffer
|
|
472
|
+
if not buffer:
|
|
473
|
+
raise EmptyKeyError(
|
|
474
|
+
"Empty key is not allowed in JSON objects."
|
|
475
|
+
)
|
|
476
|
+
self._sink.set_current(buffer)
|
|
477
|
+
self._decoder.reset()
|
|
478
|
+
self._pda.set_state(S.EXPECT_COLON)
|
|
479
|
+
else:
|
|
480
|
+
self._decoder.push(ch)
|
|
481
|
+
|
|
482
|
+
case S.EXPECT_COLON:
|
|
483
|
+
if is_json_whitespace(ch):
|
|
484
|
+
pass
|
|
485
|
+
elif ch in COLON:
|
|
486
|
+
self._pda.set_state(S.EXPECT_VALUE)
|
|
487
|
+
else:
|
|
488
|
+
raise UnexpectedCharacterError(
|
|
489
|
+
ch,
|
|
490
|
+
self._pda.stack,
|
|
491
|
+
self._pda.state,
|
|
492
|
+
"Char must be ':' or JSON whitespaces.",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
case S.EXPECT_VALUE:
|
|
496
|
+
if is_json_whitespace(ch):
|
|
497
|
+
pass
|
|
498
|
+
elif res := await self._handle_common__expect_value(ch):
|
|
499
|
+
if (
|
|
500
|
+
self._sink.current_sink_type
|
|
501
|
+
is SinkType.STREAMABLE_VALUES
|
|
502
|
+
and res is not S.PARSING_STRING
|
|
503
|
+
):
|
|
504
|
+
raise UnexpectedCharacterError(
|
|
505
|
+
ch,
|
|
506
|
+
self._pda.stack,
|
|
507
|
+
self._pda.state,
|
|
508
|
+
"Expected '[' or '\"' for 'StreamableValues'",
|
|
509
|
+
)
|
|
510
|
+
elif ch in ARRAY_OPEN:
|
|
511
|
+
self._pda.set_state(S.EXPECT_VALUE)
|
|
512
|
+
self._pda.push(M.ARRAY)
|
|
513
|
+
else:
|
|
514
|
+
raise UnexpectedCharacterError(
|
|
515
|
+
ch,
|
|
516
|
+
self._pda.stack,
|
|
517
|
+
self._pda.state,
|
|
518
|
+
"Expected '[' or white space.",
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
case S.PARSING_STRING:
|
|
522
|
+
if self._decoder.is_terminating_quote(ch):
|
|
523
|
+
if self._sink.current_sink_type == SinkType.AWAITABLE_VALUE:
|
|
524
|
+
MainType = self._sink.current_underlying_main_generic
|
|
525
|
+
if issubclass(MainType, Enum):
|
|
526
|
+
try:
|
|
527
|
+
value = MainType(self._decoder.buffer)
|
|
528
|
+
await self._sink.emit(value)
|
|
529
|
+
except ValueError as e:
|
|
530
|
+
raise ParsePrimitiveError(
|
|
531
|
+
f"Invalid enum value: "
|
|
532
|
+
f"{self._decoder.buffer}"
|
|
533
|
+
) from e
|
|
534
|
+
else:
|
|
535
|
+
await self._sink.emit(self._decoder.buffer)
|
|
536
|
+
self._decoder.reset()
|
|
537
|
+
await self._sink.close()
|
|
538
|
+
self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
|
|
539
|
+
else:
|
|
540
|
+
maybe_char = self._decoder.push(ch)
|
|
541
|
+
if (
|
|
542
|
+
maybe_char is not None
|
|
543
|
+
and self._sink.current_sink_type
|
|
544
|
+
is SinkType.STREAMABLE_VALUES
|
|
545
|
+
):
|
|
546
|
+
await self._sink.emit(maybe_char)
|
|
547
|
+
|
|
548
|
+
case _ if self._pda.state in PRIMITIVE_STATES:
|
|
549
|
+
if ch not in COMMA | OBJECT_CLOSE:
|
|
550
|
+
self._assert_primitive_character_allowed_in_state(ch)
|
|
551
|
+
self._decoder.push(ch)
|
|
552
|
+
else:
|
|
553
|
+
await self._parse_primitive()
|
|
554
|
+
await self._sink.close()
|
|
555
|
+
self._decoder.reset()
|
|
556
|
+
self._pda.set_state(S.EXPECT_KEY)
|
|
557
|
+
if ch in OBJECT_CLOSE:
|
|
558
|
+
await self._finalize()
|
|
559
|
+
|
|
560
|
+
case S.EXPECT_COMMA_OR_EOC:
|
|
561
|
+
if is_json_whitespace(ch):
|
|
562
|
+
pass
|
|
563
|
+
elif ch in COMMA:
|
|
564
|
+
self._pda.set_state(S.EXPECT_KEY)
|
|
565
|
+
elif ch in OBJECT_CLOSE:
|
|
566
|
+
await self._finalize()
|
|
567
|
+
else:
|
|
568
|
+
raise UnexpectedCharacterError(
|
|
569
|
+
ch,
|
|
570
|
+
self._pda.stack,
|
|
571
|
+
self._pda.state,
|
|
572
|
+
"Expected ',', '}' or white space.",
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
case _:
|
|
576
|
+
raise UnexpectedStateError(
|
|
577
|
+
self._pda.stack,
|
|
578
|
+
self._pda.state,
|
|
579
|
+
message="State not allowed in root context.",
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# CONTEXT: Array
|
|
583
|
+
case M.ARRAY:
|
|
584
|
+
if ch in ARRAY_OPEN:
|
|
585
|
+
raise UnexpectedCharacterError(
|
|
586
|
+
ch,
|
|
587
|
+
self._pda.stack,
|
|
588
|
+
self._pda.state,
|
|
589
|
+
"No support for 2-dimensional arrays.",
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
match self._pda.state:
|
|
593
|
+
case S.EXPECT_VALUE:
|
|
594
|
+
if is_json_whitespace(ch):
|
|
595
|
+
pass
|
|
596
|
+
elif await self._handle_common__expect_value(ch):
|
|
597
|
+
pass
|
|
598
|
+
elif ch in ARRAY_CLOSE:
|
|
599
|
+
await self._close_context(S.EXPECT_COMMA_OR_EOC)
|
|
600
|
+
else:
|
|
601
|
+
raise UnexpectedCharacterError(
|
|
602
|
+
ch,
|
|
603
|
+
self._pda.stack,
|
|
604
|
+
self._pda.state,
|
|
605
|
+
"Expected value, ']' or white space",
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
case S.PARSING_STRING:
|
|
609
|
+
if self._sink.current_sink_type is SinkType.AWAITABLE_VALUE:
|
|
610
|
+
raise UnexpectedCharacterError(
|
|
611
|
+
ch,
|
|
612
|
+
self._pda.stack,
|
|
613
|
+
self._pda.state,
|
|
614
|
+
(
|
|
615
|
+
"Cannot parse string inside of an array with "
|
|
616
|
+
"AwaitableValue sink type."
|
|
617
|
+
),
|
|
618
|
+
)
|
|
619
|
+
if self._decoder.is_terminating_quote(ch):
|
|
620
|
+
MainType = self._sink.current_underlying_main_generic
|
|
621
|
+
if issubclass(MainType, Enum):
|
|
622
|
+
try:
|
|
623
|
+
value = MainType(self._decoder.buffer)
|
|
624
|
+
await self._sink.emit(value)
|
|
625
|
+
except ValueError as e:
|
|
626
|
+
raise ParsePrimitiveError(
|
|
627
|
+
f"Invalid enum value: {self._decoder.buffer}"
|
|
628
|
+
) from e
|
|
629
|
+
else:
|
|
630
|
+
await self._sink.emit(self._decoder.buffer)
|
|
631
|
+
self._decoder.reset()
|
|
632
|
+
self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
|
|
633
|
+
else:
|
|
634
|
+
self._decoder.push(ch)
|
|
635
|
+
|
|
636
|
+
case _ if self._pda.state in PRIMITIVE_STATES:
|
|
637
|
+
if ch not in COMMA | ARRAY_CLOSE:
|
|
638
|
+
self._assert_primitive_character_allowed_in_state(ch)
|
|
639
|
+
self._decoder.push(ch)
|
|
640
|
+
else:
|
|
641
|
+
await self._parse_primitive()
|
|
642
|
+
self._decoder.reset()
|
|
643
|
+
if ch in COMMA:
|
|
644
|
+
self._pda.set_state(S.EXPECT_VALUE)
|
|
645
|
+
elif ch in ARRAY_CLOSE:
|
|
646
|
+
await self._close_context(S.EXPECT_COMMA_OR_EOC)
|
|
647
|
+
|
|
648
|
+
case S.EXPECT_COMMA_OR_EOC:
|
|
649
|
+
if is_json_whitespace(ch):
|
|
650
|
+
pass
|
|
651
|
+
elif ch in COMMA:
|
|
652
|
+
self._pda.set_state(S.EXPECT_VALUE)
|
|
653
|
+
elif ch in ARRAY_CLOSE:
|
|
654
|
+
await self._close_context(S.EXPECT_COMMA_OR_EOC)
|
|
655
|
+
else:
|
|
656
|
+
raise UnexpectedCharacterError(
|
|
657
|
+
ch,
|
|
658
|
+
self._pda.stack,
|
|
659
|
+
self._pda.state,
|
|
660
|
+
"Expected ',', ']' or white space.",
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
case _:
|
|
664
|
+
raise UnexpectedStateError(
|
|
665
|
+
self._pda.stack,
|
|
666
|
+
self._pda.state,
|
|
667
|
+
message="State not allowed in array context.",
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# CONTEXT: Object
|
|
671
|
+
case M.OBJECT:
|
|
672
|
+
if self._pda.state is not S.PARSING_OBJECT:
|
|
673
|
+
raise UnexpectedCharacterError(
|
|
674
|
+
ch,
|
|
675
|
+
self._pda.stack,
|
|
676
|
+
self._pda.state,
|
|
677
|
+
"State in object context must be 'parsing_object'",
|
|
678
|
+
)
|
|
679
|
+
if ch in OBJECT_CLOSE:
|
|
680
|
+
self._pda.pop()
|
|
681
|
+
if self._pda.top is M.ROOT:
|
|
682
|
+
await self._sink.close()
|
|
683
|
+
self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
|
|
684
|
+
return
|
|
685
|
+
else:
|
|
686
|
+
await self._sink.forward_char(ch)
|
|
687
|
+
return
|
|
688
|
+
|
|
689
|
+
async def _parse_primitive(self) -> None:
|
|
690
|
+
if self._pda.state is S.PARSING_NULL:
|
|
691
|
+
if not self._decoder.buffer == JSON_NULL:
|
|
692
|
+
raise ParsePrimitiveError(
|
|
693
|
+
f"Expected 'null', got '{self._decoder.buffer}'"
|
|
694
|
+
)
|
|
695
|
+
await self._sink.emit(None)
|
|
696
|
+
elif self._pda.state is S.PARSING_BOOLEAN:
|
|
697
|
+
await self._sink.emit(self._decoder.buffer == JSON_TRUE)
|
|
698
|
+
else:
|
|
699
|
+
try:
|
|
700
|
+
buffer = self._decoder.buffer
|
|
701
|
+
generic = self._sink.current_underlying_main_generic
|
|
702
|
+
value = float(buffer) if issubclass(generic, float) else int(buffer)
|
|
703
|
+
except ValueError as e:
|
|
704
|
+
raise ParsePrimitiveError(f"Buffer: {buffer}; Error: {e}") from e
|
|
705
|
+
await self._sink.emit(value)
|
|
706
|
+
|
|
707
|
+
async def _handle_common__expect_value(self, ch: str) -> S | None:
|
|
708
|
+
generic_set = self._sink.current_underlying_generics
|
|
709
|
+
generic = self._sink.current_underlying_main_generic
|
|
710
|
+
if ch in QUOTE:
|
|
711
|
+
if not (str in generic_set or issubclass(generic, Enum)):
|
|
712
|
+
raise UnexpectedCharacterError(
|
|
713
|
+
ch,
|
|
714
|
+
self._pda.stack,
|
|
715
|
+
self._pda.state,
|
|
716
|
+
(
|
|
717
|
+
"Trying to parse 'string' but underlying generic is "
|
|
718
|
+
f"'{generic_set}'."
|
|
719
|
+
),
|
|
720
|
+
)
|
|
721
|
+
self._pda.set_state(S.PARSING_STRING)
|
|
722
|
+
self._decoder.reset()
|
|
723
|
+
return S.PARSING_STRING
|
|
724
|
+
if ch in NUMBER_OPEN:
|
|
725
|
+
if not any(t in generic_set for t in (int, float)):
|
|
726
|
+
raise UnexpectedCharacterError(
|
|
727
|
+
ch,
|
|
728
|
+
self._pda.stack,
|
|
729
|
+
self._pda.state,
|
|
730
|
+
(
|
|
731
|
+
"Trying to parse 'number' but underlying generic is "
|
|
732
|
+
f"'{generic_set}'."
|
|
733
|
+
),
|
|
734
|
+
)
|
|
735
|
+
self._decoder.push(ch)
|
|
736
|
+
if generic is int:
|
|
737
|
+
self._pda.set_state(S.PARSING_INTEGER)
|
|
738
|
+
return S.PARSING_INTEGER
|
|
739
|
+
else:
|
|
740
|
+
self._pda.set_state(S.PARSING_FLOAT)
|
|
741
|
+
return S.PARSING_FLOAT
|
|
742
|
+
if ch in BOOLEAN_OPEN:
|
|
743
|
+
if bool not in generic_set:
|
|
744
|
+
raise UnexpectedCharacterError(
|
|
745
|
+
ch,
|
|
746
|
+
self._pda.stack,
|
|
747
|
+
self._pda.state,
|
|
748
|
+
(
|
|
749
|
+
"Trying to parse 'boolean' but underlying generic is "
|
|
750
|
+
f"'{generic.__name__}'."
|
|
751
|
+
),
|
|
752
|
+
)
|
|
753
|
+
self._pda.set_state(S.PARSING_BOOLEAN)
|
|
754
|
+
self._decoder.push(ch)
|
|
755
|
+
return S.PARSING_BOOLEAN
|
|
756
|
+
if ch in NULL_OPEN:
|
|
757
|
+
if NoneType not in generic_set:
|
|
758
|
+
raise UnexpectedCharacterError(
|
|
759
|
+
ch,
|
|
760
|
+
self._pda.stack,
|
|
761
|
+
self._pda.state,
|
|
762
|
+
(
|
|
763
|
+
"Trying to parse 'null' but underlying generic is "
|
|
764
|
+
f"'{generic.__name__}'."
|
|
765
|
+
),
|
|
766
|
+
)
|
|
767
|
+
self._pda.set_state(S.PARSING_NULL)
|
|
768
|
+
self._decoder.push(ch)
|
|
769
|
+
return S.PARSING_NULL
|
|
770
|
+
if ch in OBJECT_OPEN:
|
|
771
|
+
if not issubclass(generic, JMux):
|
|
772
|
+
raise UnexpectedCharacterError(
|
|
773
|
+
ch,
|
|
774
|
+
self._pda.stack,
|
|
775
|
+
self._pda.state,
|
|
776
|
+
(
|
|
777
|
+
f"Trying to parse 'object' but underlying generic is "
|
|
778
|
+
f"'{generic.__name__}'."
|
|
779
|
+
),
|
|
780
|
+
)
|
|
781
|
+
await self._sink.create_and_emit_nested()
|
|
782
|
+
await self._sink.forward_char(ch)
|
|
783
|
+
self._pda.set_state(S.PARSING_OBJECT)
|
|
784
|
+
self._pda.push(M.OBJECT)
|
|
785
|
+
return S.PARSING_OBJECT
|
|
786
|
+
|
|
787
|
+
async def _close_context(self, new_state: S) -> None:
|
|
788
|
+
await self._sink.close()
|
|
789
|
+
self._pda.pop()
|
|
790
|
+
self._pda.set_state(new_state)
|
|
791
|
+
|
|
792
|
+
async def _finalize(self) -> None:
|
|
793
|
+
type_hints = get_type_hints(self.__class__)
|
|
794
|
+
for attr_name, _ in type_hints.items():
|
|
795
|
+
self._sink.set_current(attr_name)
|
|
796
|
+
try:
|
|
797
|
+
await self._sink.ensure_closed()
|
|
798
|
+
except NothingEmittedError as e:
|
|
799
|
+
raise NotAllObjectPropertiesSetError(
|
|
800
|
+
f"Unable to finalize. Property '{attr_name}' was not set before "
|
|
801
|
+
"closing the JMux instance."
|
|
802
|
+
) from e
|
|
803
|
+
|
|
804
|
+
self._pda.pop()
|
|
805
|
+
self._pda.set_state(S.END)
|
|
806
|
+
|
|
807
|
+
def _assert_primitive_character_allowed_in_state(self, ch: str) -> None:
|
|
808
|
+
if self._pda.state is S.PARSING_INTEGER:
|
|
809
|
+
if ch not in INTERGER_ALLOWED:
|
|
810
|
+
raise UnexpectedCharacterError(
|
|
811
|
+
ch,
|
|
812
|
+
self._pda.stack,
|
|
813
|
+
self._pda.state,
|
|
814
|
+
"Trying to parse 'integer' but received unexpected character.",
|
|
815
|
+
)
|
|
816
|
+
elif self._pda.state is S.PARSING_FLOAT:
|
|
817
|
+
if ch not in FLOAT_ALLOWED:
|
|
818
|
+
raise UnexpectedCharacterError(
|
|
819
|
+
ch,
|
|
820
|
+
self._pda.stack,
|
|
821
|
+
self._pda.state,
|
|
822
|
+
"Trying to parse 'float' but received unexpected character.",
|
|
823
|
+
)
|
|
824
|
+
elif self._pda.state is S.PARSING_BOOLEAN:
|
|
825
|
+
if ch not in BOOLEAN_ALLOWED:
|
|
826
|
+
raise UnexpectedCharacterError(
|
|
827
|
+
ch,
|
|
828
|
+
self._pda.stack,
|
|
829
|
+
self._pda.state,
|
|
830
|
+
"Trying to parse 'boolean' but received unexpected character.",
|
|
831
|
+
)
|
|
832
|
+
if not any(
|
|
833
|
+
bool_str.startswith(f"{self._decoder.buffer}{ch}")
|
|
834
|
+
for bool_str in (JSON_TRUE, JSON_FALSE)
|
|
835
|
+
):
|
|
836
|
+
raise UnexpectedCharacterError(
|
|
837
|
+
ch,
|
|
838
|
+
self._pda.stack,
|
|
839
|
+
self._pda.state,
|
|
840
|
+
(
|
|
841
|
+
"Unexpected character added to buffer for 'boolean': "
|
|
842
|
+
f"'{self._decoder.buffer}{ch}'."
|
|
843
|
+
),
|
|
844
|
+
)
|
|
845
|
+
elif self._pda.state is S.PARSING_NULL:
|
|
846
|
+
if ch not in NULL_ALLOWED:
|
|
847
|
+
raise UnexpectedCharacterError(
|
|
848
|
+
ch,
|
|
849
|
+
self._pda.stack,
|
|
850
|
+
self._pda.state,
|
|
851
|
+
"Trying to parse 'null' but received unexpected character.",
|
|
852
|
+
)
|
|
853
|
+
if not JSON_NULL.startswith(f"{self._decoder.buffer}{ch}"):
|
|
854
|
+
raise UnexpectedCharacterError(
|
|
855
|
+
ch,
|
|
856
|
+
self._pda.stack,
|
|
857
|
+
self._pda.state,
|
|
858
|
+
(
|
|
859
|
+
f"Unexpected character added to buffer for 'null': "
|
|
860
|
+
f"'{self._decoder.buffer}{ch}'."
|
|
861
|
+
),
|
|
862
|
+
)
|
|
863
|
+
else:
|
|
864
|
+
raise UnexpectedCharacterError(
|
|
865
|
+
ch,
|
|
866
|
+
self._pda.stack,
|
|
867
|
+
self._pda.state,
|
|
868
|
+
"An unexpected error happened.",
|
|
869
|
+
)
|