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/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
+ )