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/__init__.py +0 -0
- jmux/awaitable.py +288 -0
- jmux/decoder.py +66 -0
- jmux/demux.py +869 -0
- jmux/error.py +141 -0
- jmux/helpers.py +60 -0
- jmux/pda.py +32 -0
- jmux/types.py +57 -0
- jmux-0.0.1.dist-info/METADATA +364 -0
- jmux-0.0.1.dist-info/RECORD +13 -0
- jmux-0.0.1.dist-info/WHEEL +5 -0
- jmux-0.0.1.dist-info/licenses/LICENSE +24 -0
- jmux-0.0.1.dist-info/top_level.txt +1 -0
jmux/__init__.py
ADDED
|
File without changes
|
jmux/awaitable.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from asyncio import Event, Queue
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from types import NoneType
|
|
4
|
+
from typing import (
|
|
5
|
+
AsyncGenerator,
|
|
6
|
+
Protocol,
|
|
7
|
+
Set,
|
|
8
|
+
Type,
|
|
9
|
+
cast,
|
|
10
|
+
runtime_checkable,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from jmux.error import NothingEmittedError, SinkClosedError
|
|
14
|
+
from jmux.helpers import extract_types_from_generic_alias
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SinkType(Enum):
|
|
18
|
+
STREAMABLE_VALUES = "StreamableValues"
|
|
19
|
+
AWAITABLE_VALUE = "AwaitableValue"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnderlyingGenericMixin[T]:
|
|
23
|
+
"""
|
|
24
|
+
A mixin class that provides methods for inspecting the generic types of a
|
|
25
|
+
class at runtime.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def get_underlying_generics(self) -> Set[Type[T]]:
|
|
29
|
+
"""
|
|
30
|
+
Returns the underlying generic types of the class.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
TypeError: If the class is not initialized with a defined generic type.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A set of the underlying generic types.
|
|
37
|
+
"""
|
|
38
|
+
# `__orig_class__` is only set after the `__init__` method is called
|
|
39
|
+
if not hasattr(self, "__orig_class__"):
|
|
40
|
+
raise TypeError(
|
|
41
|
+
"AwaitableValue must be initialized with a defined generic type."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
Origin = getattr(self, "__orig_class__")
|
|
45
|
+
_, type_set = extract_types_from_generic_alias(Origin)
|
|
46
|
+
return type_set
|
|
47
|
+
|
|
48
|
+
def get_underlying_main_generic(self) -> Type[T]:
|
|
49
|
+
"""
|
|
50
|
+
Returns the main underlying generic type of the class.
|
|
51
|
+
This is the generic type that is not NoneType.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The main underlying generic type.
|
|
55
|
+
"""
|
|
56
|
+
underlying_generics = self.get_underlying_generics()
|
|
57
|
+
if len(underlying_generics) == 1:
|
|
58
|
+
return underlying_generics.pop()
|
|
59
|
+
remaining = {g for g in underlying_generics if g is not NoneType}
|
|
60
|
+
return remaining.pop()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@runtime_checkable
|
|
64
|
+
class IAsyncSink[T](Protocol):
|
|
65
|
+
"""
|
|
66
|
+
An asynchronous sink protocol that defines a common interface for putting, closing,
|
|
67
|
+
and retrieving values from a sink.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def get_underlying_generics(self) -> Set[Type[T]]:
|
|
71
|
+
"""Return the underlying generic type of the sink."""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def get_underlying_main_generic(self) -> Type[T]:
|
|
75
|
+
"""Return the underlying non-NoneType generic type of the sink."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
async def put(self, item: T):
|
|
79
|
+
"""Put an item into the sink."""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
async def close(self):
|
|
83
|
+
"""Close the sink."""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
async def ensure_closed(self):
|
|
87
|
+
"""Ensure the sink is closed."""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
def get_current(self) -> T:
|
|
91
|
+
"""Get the current value from the sink."""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
def get_sink_type(self) -> SinkType:
|
|
95
|
+
"""Get the type of the sink."""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class StreamableValues[T](UnderlyingGenericMixin[T]):
|
|
100
|
+
"""
|
|
101
|
+
A class that represents a stream of values that can be asynchronously iterated over.
|
|
102
|
+
It uses an asyncio.Queue to store the items and allows for putting items into the
|
|
103
|
+
stream and closing it when no more items will be added.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self):
|
|
107
|
+
self._queue = Queue[T | None]()
|
|
108
|
+
self._last_item: T | None = None
|
|
109
|
+
self._closed = False
|
|
110
|
+
|
|
111
|
+
def get_underlying_generics(self) -> Set[Type[T]]:
|
|
112
|
+
"""
|
|
113
|
+
Returns the underlying generic types of the class.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
TypeError: If the class does not have exactly one underlying type.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A set of the underlying generic types.
|
|
120
|
+
"""
|
|
121
|
+
generic = super().get_underlying_generics()
|
|
122
|
+
if len(generic) != 1:
|
|
123
|
+
raise TypeError("StreamableValues must have exactly one underlying type.")
|
|
124
|
+
return generic
|
|
125
|
+
|
|
126
|
+
async def put(self, item: T):
|
|
127
|
+
"""
|
|
128
|
+
Puts an item into the stream.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
item: The item to put into the stream.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If the stream is closed.
|
|
135
|
+
"""
|
|
136
|
+
if self._closed:
|
|
137
|
+
raise ValueError("Cannot put item into a closed sink.")
|
|
138
|
+
self._last_item = item
|
|
139
|
+
await self._queue.put(item)
|
|
140
|
+
|
|
141
|
+
async def close(self):
|
|
142
|
+
"""
|
|
143
|
+
Closes the stream.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
SinkClosedError: If the stream is already closed.
|
|
147
|
+
"""
|
|
148
|
+
if self._closed:
|
|
149
|
+
raise SinkClosedError(
|
|
150
|
+
f"SinkType {self.get_sink_type()}[{self.get_underlying_main_generic()}]"
|
|
151
|
+
+ " is already closed."
|
|
152
|
+
)
|
|
153
|
+
self._closed = True
|
|
154
|
+
await self._queue.put(None)
|
|
155
|
+
|
|
156
|
+
async def ensure_closed(self):
|
|
157
|
+
"""
|
|
158
|
+
Ensures that the stream is closed.
|
|
159
|
+
If the stream is already closed, this method does nothing.
|
|
160
|
+
"""
|
|
161
|
+
if self._closed:
|
|
162
|
+
return
|
|
163
|
+
await self.close()
|
|
164
|
+
|
|
165
|
+
def get_current(self) -> T:
|
|
166
|
+
"""
|
|
167
|
+
Returns the last item that was put into the stream.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ValueError: If no items have been put into the stream yet.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The last item that was put into the stream.
|
|
174
|
+
"""
|
|
175
|
+
if self._last_item is None:
|
|
176
|
+
raise ValueError("StreamableValues has not received any items yet.")
|
|
177
|
+
return self._last_item
|
|
178
|
+
|
|
179
|
+
def get_sink_type(self) -> SinkType:
|
|
180
|
+
"""
|
|
181
|
+
Returns the type of the sink.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The type of the sink.
|
|
185
|
+
"""
|
|
186
|
+
return SinkType.STREAMABLE_VALUES
|
|
187
|
+
|
|
188
|
+
def __aiter__(self):
|
|
189
|
+
return self._stream()
|
|
190
|
+
|
|
191
|
+
async def _stream(self) -> AsyncGenerator[T, None]:
|
|
192
|
+
while True:
|
|
193
|
+
item = await self._queue.get()
|
|
194
|
+
if item is None and self._closed:
|
|
195
|
+
break
|
|
196
|
+
if item is None:
|
|
197
|
+
raise ValueError("Received None item, but the sink is not closed.")
|
|
198
|
+
yield item
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class AwaitableValue[T](UnderlyingGenericMixin[T]):
|
|
202
|
+
"""
|
|
203
|
+
A class that represents a value that will be available in the future.
|
|
204
|
+
It can be awaited to get the value, and it can only be set once.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(self):
|
|
208
|
+
self._is_closed = False
|
|
209
|
+
self._event = Event()
|
|
210
|
+
self._value: T | None = None
|
|
211
|
+
|
|
212
|
+
async def put(self, value: T):
|
|
213
|
+
"""
|
|
214
|
+
Sets the value of the AwaitableValue.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
value: The value to set.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
ValueError: If the value has already been set.
|
|
221
|
+
"""
|
|
222
|
+
if self._value is not None or self._is_closed or self._event.is_set():
|
|
223
|
+
raise ValueError("AwaitableValue can only be set once.")
|
|
224
|
+
self._value = value
|
|
225
|
+
self._event.set()
|
|
226
|
+
|
|
227
|
+
async def close(self):
|
|
228
|
+
"""
|
|
229
|
+
Closes the AwaitableValue.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
SinkClosedError: If the AwaitableValue is already closed.
|
|
233
|
+
NothingEmittedError: If the AwaitableValue is closed without a value,
|
|
234
|
+
and the underlying type is not NoneType.
|
|
235
|
+
"""
|
|
236
|
+
if self._is_closed:
|
|
237
|
+
raise SinkClosedError(
|
|
238
|
+
f"SinkType {self.get_sink_type()}"
|
|
239
|
+
+"[{self.get_underlying_main_generic().__name__}] is already closed."
|
|
240
|
+
)
|
|
241
|
+
elif not self._event.is_set() and NoneType in self.get_underlying_generics():
|
|
242
|
+
self._event.set()
|
|
243
|
+
elif not self._event.is_set():
|
|
244
|
+
raise NothingEmittedError(
|
|
245
|
+
"Trying to close non-NoneType AwaitableValue without a value."
|
|
246
|
+
)
|
|
247
|
+
self._is_closed = True
|
|
248
|
+
|
|
249
|
+
async def ensure_closed(self):
|
|
250
|
+
"""
|
|
251
|
+
Ensures that the AwaitableValue is closed.
|
|
252
|
+
If the AwaitableValue is already closed, this method does nothing.
|
|
253
|
+
"""
|
|
254
|
+
if self._is_closed:
|
|
255
|
+
return
|
|
256
|
+
await self.close()
|
|
257
|
+
|
|
258
|
+
def get_current(self) -> T:
|
|
259
|
+
"""
|
|
260
|
+
Returns the value of the AwaitableValue.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
ValueError: If the value has not been set yet.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
The value of the AwaitableValue.
|
|
267
|
+
"""
|
|
268
|
+
if self._value is None:
|
|
269
|
+
raise ValueError("AwaitableValue has not been set yet.")
|
|
270
|
+
return self._value
|
|
271
|
+
|
|
272
|
+
def get_sink_type(self) -> SinkType:
|
|
273
|
+
"""
|
|
274
|
+
Returns the type of the sink.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
The type of the sink.
|
|
278
|
+
"""
|
|
279
|
+
return SinkType.AWAITABLE_VALUE
|
|
280
|
+
|
|
281
|
+
def __await__(self):
|
|
282
|
+
return self._wait().__await__()
|
|
283
|
+
|
|
284
|
+
async def _wait(self) -> T:
|
|
285
|
+
await self._event.wait()
|
|
286
|
+
if self._value is None and not self._event.is_set():
|
|
287
|
+
raise ValueError("No value has been put into the sink.")
|
|
288
|
+
return cast(T, self._value)
|
jmux/decoder.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IDecoder(Protocol):
|
|
5
|
+
def push(self, ch: str) -> str | None: ...
|
|
6
|
+
|
|
7
|
+
def is_terminating_quote(self, ch: str) -> bool: ...
|
|
8
|
+
|
|
9
|
+
def reset(self) -> None: ...
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def buffer(self) -> str: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StringEscapeDecoder:
|
|
16
|
+
r"""
|
|
17
|
+
Decoder for strings with escape sequences, such as JSON strings.
|
|
18
|
+
Handles escape sequences like \", \\, \/, \b, \f, \n, \r, \t, and unicode escapes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
escape_map = {
|
|
22
|
+
'"': '"',
|
|
23
|
+
"\\": "\\",
|
|
24
|
+
"/": "/",
|
|
25
|
+
"b": "\b",
|
|
26
|
+
"f": "\f",
|
|
27
|
+
"n": "\n",
|
|
28
|
+
"r": "\r",
|
|
29
|
+
"t": "\t",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self._buffer = ""
|
|
34
|
+
self._string_escape = False
|
|
35
|
+
|
|
36
|
+
def push(self, ch: str) -> str | None:
|
|
37
|
+
if self._string_escape:
|
|
38
|
+
self._string_escape = False
|
|
39
|
+
if ch == "u":
|
|
40
|
+
self.is_parsing_unicode = True
|
|
41
|
+
self.unicode_buffer = ""
|
|
42
|
+
return
|
|
43
|
+
escaped_char = self.escape_map.get(ch, ch)
|
|
44
|
+
self._buffer += escaped_char
|
|
45
|
+
return escaped_char
|
|
46
|
+
|
|
47
|
+
if ch == "\\":
|
|
48
|
+
self._string_escape = True
|
|
49
|
+
else:
|
|
50
|
+
self._buffer += ch
|
|
51
|
+
return ch
|
|
52
|
+
|
|
53
|
+
def is_terminating_quote(self, ch: str) -> bool:
|
|
54
|
+
if self._string_escape:
|
|
55
|
+
return False
|
|
56
|
+
if ch == '"':
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def reset(self) -> None:
|
|
61
|
+
self._buffer = ""
|
|
62
|
+
self._string_escape = False
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def buffer(self) -> str:
|
|
66
|
+
return self._buffer
|