jmux 0.0.2__py3-none-any.whl → 0.0.4__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 CHANGED
@@ -32,7 +32,7 @@ from jmux.error import (
32
32
  from jmux.helpers import (
33
33
  extract_types_from_generic_alias,
34
34
  get_main_type,
35
- is_json_whitespace,
35
+ str_to_bool,
36
36
  )
37
37
  from jmux.pda import PushDownAutomata
38
38
  from jmux.types import (
@@ -47,12 +47,13 @@ from jmux.types import (
47
47
  JSON_FALSE,
48
48
  JSON_NULL,
49
49
  JSON_TRUE,
50
+ JSON_WHITESPACE,
50
51
  NULL_ALLOWED,
51
52
  NULL_OPEN,
52
53
  NUMBER_OPEN,
53
54
  OBJECT_CLOSE,
54
55
  OBJECT_OPEN,
55
- PRIMITIVE_STATES,
56
+ PARSING_PRIMITIVE_STATES,
56
57
  QUOTE,
57
58
  )
58
59
  from jmux.types import Mode as M
@@ -215,17 +216,11 @@ class JMux(ABC):
215
216
  pydantic_main_type_set,
216
217
  pydantic_subtype_set,
217
218
  )
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
- )
219
+ cls._assert_correct_set_combinations(
220
+ jmux_main_type_set,
221
+ pydantic_main_type_set,
222
+ pydantic_subtype_set,
223
+ )
229
224
 
230
225
  if StreamableValues in jmux_main_type_set:
231
226
  cls._assert_is_allowed_streamable_values(
@@ -251,6 +246,80 @@ class JMux(ABC):
251
246
  message="Unexpected main type on JMux",
252
247
  )
253
248
 
249
+ @classmethod
250
+ def _assert_correct_set_combinations(
251
+ cls,
252
+ jmux_main_type_set: Set[Type],
253
+ pydantic_main_type_set: Set[Type],
254
+ pydantic_subtype_set: Set[Type],
255
+ ):
256
+ if (
257
+ pydantic_wrong := (
258
+ len(pydantic_main_type_set) != 1 and list not in pydantic_main_type_set
259
+ )
260
+ and len(pydantic_subtype_set) > 0
261
+ ) or len(jmux_main_type_set) != 1:
262
+ wrong_obj = "pydantic" if pydantic_wrong else "JMux"
263
+ wrong_set = pydantic_main_type_set if pydantic_wrong else jmux_main_type_set
264
+ raise ForbiddenTypeHintsError(
265
+ message=(f"Forbidden typing received on {wrong_obj}: {wrong_set}"),
266
+ )
267
+
268
+ @classmethod
269
+ def _assert_only_allowed_types(
270
+ cls,
271
+ jmux_main_type_set: Set[Type],
272
+ jmux_subtype_set: Set[Type],
273
+ pydantic_main_type_set: Set[Type],
274
+ pydantic_subtype_set: Set[Type],
275
+ ) -> None:
276
+ if not all(t in (AwaitableValue, StreamableValues) for t in jmux_main_type_set):
277
+ raise ForbiddenTypeHintsError(
278
+ message=(
279
+ "JMux must have either AwaitableValue or StreamableValues as "
280
+ f"main type, got {jmux_main_type_set}."
281
+ )
282
+ )
283
+
284
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
285
+ set_a=jmux_subtype_set,
286
+ set_b={int, float, str, bool, NoneType, JMux, Enum},
287
+ ):
288
+ raise ForbiddenTypeHintsError(
289
+ message=(
290
+ "JMux sub type must be one of the emittable types, got: "
291
+ f"{jmux_subtype_set}."
292
+ )
293
+ )
294
+
295
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
296
+ set_a=pydantic_subtype_set,
297
+ set_b={int, float, str, bool, NoneType, BaseModel, Enum},
298
+ ):
299
+ raise ForbiddenTypeHintsError(
300
+ message=(
301
+ "Pydantic sub type must be one of the primitive, enum or "
302
+ f"BaseModel, got: {pydantic_subtype_set}."
303
+ )
304
+ )
305
+
306
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
307
+ set_a=pydantic_main_type_set,
308
+ set_b={int, float, str, bool, list, NoneType, BaseModel, Enum},
309
+ ):
310
+ raise ForbiddenTypeHintsError(
311
+ message=(
312
+ "Pydantic main type must be one of the primitive, enum, list "
313
+ f"or BaseModel, got {pydantic_main_type_set}."
314
+ )
315
+ )
316
+
317
+ @classmethod
318
+ def _all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
319
+ cls, set_a: Set[Type], set_b: Set[Type]
320
+ ) -> bool:
321
+ return all(any(issubclass(elem, t) for t in set_b) for elem in set_a)
322
+
254
323
  @classmethod
255
324
  def _assert_is_allowed_streamable_values(
256
325
  cls,
@@ -328,58 +397,6 @@ class JMux(ABC):
328
397
  ),
329
398
  )
330
399
 
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
400
  async def feed_chunks(self, chunks: str) -> None:
384
401
  """
385
402
  Feeds a string of characters to the JMux parser.
@@ -421,7 +438,7 @@ class JMux(ABC):
421
438
  case None:
422
439
  match self._pda.state:
423
440
  case S.START:
424
- if is_json_whitespace(ch):
441
+ if ch in JSON_WHITESPACE:
425
442
  pass
426
443
  elif ch in OBJECT_OPEN:
427
444
  self._pda.push(M.ROOT)
@@ -434,7 +451,7 @@ class JMux(ABC):
434
451
  "JSON must start with '{' character.",
435
452
  )
436
453
  case S.END:
437
- if is_json_whitespace(ch):
454
+ if ch in JSON_WHITESPACE:
438
455
  pass
439
456
  else:
440
457
  raise ObjectAlreadyClosedError(
@@ -458,7 +475,7 @@ class JMux(ABC):
458
475
  case M.ROOT:
459
476
  match self._pda.state:
460
477
  case S.EXPECT_KEY:
461
- if is_json_whitespace(ch):
478
+ if ch in JSON_WHITESPACE:
462
479
  pass
463
480
  elif ch == '"':
464
481
  self._pda.set_state(S.PARSING_KEY)
@@ -485,7 +502,7 @@ class JMux(ABC):
485
502
  self._decoder.push(ch)
486
503
 
487
504
  case S.EXPECT_COLON:
488
- if is_json_whitespace(ch):
505
+ if ch in JSON_WHITESPACE:
489
506
  pass
490
507
  elif ch in COLON:
491
508
  self._pda.set_state(S.EXPECT_VALUE)
@@ -498,7 +515,7 @@ class JMux(ABC):
498
515
  )
499
516
 
500
517
  case S.EXPECT_VALUE:
501
- if is_json_whitespace(ch):
518
+ if ch in JSON_WHITESPACE:
502
519
  pass
503
520
  elif res := await self._handle_common__expect_value(ch):
504
521
  if (
@@ -550,20 +567,23 @@ class JMux(ABC):
550
567
  ):
551
568
  await self._sink.emit(maybe_char)
552
569
 
553
- case _ if self._pda.state in PRIMITIVE_STATES:
554
- if ch not in COMMA | OBJECT_CLOSE:
555
- self._assert_primitive_character_allowed_in_state(ch)
556
- self._decoder.push(ch)
557
- else:
570
+ case _ if self._pda.state in PARSING_PRIMITIVE_STATES:
571
+ if ch in COMMA | OBJECT_CLOSE | JSON_WHITESPACE:
558
572
  await self._parse_primitive()
559
573
  await self._sink.close()
560
574
  self._decoder.reset()
561
- self._pda.set_state(S.EXPECT_KEY)
575
+ if ch in JSON_WHITESPACE:
576
+ self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
577
+ else:
578
+ self._pda.set_state(S.EXPECT_KEY)
562
579
  if ch in OBJECT_CLOSE:
563
580
  await self._finalize()
581
+ else:
582
+ self._assert_primitive_character_allowed_in_state(ch)
583
+ self._decoder.push(ch)
564
584
 
565
585
  case S.EXPECT_COMMA_OR_EOC:
566
- if is_json_whitespace(ch):
586
+ if ch in JSON_WHITESPACE:
567
587
  pass
568
588
  elif ch in COMMA:
569
589
  self._pda.set_state(S.EXPECT_KEY)
@@ -596,7 +616,7 @@ class JMux(ABC):
596
616
 
597
617
  match self._pda.state:
598
618
  case S.EXPECT_VALUE:
599
- if is_json_whitespace(ch):
619
+ if ch in JSON_WHITESPACE:
600
620
  pass
601
621
  elif await self._handle_common__expect_value(ch):
602
622
  pass
@@ -638,20 +658,20 @@ class JMux(ABC):
638
658
  else:
639
659
  self._decoder.push(ch)
640
660
 
641
- case _ if self._pda.state in PRIMITIVE_STATES:
642
- if ch not in COMMA | ARRAY_CLOSE:
643
- self._assert_primitive_character_allowed_in_state(ch)
644
- self._decoder.push(ch)
645
- else:
661
+ case _ if self._pda.state in PARSING_PRIMITIVE_STATES:
662
+ if ch in COMMA | ARRAY_CLOSE | JSON_WHITESPACE:
646
663
  await self._parse_primitive()
647
664
  self._decoder.reset()
648
665
  if ch in COMMA:
649
666
  self._pda.set_state(S.EXPECT_VALUE)
650
667
  elif ch in ARRAY_CLOSE:
651
668
  await self._close_context(S.EXPECT_COMMA_OR_EOC)
669
+ else:
670
+ self._assert_primitive_character_allowed_in_state(ch)
671
+ self._decoder.push(ch)
652
672
 
653
673
  case S.EXPECT_COMMA_OR_EOC:
654
- if is_json_whitespace(ch):
674
+ if ch in JSON_WHITESPACE:
655
675
  pass
656
676
  elif ch in COMMA:
657
677
  self._pda.set_state(S.EXPECT_VALUE)
@@ -681,8 +701,16 @@ class JMux(ABC):
681
701
  self._pda.state,
682
702
  "State in object context must be 'parsing_object'",
683
703
  )
684
- if ch in OBJECT_CLOSE:
704
+ if ch in OBJECT_OPEN:
705
+ if self._pda.top is M.OBJECT:
706
+ await self._sink.forward_char(ch)
707
+ self._pda.push(M.OBJECT)
708
+ elif ch in OBJECT_CLOSE:
685
709
  self._pda.pop()
710
+ if self._pda.top is M.OBJECT:
711
+ await self._sink.forward_char(ch)
712
+ return
713
+
686
714
  if self._pda.top is M.ROOT:
687
715
  await self._sink.close()
688
716
  self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
@@ -699,7 +727,8 @@ class JMux(ABC):
699
727
  )
700
728
  await self._sink.emit(None)
701
729
  elif self._pda.state is S.PARSING_BOOLEAN:
702
- await self._sink.emit(self._decoder.buffer == JSON_TRUE)
730
+ bool_value = str_to_bool(self._decoder.buffer)
731
+ await self._sink.emit(bool_value)
703
732
  else:
704
733
  try:
705
734
  buffer = self._decoder.buffer
jmux/helpers.py CHANGED
@@ -1,17 +1,37 @@
1
- from types import NoneType, UnionType
2
- from typing import Set, Tuple, Type, Union, get_args, get_origin
1
+ from types import NoneType
2
+ from typing import Set, Tuple, Type, get_args, get_origin
3
3
 
4
+ from jmux.error import ParsePrimitiveError
5
+ from jmux.types import TYPES_LIKE_NONE, TYPES_LIKE_UNION
4
6
 
5
- def is_json_whitespace(ch: str) -> bool:
6
- return ch in {" ", "\t", "\n", "\r"}
7
+
8
+ def str_to_bool(s: str) -> bool:
9
+ if s == "true":
10
+ return True
11
+ elif s == "false":
12
+ return False
13
+ else:
14
+ raise ParsePrimitiveError(
15
+ f"Cannot convert string '{s}' to boolean. Expected 'true' or 'false', got"
16
+ f" '{s}'."
17
+ )
7
18
 
8
19
 
9
20
  def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[Type]]:
10
21
  Origin: Type | None = get_origin(UnknownType)
11
22
  if Origin is None:
12
23
  return {UnknownType}, set()
13
- if Origin is UnionType or Origin is Union:
14
- return deconstruct_type(UnknownType), set()
24
+ if Origin in TYPES_LIKE_UNION:
25
+ deconstructed = deconstruct_flat_type(UnknownType)
26
+ maybe_list_types = [
27
+ subtypes for subtypes in deconstructed if get_origin(subtypes) is list
28
+ ]
29
+ if len(maybe_list_types) == 1:
30
+ list_based_type = maybe_list_types[0]
31
+ non_list_types = deconstructed - {list_based_type}
32
+ main_type, subtype = extract_types_from_generic_alias(list_based_type)
33
+ return non_list_types | main_type, subtype
34
+ return deconstructed, set()
15
35
 
16
36
  type_args = get_args(UnknownType)
17
37
  if len(type_args) != 1:
@@ -21,7 +41,7 @@ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[
21
41
  )
22
42
 
23
43
  Generic: Type = type_args[0]
24
- type_set = deconstruct_type(Generic)
44
+ type_set = deconstruct_flat_type(Generic)
25
45
  if len(type_set) == 1:
26
46
  return {Origin}, type_set
27
47
  if len(type_set) != 2:
@@ -36,16 +56,20 @@ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[
36
56
  return {Origin}, type_set
37
57
 
38
58
 
39
- def deconstruct_type(UnknownType: Type) -> Set[Type]:
59
+ def deconstruct_flat_type(UnknownType: Type) -> Set[Type]:
40
60
  Origin: Type | None = get_origin(UnknownType)
41
- if UnknownType is None:
61
+ if UnknownType in TYPES_LIKE_NONE:
42
62
  return {NoneType}
43
63
  if Origin is None:
44
64
  return {UnknownType}
45
- if not (Origin is UnionType or Origin is Union):
46
- return {Origin}
47
- type_args = get_args(UnknownType)
48
- return set(type_args)
65
+ if Origin in TYPES_LIKE_UNION:
66
+ type_args = get_args(UnknownType)
67
+ return set(type_args)
68
+ raise TypeError(
69
+ f"Unknown type {UnknownType} is not a Union or optional type, "
70
+ "only only those types and their syntactic sugar are supported "
71
+ "for flat deconstruction."
72
+ )
49
73
 
50
74
 
51
75
  def get_main_type(type_set: Set[Type]) -> Type:
jmux/types.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from enum import Enum
2
- from typing import Set
2
+ from types import NoneType, UnionType
3
+ from typing import List, Set, Union
3
4
 
4
5
 
5
6
  class State(Enum):
@@ -21,7 +22,7 @@ class State(Enum):
21
22
  PARSING_OBJECT = "parsing_object"
22
23
 
23
24
 
24
- PRIMITIVE_STATES: Set[State] = {
25
+ PARSING_PRIMITIVE_STATES: Set[State] = {
25
26
  State.PARSING_INTEGER,
26
27
  State.PARSING_FLOAT,
27
28
  State.PARSING_BOOLEAN,
@@ -55,3 +56,8 @@ NULL_ALLOWED = set("nul")
55
56
  JSON_FALSE = "false"
56
57
  JSON_TRUE = "true"
57
58
  JSON_NULL = "null"
59
+ JSON_WHITESPACE = set(" \t\n\r")
60
+
61
+ TYPES_LIKE_UNION = {UnionType, Union}
62
+ TYPES_LIKE_NONE = {NoneType, None}
63
+ TYPES_LIKE_LIST = {List, list}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmux
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: JMux: A Python package for demultiplexing a JSON string into multiple awaitable variables.
5
5
  Author-email: "Johannes A.I. Unruh" <johannes@unruh.ai>
6
6
  License: MIT License
@@ -0,0 +1,13 @@
1
+ jmux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ jmux/awaitable.py,sha256=gceBygIf3fAIWLsN1lWxsz9ExWNasDuk1WaGz8d9FAc,8427
3
+ jmux/decoder.py,sha256=Y6KVryRDLvGV5nBsneXpTvC0WUGhR5Z89Dvqz4HMAgg,1562
4
+ jmux/demux.py,sha256=g9DkMc9sLs31nuVwb7jbGZAjs0KulsKDGfOIM87NuWQ,36317
5
+ jmux/error.py,sha256=VZJYivt8RPfjcF2bs-T7_UkH3dVA3xH-xGbZggQV14k,4665
6
+ jmux/helpers.py,sha256=zOlw1Yk7-sdKAeasswRRcuUOTEBAUbymoAGwBTMaOjg,2902
7
+ jmux/pda.py,sha256=81gnh0eWGsgd_SrHkqjRQy_KkOSlBf5nor7pqKGgYjw,791
8
+ jmux/types.py,sha256=CJhFS9RVgR0cDBNJR8ROAFnxzG4YTYpNZ90hyD2SxsY,1389
9
+ jmux-0.0.4.dist-info/licenses/LICENSE,sha256=y0qnwaAe4bEqzNPyq4M_VZA2I2mQly8MawajyZhqw0k,1169
10
+ jmux-0.0.4.dist-info/METADATA,sha256=vVREVDZvfTC0KadGCOrw7RrSGxXdmZZNHJ8K-Zr3ZLM,13330
11
+ jmux-0.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ jmux-0.0.4.dist-info/top_level.txt,sha256=TF2N6kHqLghfOkCiNlCueMDX4l5rPn_5MSPNtYrS1-o,5
13
+ jmux-0.0.4.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- jmux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- jmux/awaitable.py,sha256=gceBygIf3fAIWLsN1lWxsz9ExWNasDuk1WaGz8d9FAc,8427
3
- jmux/decoder.py,sha256=Y6KVryRDLvGV5nBsneXpTvC0WUGhR5Z89Dvqz4HMAgg,1562
4
- jmux/demux.py,sha256=7YJBD_EGUyxjiwjVnKCZOA-NrpXWrTe_y2Eitl4EU3E,35089
5
- jmux/error.py,sha256=VZJYivt8RPfjcF2bs-T7_UkH3dVA3xH-xGbZggQV14k,4665
6
- jmux/helpers.py,sha256=6y33RohUGVvGGaAREyRQTmEWfgV4w295SyqDwIMnUNs,1982
7
- jmux/pda.py,sha256=81gnh0eWGsgd_SrHkqjRQy_KkOSlBf5nor7pqKGgYjw,791
8
- jmux/types.py,sha256=V63wMx1I7l_P83JAqzOQ7H7B-xKrNUuGshJ3NjKsdeQ,1192
9
- jmux-0.0.2.dist-info/licenses/LICENSE,sha256=y0qnwaAe4bEqzNPyq4M_VZA2I2mQly8MawajyZhqw0k,1169
10
- jmux-0.0.2.dist-info/METADATA,sha256=G3PPpw0VhSYx9YyeVUT9T12YCGxORI7HVFAQYTB4Od4,13330
11
- jmux-0.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- jmux-0.0.2.dist-info/top_level.txt,sha256=TF2N6kHqLghfOkCiNlCueMDX4l5rPn_5MSPNtYrS1-o,5
13
- jmux-0.0.2.dist-info/RECORD,,
File without changes