jmux 0.0.3__tar.gz → 0.0.5__tar.gz
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-0.0.3 → jmux-0.0.5}/.github/workflows/ci.yml +1 -4
- {jmux-0.0.3 → jmux-0.0.5}/.gitignore +5 -1
- {jmux-0.0.3/src/jmux.egg-info → jmux-0.0.5}/PKG-INFO +2 -2
- {jmux-0.0.3 → jmux-0.0.5}/pyproject.toml +1 -1
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux/awaitable.py +10 -4
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux/demux.py +93 -67
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux/helpers.py +25 -11
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux/pda.py +8 -3
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux/types.py +6 -1
- {jmux-0.0.3 → jmux-0.0.5/src/jmux.egg-info}/PKG-INFO +2 -2
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux.egg-info/SOURCES.txt +0 -1
- {jmux-0.0.3 → jmux-0.0.5}/tests/test_demux__parse.py +7 -3
- {jmux-0.0.3 → jmux-0.0.5}/tests/test_demux__validate.py +9 -0
- {jmux-0.0.3 → jmux-0.0.5}/tests/test_helpers.py +27 -4
- jmux-0.0.3/uv.lock +0 -696
- {jmux-0.0.3 → jmux-0.0.5}/LICENSE +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/README.md +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/setup.cfg +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux/__init__.py +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux/decoder.py +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux/error.py +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux.egg-info/dependency_links.txt +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux.egg-info/requires.txt +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/src/jmux.egg-info/top_level.txt +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/tests/conftest.py +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/tests/test_awaitables.py +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/tests/test_decoder.py +0 -0
- {jmux-0.0.3 → jmux-0.0.5}/tests/test_demux__stream.py +0 -0
|
@@ -8,16 +8,13 @@ on:
|
|
|
8
8
|
- "v*.*.*"
|
|
9
9
|
pull_request: {}
|
|
10
10
|
|
|
11
|
-
env:
|
|
12
|
-
UV_FROZEN: true
|
|
13
|
-
|
|
14
11
|
jobs:
|
|
15
12
|
check:
|
|
16
13
|
runs-on: ubuntu-latest
|
|
17
14
|
strategy:
|
|
18
15
|
fail-fast: false
|
|
19
16
|
matrix:
|
|
20
|
-
python-version: ["3.12", "3.13"]
|
|
17
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
21
18
|
|
|
22
19
|
steps:
|
|
23
20
|
- uses: actions/checkout@v4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jmux
|
|
3
|
-
Version: 0.0.
|
|
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.
|
|
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
|
|
@@ -7,7 +7,7 @@ name = "jmux"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "JMux: A Python package for demultiplexing a JSON string into multiple awaitable variables."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Programming Language :: Python :: 3",
|
|
13
13
|
"Operating System :: OS Independent",
|
|
@@ -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]
|
|
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]
|
|
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]
|
|
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.
|
|
@@ -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
|
-
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from types import NoneType
|
|
2
|
-
from typing import Set, Tuple, Type,
|
|
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
|
|
24
|
-
|
|
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 =
|
|
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
|
|
59
|
+
def deconstruct_flat_type(UnknownType: Type) -> Set[Type]:
|
|
50
60
|
Origin: Type | None = get_origin(UnknownType)
|
|
51
|
-
if UnknownType
|
|
61
|
+
if UnknownType in TYPES_LIKE_NONE:
|
|
52
62
|
return {NoneType}
|
|
53
63
|
if Origin is None:
|
|
54
64
|
return {UnknownType}
|
|
55
|
-
if
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import Generic, List, Optional, TypeVar
|
|
3
4
|
|
|
4
|
-
|
|
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
|
|
23
|
+
def top(self) -> Optional[Context]:
|
|
19
24
|
if not self._stack:
|
|
20
25
|
return None
|
|
21
26
|
return self._stack[-1]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from
|
|
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
|
+
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.
|
|
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
|
|
@@ -282,7 +282,7 @@ parse_incorrect_stream__params = [
|
|
|
282
282
|
parse_incorrect_stream__params,
|
|
283
283
|
)
|
|
284
284
|
@pytest.mark.anyio
|
|
285
|
-
async def
|
|
285
|
+
async def test_json_demux__parse_stream__assert_error(
|
|
286
286
|
stream: str, MaybeExpectedError: Type[Exception] | None
|
|
287
287
|
):
|
|
288
288
|
class SObject(JMux):
|
|
@@ -375,7 +375,7 @@ parse_incorrect_stream_with_optionals__params = [
|
|
|
375
375
|
parse_incorrect_stream_with_optionals__params,
|
|
376
376
|
)
|
|
377
377
|
@pytest.mark.anyio
|
|
378
|
-
async def
|
|
378
|
+
async def test_json_demux__parse_stream_with_optionals__assert_error(
|
|
379
379
|
stream: str, MaybeExpectedError: Type[Exception] | None
|
|
380
380
|
):
|
|
381
381
|
class SObject(JMux):
|
|
@@ -416,6 +416,10 @@ parse_correct_stream__double_nested__params = [
|
|
|
416
416
|
('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": "val', None),
|
|
417
417
|
('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": "val"}', None),
|
|
418
418
|
('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": "val"}}', None),
|
|
419
|
+
('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": null}}', None),
|
|
420
|
+
('{"key_first_nested": {"key_second_nested": {"key_str": "val"}, "key_str": null\n}}', None),
|
|
421
|
+
('{"key_first_nested": null}', None),
|
|
422
|
+
('{"key_first_nested": null\n}', None),
|
|
419
423
|
]
|
|
420
424
|
# fmt: on
|
|
421
425
|
@pytest.mark.parametrize(
|
|
@@ -423,7 +427,7 @@ parse_correct_stream__double_nested__params = [
|
|
|
423
427
|
parse_correct_stream__double_nested__params,
|
|
424
428
|
)
|
|
425
429
|
@pytest.mark.anyio
|
|
426
|
-
async def
|
|
430
|
+
async def test_json_demux__parse_stream__double_nested(
|
|
427
431
|
stream: str, MaybeExpectedError: Type[Exception] | None
|
|
428
432
|
):
|
|
429
433
|
class SObject(JMux):
|
|
@@ -72,6 +72,14 @@ class CorrectPydantic_2(BaseModel):
|
|
|
72
72
|
key_bool: bool | None
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
class CorrectJMux_3(JMux):
|
|
76
|
+
arr_str: StreamableValues[str]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CorrectPydantic_3(BaseModel):
|
|
80
|
+
arr_str: list[str] | None
|
|
81
|
+
|
|
82
|
+
|
|
75
83
|
class IncorrectJMux_1(JMux):
|
|
76
84
|
key_str: AwaitableValue[str]
|
|
77
85
|
|
|
@@ -107,6 +115,7 @@ class IncorrectPydantic_3(BaseModel):
|
|
|
107
115
|
[
|
|
108
116
|
(CorrectJMux_1, CorrectPydantic_1, None),
|
|
109
117
|
(CorrectJMux_2, CorrectPydantic_2, None),
|
|
118
|
+
(CorrectJMux_3, CorrectPydantic_3, None),
|
|
110
119
|
(IncorrectJMux_1, IncorrectPydantic_1, ObjectMissmatchedError),
|
|
111
120
|
(IncorrectJMux_2, IncorrectPydantic_2, ObjectMissmatchedError),
|
|
112
121
|
(IncorrectJMux_3, IncorrectPydantic_3, ObjectMissmatchedError),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from types import NoneType
|
|
2
|
-
from typing import List, Optional, Set, Tuple, Type
|
|
2
|
+
from typing import List, Optional, Set, Tuple, Type, Union
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
@@ -9,26 +9,43 @@ from jmux.awaitable import (
|
|
|
9
9
|
UnderlyingGenericMixin,
|
|
10
10
|
)
|
|
11
11
|
from jmux.demux import JMux
|
|
12
|
-
from jmux.helpers import
|
|
12
|
+
from jmux.helpers import deconstruct_flat_type, extract_types_from_generic_alias
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@pytest.mark.parametrize(
|
|
16
16
|
"TargetType,expected_tuple",
|
|
17
17
|
[
|
|
18
18
|
(int, {int}),
|
|
19
|
+
(Union[int], {int}),
|
|
20
|
+
#
|
|
19
21
|
(Optional[int], {int, NoneType}),
|
|
20
22
|
(int | None, {int, NoneType}),
|
|
23
|
+
#
|
|
24
|
+
(Union[int, None], {int, NoneType}),
|
|
25
|
+
(int | NoneType, {int, NoneType}),
|
|
26
|
+
(Union[int, NoneType], {int, NoneType}),
|
|
27
|
+
#
|
|
21
28
|
(int | str, {str, int}),
|
|
29
|
+
(Union[int, str], {str, int}),
|
|
30
|
+
#
|
|
22
31
|
(int | str | NoneType, {str, int, NoneType}),
|
|
32
|
+
(Union[int, str, NoneType], {str, int, NoneType}),
|
|
33
|
+
#
|
|
23
34
|
(JMux, {JMux}),
|
|
35
|
+
(Union[JMux], {JMux}),
|
|
36
|
+
#
|
|
24
37
|
(JMux | None, {JMux, NoneType}),
|
|
38
|
+
(Union[JMux, None], {JMux, NoneType}),
|
|
39
|
+
(Union[JMux, NoneType], {JMux, NoneType}),
|
|
40
|
+
#
|
|
25
41
|
(JMux | NoneType, {JMux, NoneType}),
|
|
42
|
+
(Union[JMux, NoneType], {JMux, NoneType}),
|
|
26
43
|
],
|
|
27
44
|
)
|
|
28
|
-
def
|
|
45
|
+
def test_deconstruct_flat_types(
|
|
29
46
|
TargetType: Type[UnderlyingGenericMixin], expected_tuple: Tuple[Type, Set[Type]]
|
|
30
47
|
):
|
|
31
|
-
underlying_types =
|
|
48
|
+
underlying_types = deconstruct_flat_type(TargetType)
|
|
32
49
|
|
|
33
50
|
assert underlying_types == expected_tuple
|
|
34
51
|
|
|
@@ -46,7 +63,13 @@ class NestedObject(JMux):
|
|
|
46
63
|
(Optional[int], ({int, NoneType}, set())),
|
|
47
64
|
(int | None, ({int, NoneType}, set())),
|
|
48
65
|
(List[int], ({list}, {int})),
|
|
66
|
+
(list[int], ({list}, {int})),
|
|
49
67
|
(List[int | None], ({list}, {int, NoneType})),
|
|
68
|
+
(list[int | None], ({list}, {int, NoneType})),
|
|
69
|
+
(List[int] | None, ({list, NoneType}, {int})),
|
|
70
|
+
(list[int] | None, ({list, NoneType}, {int})),
|
|
71
|
+
(Optional[List[int]], ({list, NoneType}, {int})),
|
|
72
|
+
(Optional[list[int]], ({list, NoneType}, {int})),
|
|
50
73
|
(AwaitableValue[int], ({AwaitableValue}, {int})),
|
|
51
74
|
(AwaitableValue[float], ({AwaitableValue}, {float})),
|
|
52
75
|
(AwaitableValue[str], ({AwaitableValue}, {str})),
|