jmux 0.0.3__py3-none-any.whl → 0.0.5__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/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,
@@ -59,11 +64,13 @@ from jmux.types import (
59
64
  from jmux.types import Mode as M
60
65
  from jmux.types import State as S
61
66
 
62
- type Primitive = int | float | str | bool | None
63
- type Emittable = Primitive | "JMux" | Enum
67
+ Primitive = Union[int, float, str, bool, None]
68
+ Emittable = Union[int, float, str, bool, None, "JMux", Enum]
69
+
70
+ T = TypeVar("T")
64
71
 
65
72
 
66
- class Sink[T: Emittable]:
73
+ class Sink(Generic[T]):
67
74
  def __init__(self, delegate: "JMux"):
68
75
  self._current_key: Optional[str] = None
69
76
  self._current_sink: Optional[IAsyncSink[T]] = None
@@ -216,17 +223,11 @@ class JMux(ABC):
216
223
  pydantic_main_type_set,
217
224
  pydantic_subtype_set,
218
225
  )
219
- if (
220
- pydantic_wrong := len(pydantic_main_type_set) != 1
221
- and len(pydantic_subtype_set) > 0
222
- ) or len(jmux_main_type_set) != 1:
223
- wrong_obj = "pydantic" if pydantic_wrong else "JMux"
224
- wrong_set = (
225
- jmux_main_type_set if pydantic_wrong else pydantic_main_type_set
226
- )
227
- raise ForbiddenTypeHintsError(
228
- message=(f"Forbidden typing received on {wrong_obj}: {wrong_set}"),
229
- )
226
+ cls._assert_correct_set_combinations(
227
+ jmux_main_type_set,
228
+ pydantic_main_type_set,
229
+ pydantic_subtype_set,
230
+ )
230
231
 
231
232
  if StreamableValues in jmux_main_type_set:
232
233
  cls._assert_is_allowed_streamable_values(
@@ -252,6 +253,80 @@ class JMux(ABC):
252
253
  message="Unexpected main type on JMux",
253
254
  )
254
255
 
256
+ @classmethod
257
+ def _assert_correct_set_combinations(
258
+ cls,
259
+ jmux_main_type_set: Set[Type],
260
+ pydantic_main_type_set: Set[Type],
261
+ pydantic_subtype_set: Set[Type],
262
+ ):
263
+ if (
264
+ pydantic_wrong := (
265
+ len(pydantic_main_type_set) != 1 and list not in pydantic_main_type_set
266
+ )
267
+ and len(pydantic_subtype_set) > 0
268
+ ) or len(jmux_main_type_set) != 1:
269
+ wrong_obj = "pydantic" if pydantic_wrong else "JMux"
270
+ wrong_set = pydantic_main_type_set if pydantic_wrong else jmux_main_type_set
271
+ raise ForbiddenTypeHintsError(
272
+ message=(f"Forbidden typing received on {wrong_obj}: {wrong_set}"),
273
+ )
274
+
275
+ @classmethod
276
+ def _assert_only_allowed_types(
277
+ cls,
278
+ jmux_main_type_set: Set[Type],
279
+ jmux_subtype_set: Set[Type],
280
+ pydantic_main_type_set: Set[Type],
281
+ pydantic_subtype_set: Set[Type],
282
+ ) -> None:
283
+ if not all(t in (AwaitableValue, StreamableValues) for t in jmux_main_type_set):
284
+ raise ForbiddenTypeHintsError(
285
+ message=(
286
+ "JMux must have either AwaitableValue or StreamableValues as "
287
+ f"main type, got {jmux_main_type_set}."
288
+ )
289
+ )
290
+
291
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
292
+ set_a=jmux_subtype_set,
293
+ set_b={int, float, str, bool, NoneType, JMux, Enum},
294
+ ):
295
+ raise ForbiddenTypeHintsError(
296
+ message=(
297
+ "JMux sub type must be one of the emittable types, got: "
298
+ f"{jmux_subtype_set}."
299
+ )
300
+ )
301
+
302
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
303
+ set_a=pydantic_subtype_set,
304
+ set_b={int, float, str, bool, NoneType, BaseModel, Enum},
305
+ ):
306
+ raise ForbiddenTypeHintsError(
307
+ message=(
308
+ "Pydantic sub type must be one of the primitive, enum or "
309
+ f"BaseModel, got: {pydantic_subtype_set}."
310
+ )
311
+ )
312
+
313
+ if not cls._all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
314
+ set_a=pydantic_main_type_set,
315
+ set_b={int, float, str, bool, list, NoneType, BaseModel, Enum},
316
+ ):
317
+ raise ForbiddenTypeHintsError(
318
+ message=(
319
+ "Pydantic main type must be one of the primitive, enum, list "
320
+ f"or BaseModel, got {pydantic_main_type_set}."
321
+ )
322
+ )
323
+
324
+ @classmethod
325
+ def _all_elements_in_set_a_are_subclass_of_an_element_in_set_b(
326
+ cls, set_a: Set[Type], set_b: Set[Type]
327
+ ) -> bool:
328
+ return all(any(issubclass(elem, t) for t in set_b) for elem in set_a)
329
+
255
330
  @classmethod
256
331
  def _assert_is_allowed_streamable_values(
257
332
  cls,
@@ -329,58 +404,6 @@ class JMux(ABC):
329
404
  ),
330
405
  )
331
406
 
332
- @classmethod
333
- def _assert_only_allowed_types(
334
- cls,
335
- jmux_main_type_set: Set[Type],
336
- jmux_subtype_set: Set[Type],
337
- pydantic_main_type_set: Set[Type],
338
- pydantic_subtype_set: Set[Type],
339
- ) -> None:
340
- if not all(t in (AwaitableValue, StreamableValues) for t in jmux_main_type_set):
341
- raise ForbiddenTypeHintsError(
342
- message=(
343
- "JMux must have either AwaitableValue or StreamableValues as "
344
- f"main type, got {jmux_main_type_set}."
345
- )
346
- )
347
-
348
- if not any(
349
- issubclass(elem, t)
350
- for t in (int, float, str, bool, NoneType, JMux, Enum)
351
- for elem in jmux_subtype_set
352
- ):
353
- raise ForbiddenTypeHintsError(
354
- message=(
355
- "JMux sub type must be one of the emittable types: "
356
- f"{jmux_subtype_set}."
357
- )
358
- )
359
-
360
- if len(pydantic_subtype_set) > 0 and not any(
361
- issubclass(elem, t)
362
- for t in (int, float, str, bool, NoneType, BaseModel, Enum)
363
- for elem in pydantic_subtype_set
364
- ):
365
- raise ForbiddenTypeHintsError(
366
- message=(
367
- "Pydantic sub type must be one of the primitive, enum or "
368
- f"BaseModel, got: {pydantic_subtype_set}."
369
- )
370
- )
371
-
372
- if not any(
373
- issubclass(elem, t)
374
- for t in (int, float, str, bool, list, NoneType, BaseModel, Enum)
375
- for elem in pydantic_main_type_set
376
- ):
377
- raise ForbiddenTypeHintsError(
378
- message=(
379
- "Pydantic main type must be one of the primitive, enum, list "
380
- f"or BaseModel, got {pydantic_main_type_set}."
381
- )
382
- )
383
-
384
407
  async def feed_chunks(self, chunks: str) -> None:
385
408
  """
386
409
  Feeds a string of characters to the JMux parser.
@@ -556,7 +579,10 @@ class JMux(ABC):
556
579
  await self._parse_primitive()
557
580
  await self._sink.close()
558
581
  self._decoder.reset()
559
- self._pda.set_state(S.EXPECT_KEY)
582
+ if ch in JSON_WHITESPACE:
583
+ self._pda.set_state(S.EXPECT_COMMA_OR_EOC)
584
+ else:
585
+ self._pda.set_state(S.EXPECT_KEY)
560
586
  if ch in OBJECT_CLOSE:
561
587
  await self._finalize()
562
588
  else:
jmux/helpers.py CHANGED
@@ -1,7 +1,8 @@
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
4
  from jmux.error import ParsePrimitiveError
5
+ from jmux.types import TYPES_LIKE_NONE, TYPES_LIKE_UNION
5
6
 
6
7
 
7
8
  def str_to_bool(s: str) -> bool:
@@ -20,8 +21,17 @@ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[
20
21
  Origin: Type | None = get_origin(UnknownType)
21
22
  if Origin is None:
22
23
  return {UnknownType}, set()
23
- if Origin is UnionType or Origin is Union:
24
- 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()
25
35
 
26
36
  type_args = get_args(UnknownType)
27
37
  if len(type_args) != 1:
@@ -31,7 +41,7 @@ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[
31
41
  )
32
42
 
33
43
  Generic: Type = type_args[0]
34
- type_set = deconstruct_type(Generic)
44
+ type_set = deconstruct_flat_type(Generic)
35
45
  if len(type_set) == 1:
36
46
  return {Origin}, type_set
37
47
  if len(type_set) != 2:
@@ -46,16 +56,20 @@ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[
46
56
  return {Origin}, type_set
47
57
 
48
58
 
49
- def deconstruct_type(UnknownType: Type) -> Set[Type]:
59
+ def deconstruct_flat_type(UnknownType: Type) -> Set[Type]:
50
60
  Origin: Type | None = get_origin(UnknownType)
51
- if UnknownType is None:
61
+ if UnknownType in TYPES_LIKE_NONE:
52
62
  return {NoneType}
53
63
  if Origin is None:
54
64
  return {UnknownType}
55
- if not (Origin is UnionType or Origin is Union):
56
- return {Origin}
57
- type_args = get_args(UnknownType)
58
- 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
+ )
59
73
 
60
74
 
61
75
  def get_main_type(type_set: Set[Type]) -> Type:
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
@@ -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):
@@ -56,3 +57,7 @@ JSON_FALSE = "false"
56
57
  JSON_TRUE = "true"
57
58
  JSON_NULL = "null"
58
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.3
3
+ Version: 0.0.5
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=Y6KVryRDLvGV5nBsneXpTvC0WUGhR5Z89Dvqz4HMAgg,1562
4
+ jmux/demux.py,sha256=OuUaNwvKI6WJxdzBzbBgx0yPq1kn4jSttuB5lvwzztk,36422
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=CJhFS9RVgR0cDBNJR8ROAFnxzG4YTYpNZ90hyD2SxsY,1389
9
+ jmux-0.0.5.dist-info/licenses/LICENSE,sha256=y0qnwaAe4bEqzNPyq4M_VZA2I2mQly8MawajyZhqw0k,1169
10
+ jmux-0.0.5.dist-info/METADATA,sha256=31D2JB7C5kqeislSLEg3WRkyGXO5cx3mY34SAJd1ntU,13330
11
+ jmux-0.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
12
+ jmux-0.0.5.dist-info/top_level.txt,sha256=TF2N6kHqLghfOkCiNlCueMDX4l5rPn_5MSPNtYrS1-o,5
13
+ jmux-0.0.5.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=0dKIQibyDQkijuIcT53Za1L2DZbC_Ll0_1mhroqHg9I,35516
5
- jmux/error.py,sha256=VZJYivt8RPfjcF2bs-T7_UkH3dVA3xH-xGbZggQV14k,4665
6
- jmux/helpers.py,sha256=DQyPeuwx3AR65aHrET7xpSbfTLTC7ji3CyicyEhqJpk,2226
7
- jmux/pda.py,sha256=81gnh0eWGsgd_SrHkqjRQy_KkOSlBf5nor7pqKGgYjw,791
8
- jmux/types.py,sha256=5Ox01s-rGMQ1xGX8NRNBlyWtxvGKwnt9bjexXdESSYc,1233
9
- jmux-0.0.3.dist-info/licenses/LICENSE,sha256=y0qnwaAe4bEqzNPyq4M_VZA2I2mQly8MawajyZhqw0k,1169
10
- jmux-0.0.3.dist-info/METADATA,sha256=ZSi9Y5l5FsyaJuTW3N2K199mCNfkMe8_FLmxjIEfSAA,13330
11
- jmux-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- jmux-0.0.3.dist-info/top_level.txt,sha256=TF2N6kHqLghfOkCiNlCueMDX4l5rPn_5MSPNtYrS1-o,5
13
- jmux-0.0.3.dist-info/RECORD,,