jmux 0.0.4__py3-none-any.whl → 0.0.6__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/awaitable.py CHANGED
@@ -1,11 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  from asyncio import Event, Queue
2
4
  from enum import Enum
3
5
  from types import NoneType
4
6
  from typing import (
5
7
  AsyncGenerator,
8
+ Generic,
6
9
  Protocol,
7
10
  Set,
8
11
  Type,
12
+ TypeVar,
9
13
  cast,
10
14
  runtime_checkable,
11
15
  )
@@ -13,13 +17,15 @@ from typing import (
13
17
  from jmux.error import NothingEmittedError, SinkClosedError
14
18
  from jmux.helpers import extract_types_from_generic_alias
15
19
 
20
+ T = TypeVar("T")
21
+
16
22
 
17
23
  class SinkType(Enum):
18
24
  STREAMABLE_VALUES = "StreamableValues"
19
25
  AWAITABLE_VALUE = "AwaitableValue"
20
26
 
21
27
 
22
- class UnderlyingGenericMixin[T]:
28
+ class UnderlyingGenericMixin(Generic[T]):
23
29
  """
24
30
  A mixin class that provides methods for inspecting the generic types of a
25
31
  class at runtime.
@@ -61,7 +67,7 @@ class UnderlyingGenericMixin[T]:
61
67
 
62
68
 
63
69
  @runtime_checkable
64
- class IAsyncSink[T](Protocol):
70
+ class IAsyncSink(Protocol[T]):
65
71
  """
66
72
  An asynchronous sink protocol that defines a common interface for putting, closing,
67
73
  and retrieving values from a sink.
@@ -96,7 +102,7 @@ class IAsyncSink[T](Protocol):
96
102
  ...
97
103
 
98
104
 
99
- class StreamableValues[T](UnderlyingGenericMixin[T]):
105
+ class StreamableValues(UnderlyingGenericMixin[T], Generic[T]):
100
106
  """
101
107
  A class that represents a stream of values that can be asynchronously iterated over.
102
108
  It uses an asyncio.Queue to store the items and allows for putting items into the
@@ -198,7 +204,7 @@ class StreamableValues[T](UnderlyingGenericMixin[T]):
198
204
  yield item
199
205
 
200
206
 
201
- class AwaitableValue[T](UnderlyingGenericMixin[T]):
207
+ class AwaitableValue(UnderlyingGenericMixin[T], Generic[T]):
202
208
  """
203
209
  A class that represents a value that will be available in the future.
204
210
  It can be awaited to get the value, and it can only be set once.
jmux/decoder.py CHANGED
@@ -32,26 +32,40 @@ class StringEscapeDecoder:
32
32
  def __init__(self):
33
33
  self._buffer = ""
34
34
  self._string_escape = False
35
+ self._is_parsing_unicode = False
36
+ self._unicode_buffer = ""
35
37
 
36
38
  def push(self, ch: str) -> str | None:
39
+ if self._is_parsing_unicode:
40
+ self._unicode_buffer += ch
41
+ if len(self._unicode_buffer) == 4:
42
+ code_point = int(self._unicode_buffer, 16)
43
+ decoded_char = chr(code_point)
44
+ self._buffer += decoded_char
45
+ self._is_parsing_unicode = False
46
+ self._unicode_buffer = ""
47
+ return decoded_char
48
+ return None
49
+
37
50
  if self._string_escape:
38
51
  self._string_escape = False
39
52
  if ch == "u":
40
- self.is_parsing_unicode = True
41
- self.unicode_buffer = ""
42
- return
53
+ self._is_parsing_unicode = True
54
+ self._unicode_buffer = ""
55
+ return None
43
56
  escaped_char = self.escape_map.get(ch, ch)
44
57
  self._buffer += escaped_char
45
58
  return escaped_char
46
59
 
47
60
  if ch == "\\":
48
61
  self._string_escape = True
62
+ return None
49
63
  else:
50
64
  self._buffer += ch
51
65
  return ch
52
66
 
53
67
  def is_terminating_quote(self, ch: str) -> bool:
54
- if self._string_escape:
68
+ if self._string_escape or self._is_parsing_unicode:
55
69
  return False
56
70
  if ch == '"':
57
71
  return True
@@ -60,6 +74,8 @@ class StringEscapeDecoder:
60
74
  def reset(self) -> None:
61
75
  self._buffer = ""
62
76
  self._string_escape = False
77
+ self._is_parsing_unicode = False
78
+ self._unicode_buffer = ""
63
79
 
64
80
  @property
65
81
  def buffer(self) -> str:
jmux/demux.py CHANGED
@@ -1,10 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABC
2
4
  from enum import Enum
3
5
  from types import NoneType
4
6
  from typing import (
7
+ Generic,
5
8
  Optional,
6
9
  Set,
7
10
  Type,
11
+ TypeVar,
12
+ Union,
8
13
  get_args,
9
14
  get_origin,
10
15
  get_type_hints,
@@ -42,6 +47,8 @@ from jmux.types import (
42
47
  BOOLEAN_OPEN,
43
48
  COLON,
44
49
  COMMA,
50
+ EXPECT_KEY_IN_ROOT,
51
+ EXPECT_VALUE_IN_ARRAY,
45
52
  FLOAT_ALLOWED,
46
53
  INTERGER_ALLOWED,
47
54
  JSON_FALSE,
@@ -59,11 +66,13 @@ from jmux.types import (
59
66
  from jmux.types import Mode as M
60
67
  from jmux.types import State as S
61
68
 
62
- type Primitive = int | float | str | bool | None
63
- type Emittable = Primitive | "JMux" | Enum
69
+ Primitive = Union[int, float, str, bool, None]
70
+ Emittable = Union[int, float, str, bool, None, "JMux", Enum]
71
+
72
+ T = TypeVar("T")
64
73
 
65
74
 
66
- class Sink[T: Emittable]:
75
+ class Sink(Generic[T]):
67
76
  def __init__(self, delegate: "JMux"):
68
77
  self._current_key: Optional[str] = None
69
78
  self._current_sink: Optional[IAsyncSink[T]] = None
@@ -192,60 +201,74 @@ class JMux(ABC):
192
201
  ForbiddenTypeHintsError: If a type hint is not allowed.
193
202
  ObjectMissmatchedError: If the JMux class does not match the Pydantic model.
194
203
  """
195
- for attr_name, type_alias in get_type_hints(cls).items():
196
- jmux_main_type_set, jmux_subtype_set = extract_types_from_generic_alias(
197
- type_alias
198
- )
199
-
200
- MaybePydanticType = get_type_hints(pydantic_model).get(attr_name, None)
201
- if MaybePydanticType is None:
202
- if NoneType in jmux_subtype_set:
203
- continue
204
- else:
205
- raise MissingAttributeError(
206
- object_name=pydantic_model.__name__,
207
- attribute=attr_name,
208
- )
204
+ try:
205
+ for attr_name, type_alias in get_type_hints(cls).items():
206
+ jmux_main_type_set, jmux_subtype_set = extract_types_from_generic_alias(
207
+ type_alias
208
+ )
209
209
 
210
- pydantic_main_type_set, pydantic_subtype_set = (
211
- extract_types_from_generic_alias(MaybePydanticType)
212
- )
213
- cls._assert_only_allowed_types(
214
- jmux_main_type_set,
215
- jmux_subtype_set,
216
- pydantic_main_type_set,
217
- pydantic_subtype_set,
218
- )
219
- cls._assert_correct_set_combinations(
220
- jmux_main_type_set,
221
- pydantic_main_type_set,
222
- pydantic_subtype_set,
223
- )
210
+ MaybePydanticType = get_type_hints(pydantic_model).get(attr_name, None)
211
+ if MaybePydanticType is None:
212
+ if NoneType in jmux_subtype_set:
213
+ continue
214
+ else:
215
+ raise MissingAttributeError(
216
+ object_name=pydantic_model.__name__,
217
+ attribute=attr_name,
218
+ )
224
219
 
225
- if StreamableValues in jmux_main_type_set:
226
- cls._assert_is_allowed_streamable_values(
227
- jmux_subtype_set,
228
- pydantic_subtype_set,
229
- pydantic_main_type_set,
230
- pydantic_model,
231
- attr_name,
220
+ pydantic_main_type_set, pydantic_subtype_set = (
221
+ extract_types_from_generic_alias(MaybePydanticType)
232
222
  )
233
- elif AwaitableValue in jmux_main_type_set:
234
- cls._assert_is_allowed_awaitable_value(
223
+ cls._assert_only_allowed_types(
224
+ jmux_main_type_set,
235
225
  jmux_subtype_set,
236
- pydantic_subtype_set,
237
226
  pydantic_main_type_set,
238
- pydantic_model,
239
- attr_name,
227
+ pydantic_subtype_set,
240
228
  )
241
- else:
242
- raise ObjectMissmatchedError(
243
- jmux_model=cls.__name__,
244
- pydantic_model=pydantic_model.__name__,
245
- attribute=attr_name,
246
- message="Unexpected main type on JMux",
229
+ cls._assert_correct_set_combinations(
230
+ jmux_main_type_set,
231
+ pydantic_main_type_set,
232
+ pydantic_subtype_set,
247
233
  )
248
234
 
235
+ if StreamableValues in jmux_main_type_set:
236
+ cls._assert_is_allowed_streamable_values(
237
+ jmux_subtype_set,
238
+ pydantic_subtype_set,
239
+ pydantic_main_type_set,
240
+ pydantic_model,
241
+ attr_name,
242
+ )
243
+ elif AwaitableValue in jmux_main_type_set:
244
+ cls._assert_is_allowed_awaitable_value(
245
+ jmux_subtype_set,
246
+ pydantic_subtype_set,
247
+ pydantic_main_type_set,
248
+ pydantic_model,
249
+ attr_name,
250
+ )
251
+ else:
252
+ raise ObjectMissmatchedError(
253
+ jmux_model=cls.__name__,
254
+ pydantic_model=pydantic_model.__name__,
255
+ attribute=attr_name,
256
+ message="Unexpected main type on JMux",
257
+ )
258
+ except TypeError as e:
259
+ raise ForbiddenTypeHintsError(message=f"Unexpected type hint: {e}") from e
260
+ except MissingAttributeError as e:
261
+ raise e
262
+ except ObjectMissmatchedError as e:
263
+ raise e
264
+ except Exception as e:
265
+ raise ObjectMissmatchedError(
266
+ jmux_model=cls.__name__,
267
+ pydantic_model=pydantic_model.__name__,
268
+ attribute=attr_name,
269
+ message=f"Unexpected main type on JMux: {e}",
270
+ )
271
+
249
272
  @classmethod
250
273
  def _assert_correct_set_combinations(
251
274
  cls,
@@ -474,18 +497,27 @@ class JMux(ABC):
474
497
  # CONTEXT: Root
475
498
  case M.ROOT:
476
499
  match self._pda.state:
477
- case S.EXPECT_KEY:
500
+ case _ if self._pda.state in EXPECT_KEY_IN_ROOT:
478
501
  if ch in JSON_WHITESPACE:
479
502
  pass
480
503
  elif ch == '"':
481
504
  self._pda.set_state(S.PARSING_KEY)
482
505
  self._decoder.reset()
506
+ elif ch in OBJECT_CLOSE:
507
+ if self._pda.state is S.EXPECT_KEY_AFTER_COMMA:
508
+ raise UnexpectedCharacterError(
509
+ ch,
510
+ self._pda.stack,
511
+ self._pda.state,
512
+ "Trailing comma in object is not allowed.",
513
+ )
514
+ await self._finalize()
483
515
  else:
484
516
  raise UnexpectedCharacterError(
485
517
  ch,
486
518
  self._pda.stack,
487
519
  self._pda.state,
488
- "Char needs to be '\"' or JSON whitespaces",
520
+ "Char needs to be '\"', '}' or JSON whitespaces",
489
521
  )
490
522
 
491
523
  case S.PARSING_KEY:
@@ -530,6 +562,14 @@ class JMux(ABC):
530
562
  "Expected '[' or '\"' for 'StreamableValues'",
531
563
  )
532
564
  elif ch in ARRAY_OPEN:
565
+ if self._sink.current_sink_type == SinkType.AWAITABLE_VALUE:
566
+ raise UnexpectedCharacterError(
567
+ ch,
568
+ self._pda.stack,
569
+ self._pda.state,
570
+ "Trying to parse 'array' but sink type is "
571
+ "'AwaitableValue'.",
572
+ )
533
573
  self._pda.set_state(S.EXPECT_VALUE)
534
574
  self._pda.push(M.ARRAY)
535
575
  else:
@@ -572,12 +612,12 @@ class JMux(ABC):
572
612
  await self._parse_primitive()
573
613
  await self._sink.close()
574
614
  self._decoder.reset()
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)
579
- if ch in OBJECT_CLOSE:
615
+ if ch in COMMA:
616
+ self._pda.set_state(S.EXPECT_KEY_AFTER_COMMA)
617
+ elif ch in OBJECT_CLOSE:
580
618
  await self._finalize()
619
+ else:
620
+ self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
581
621
  else:
582
622
  self._assert_primitive_character_allowed_in_state(ch)
583
623
  self._decoder.push(ch)
@@ -586,7 +626,7 @@ class JMux(ABC):
586
626
  if ch in JSON_WHITESPACE:
587
627
  pass
588
628
  elif ch in COMMA:
589
- self._pda.set_state(S.EXPECT_KEY)
629
+ self._pda.set_state(S.EXPECT_KEY_AFTER_COMMA)
590
630
  elif ch in OBJECT_CLOSE:
591
631
  await self._finalize()
592
632
  else:
@@ -615,12 +655,19 @@ class JMux(ABC):
615
655
  )
616
656
 
617
657
  match self._pda.state:
618
- case S.EXPECT_VALUE:
658
+ case _ if self._pda.state in EXPECT_VALUE_IN_ARRAY:
619
659
  if ch in JSON_WHITESPACE:
620
660
  pass
621
661
  elif await self._handle_common__expect_value(ch):
622
662
  pass
623
663
  elif ch in ARRAY_CLOSE:
664
+ if self._pda.state is S.EXPECT_VALUE_AFTER_COMMA:
665
+ raise UnexpectedCharacterError(
666
+ ch,
667
+ self._pda.stack,
668
+ self._pda.state,
669
+ "Trailing comma in array is not allowed.",
670
+ )
624
671
  await self._close_context(S.EXPECT_COMMA_OR_EOC)
625
672
  else:
626
673
  raise UnexpectedCharacterError(
@@ -663,9 +710,11 @@ class JMux(ABC):
663
710
  await self._parse_primitive()
664
711
  self._decoder.reset()
665
712
  if ch in COMMA:
666
- self._pda.set_state(S.EXPECT_VALUE)
713
+ self._pda.set_state(S.EXPECT_VALUE_AFTER_COMMA)
667
714
  elif ch in ARRAY_CLOSE:
668
715
  await self._close_context(S.EXPECT_COMMA_OR_EOC)
716
+ else:
717
+ self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
669
718
  else:
670
719
  self._assert_primitive_character_allowed_in_state(ch)
671
720
  self._decoder.push(ch)
@@ -674,7 +723,7 @@ class JMux(ABC):
674
723
  if ch in JSON_WHITESPACE:
675
724
  pass
676
725
  elif ch in COMMA:
677
- self._pda.set_state(S.EXPECT_VALUE)
726
+ self._pda.set_state(S.EXPECT_VALUE_AFTER_COMMA)
678
727
  elif ch in ARRAY_CLOSE:
679
728
  await self._close_context(S.EXPECT_COMMA_OR_EOC)
680
729
  else:
jmux/pda.py CHANGED
@@ -1,7 +1,12 @@
1
- from typing import List
1
+ from __future__ import annotations
2
2
 
3
+ from typing import Generic, List, Optional, TypeVar
3
4
 
4
- class PushDownAutomata[Context, State]:
5
+ Context = TypeVar("Context")
6
+ State = TypeVar("State")
7
+
8
+
9
+ class PushDownAutomata(Generic[Context, State]):
5
10
  def __init__(self, start_state: State) -> None:
6
11
  self._stack: List[Context] = []
7
12
  self._state: State = start_state
@@ -15,7 +20,7 @@ class PushDownAutomata[Context, State]:
15
20
  return self._stack
16
21
 
17
22
  @property
18
- def top(self) -> Context | None:
23
+ def top(self) -> Optional[Context]:
19
24
  if not self._stack:
20
25
  return None
21
26
  return self._stack[-1]
jmux/types.py CHANGED
@@ -9,8 +9,10 @@ class State(Enum):
9
9
  ERROR = "error"
10
10
  # expect
11
11
  EXPECT_KEY = "expect_key"
12
+ EXPECT_KEY_AFTER_COMMA = "expect_key_after_comma"
12
13
  EXPECT_COLON = "expect_colon"
13
14
  EXPECT_VALUE = "expect_value"
15
+ EXPECT_VALUE_AFTER_COMMA = "expect_value_after_comma"
14
16
  EXPECT_COMMA_OR_EOC = "expect_comma_or_eoc"
15
17
  # parsing
16
18
  PARSING_KEY = "parsing_key"
@@ -29,6 +31,9 @@ PARSING_PRIMITIVE_STATES: Set[State] = {
29
31
  State.PARSING_NULL,
30
32
  }
31
33
 
34
+ EXPECT_KEY_IN_ROOT = {State.EXPECT_KEY, State.EXPECT_KEY_AFTER_COMMA}
35
+ EXPECT_VALUE_IN_ARRAY = {State.EXPECT_VALUE, State.EXPECT_VALUE_AFTER_COMMA}
36
+
32
37
 
33
38
  class Mode(Enum):
34
39
  ROOT = "$"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jmux
3
- Version: 0.0.4
3
+ Version: 0.0.6
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
@@ -32,7 +32,7 @@ Project-URL: Repository, https://github.com/jaunruh/jmux
32
32
  Keywords: demultiplexer,python,package,json
33
33
  Classifier: Programming Language :: Python :: 3
34
34
  Classifier: Operating System :: OS Independent
35
- Requires-Python: >=3.12
35
+ Requires-Python: >=3.10
36
36
  Description-Content-Type: text/markdown
37
37
  License-File: LICENSE
38
38
  Requires-Dist: anyio>=4.0.0
@@ -0,0 +1,13 @@
1
+ jmux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ jmux/awaitable.py,sha256=otX4vbWzVmIwxvMDWOtaIvS9do8YJtbls9xFjwH9Yw0,8534
3
+ jmux/decoder.py,sha256=YoCkBKposMx1PA78BIVawgFw96QsqTZaOW4gNCSkI7I,2197
4
+ jmux/demux.py,sha256=R2vazLOtfh4zvQKoj3_92RgdqG-zaG3CkQMBz72fAIE,38800
5
+ jmux/error.py,sha256=VZJYivt8RPfjcF2bs-T7_UkH3dVA3xH-xGbZggQV14k,4665
6
+ jmux/helpers.py,sha256=zOlw1Yk7-sdKAeasswRRcuUOTEBAUbymoAGwBTMaOjg,2902
7
+ jmux/pda.py,sha256=19joQd0DD5OAmwRpp2jVVbtiFXnjv5P_1mZm87-QOeY,922
8
+ jmux/types.py,sha256=a5Xyx_8A3gsZkscII1_7E6oyu4VQ-oI4_osTnPLi_xs,1649
9
+ jmux-0.0.6.dist-info/licenses/LICENSE,sha256=y0qnwaAe4bEqzNPyq4M_VZA2I2mQly8MawajyZhqw0k,1169
10
+ jmux-0.0.6.dist-info/METADATA,sha256=cJJfsEjvnpvffxjEgdwbsTQFxB8uqIArJApCYRjTETo,13330
11
+ jmux-0.0.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
+ jmux-0.0.6.dist-info/top_level.txt,sha256=TF2N6kHqLghfOkCiNlCueMDX4l5rPn_5MSPNtYrS1-o,5
13
+ jmux-0.0.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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=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,,