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 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