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/error.py ADDED
@@ -0,0 +1,141 @@
1
+ from enum import Enum
2
+ from typing import Sequence
3
+
4
+
5
+ class MissingAttributeError(Exception):
6
+ def __init__(self, object_name: str, attribute: str) -> None:
7
+ super().__init__(f"'{object_name}' is missing required attribute '{attribute}'")
8
+
9
+
10
+ class UnexpectedAttributeTypeError(Exception):
11
+ def __init__(self, object_name: str, attribute: str, expected_type: str) -> None:
12
+ super().__init__(
13
+ f"'{object_name}' has attribute '{attribute}' with unexpected type. "
14
+ f"Attribute must conform to {expected_type}."
15
+ )
16
+
17
+
18
+ class EmptyKeyError(Exception):
19
+ def __init__(self, message: str | None = None) -> None:
20
+ super().__init__("Key cannot be empty" + (f": {message}" if message else ""))
21
+ self.message = message
22
+
23
+
24
+ class TypeEmitError(Exception):
25
+ def __init__(self, expected_type: str, actual_type: str) -> None:
26
+ super().__init__(
27
+ f"Cannot emit to current sink. Type mismatch: expected {expected_type}, "
28
+ f"got {actual_type}"
29
+ )
30
+ self.expected_type = expected_type
31
+ self.actual_type = actual_type
32
+
33
+
34
+ class NoCurrentSinkError(Exception):
35
+ def __init__(self, message: str | None = None) -> None:
36
+ super().__init__(
37
+ "No current sink available" + (f": {message}" if message else "")
38
+ )
39
+
40
+
41
+ class ParsePrimitiveError(Exception):
42
+ def __init__(self, message: str | None = None) -> None:
43
+ super().__init__(
44
+ "Failed to parse primitive value" + (f": {message}" if message else "")
45
+ )
46
+
47
+
48
+ class NothingEmittedError(Exception):
49
+ def __init__(self, message: str | None = None) -> None:
50
+ super().__init__(
51
+ "Nothing was emitted to the sink" + (f": {message}" if message else "")
52
+ )
53
+ self.message = message
54
+
55
+
56
+ class SinkClosedError(Exception):
57
+ def __init__(self, message: str | None = None) -> None:
58
+ super().__init__(
59
+ "Sink is closed and cannot accept new items"
60
+ + (f": {message}" if message else "")
61
+ )
62
+ self.message = message
63
+
64
+
65
+ class NotAllObjectPropertiesSetError(Exception):
66
+ def __init__(self, message: str | None = None) -> None:
67
+ super().__init__(
68
+ "Not all properties were set before closing the sink respective stream"
69
+ + (f": {message}" if message else "")
70
+ )
71
+
72
+
73
+ class ObjectAlreadyClosedError(Exception):
74
+ def __init__(self, object_name: str, message: str | None = None) -> None:
75
+ super().__init__(
76
+ f"Object '{object_name}' is already closed"
77
+ + (f": {message}" if message else "")
78
+ )
79
+
80
+
81
+ class ObjectMissmatchedError(Exception):
82
+ def __init__(
83
+ self,
84
+ jmux_model: str,
85
+ pydantic_model: str,
86
+ attribute: str,
87
+ message: str | None = None,
88
+ ) -> None:
89
+ super().__init__(
90
+ f"JMux object '{jmux_model}' and '{pydantic_model}' are missmatched on "
91
+ f"attribute '{attribute}'" + (f": {message}" if message else "")
92
+ )
93
+
94
+
95
+ class ForbiddenTypeHintsError(Exception):
96
+ def __init__(self, message: str | None = None) -> None:
97
+ super().__init__(
98
+ "Forbidden type hints used in object definition"
99
+ + (f": {message}" if message else "")
100
+ )
101
+
102
+
103
+ class UnexpectedCharacterError(Exception):
104
+ def __init__(
105
+ self,
106
+ character: str,
107
+ pda_stack: Sequence[Enum] | str,
108
+ pda_state: Enum | str,
109
+ message: str | None,
110
+ ) -> None:
111
+ pda_stack_str = [
112
+ item.value if isinstance(item, Enum) else item for item in pda_stack
113
+ ]
114
+ pda_state_str = pda_state.value if isinstance(pda_state, Enum) else pda_state
115
+ super().__init__(
116
+ f"Received unexpected character '{character}' in state '{pda_state_str}' "
117
+ f"with stack {pda_stack_str}" + (f": {message}" if message else "")
118
+ )
119
+
120
+
121
+ class StreamParseError(Exception):
122
+ def __init__(self, message: str | None = None) -> None:
123
+ super().__init__("Failed to parse stream" + (f": {message}" if message else ""))
124
+ self.message = message
125
+
126
+
127
+ class UnexpectedStateError(Exception):
128
+ def __init__(
129
+ self,
130
+ pda_stack: Sequence[Enum] | str,
131
+ pda_state: Enum | str,
132
+ message: str | None = None,
133
+ ) -> None:
134
+ pda_stack_str = [
135
+ item.value if isinstance(item, Enum) else item for item in pda_stack
136
+ ]
137
+ pda_state_str = pda_state.value if isinstance(pda_state, Enum) else pda_state
138
+ super().__init__(
139
+ f"Unexpected state '{pda_state_str}' with stack {pda_stack_str}"
140
+ + (f": {message}" if message else "")
141
+ )
jmux/helpers.py ADDED
@@ -0,0 +1,60 @@
1
+ from types import NoneType, UnionType
2
+ from typing import Set, Tuple, Type, Union, get_args, get_origin
3
+
4
+
5
+ def is_json_whitespace(ch: str) -> bool:
6
+ return ch in {" ", "\t", "\n", "\r"}
7
+
8
+
9
+ def extract_types_from_generic_alias(UnknownType: Type) -> Tuple[Set[Type], Set[Type]]:
10
+ Origin: Type | None = get_origin(UnknownType)
11
+ if Origin is None:
12
+ return {UnknownType}, set()
13
+ if Origin is UnionType or Origin is Union:
14
+ return deconstruct_type(UnknownType), set()
15
+
16
+ type_args = get_args(UnknownType)
17
+ if len(type_args) != 1:
18
+ raise TypeError(
19
+ f"Only single type generics can be deconstruct with this function, "
20
+ f"got {type_args}."
21
+ )
22
+
23
+ Generic: Type = type_args[0]
24
+ type_set = deconstruct_type(Generic)
25
+ if len(type_set) == 1:
26
+ return {Origin}, type_set
27
+ if len(type_set) != 2:
28
+ raise TypeError(
29
+ f"Union type must have exactly two types in its union, "
30
+ f"got {get_args(Generic)}."
31
+ )
32
+ if NoneType not in get_args(Generic):
33
+ raise TypeError(
34
+ "Union type must include NoneType if it is used as a generic argument."
35
+ )
36
+ return {Origin}, type_set
37
+
38
+
39
+ def deconstruct_type(UnknownType: Type) -> Set[Type]:
40
+ Origin: Type | None = get_origin(UnknownType)
41
+ if UnknownType is None:
42
+ return {NoneType}
43
+ if Origin is None:
44
+ return {UnknownType}
45
+ if not (Origin is UnionType or Origin is Union):
46
+ return {Origin}
47
+ type_args = get_args(UnknownType)
48
+ return set(type_args)
49
+
50
+
51
+ def get_main_type(type_set: Set[Type]) -> Type:
52
+ type_set_copy: Set[Type] = type_set.copy()
53
+ if NoneType in type_set_copy and len(type_set_copy) == 2:
54
+ type_set_copy.remove(NoneType)
55
+ if len(type_set_copy) != 1:
56
+ raise TypeError(
57
+ f"Expected exactly one type, got {type_set_copy}. If you want to allow "
58
+ "NoneType, use Union[int, NoneType]."
59
+ )
60
+ return type_set_copy.pop()
jmux/pda.py ADDED
@@ -0,0 +1,32 @@
1
+ from typing import List
2
+
3
+
4
+ class PushDownAutomata[Context, State]:
5
+ def __init__(self, start_state: State) -> None:
6
+ self._stack: List[Context] = []
7
+ self._state: State = start_state
8
+
9
+ @property
10
+ def state(self) -> State:
11
+ return self._state
12
+
13
+ @property
14
+ def stack(self) -> List[Context]:
15
+ return self._stack
16
+
17
+ @property
18
+ def top(self) -> Context | None:
19
+ if not self._stack:
20
+ return None
21
+ return self._stack[-1]
22
+
23
+ def set_state(self, new_state: State) -> None:
24
+ self._state = new_state
25
+
26
+ def push(self, mode: Context) -> None:
27
+ self._stack.append(mode)
28
+
29
+ def pop(self) -> Context:
30
+ if not self._stack:
31
+ raise IndexError("PDA stack is empty.")
32
+ return self._stack.pop()
jmux/types.py ADDED
@@ -0,0 +1,57 @@
1
+ from enum import Enum
2
+ from typing import Set
3
+
4
+
5
+ class State(Enum):
6
+ START = "start"
7
+ END = "end"
8
+ ERROR = "error"
9
+ # expect
10
+ EXPECT_KEY = "expect_key"
11
+ EXPECT_COLON = "expect_colon"
12
+ EXPECT_VALUE = "expect_value"
13
+ EXPECT_COMMA_OR_EOC = "expect_comma_or_eoc"
14
+ # parsing
15
+ PARSING_KEY = "parsing_key"
16
+ PARSING_STRING = "parsing_string"
17
+ PARSING_INTEGER = "parsing_integer"
18
+ PARSING_FLOAT = "parsing_float"
19
+ PARSING_BOOLEAN = "parsing_boolean"
20
+ PARSING_NULL = "parsing_null"
21
+ PARSING_OBJECT = "parsing_object"
22
+
23
+
24
+ PRIMITIVE_STATES: Set[State] = {
25
+ State.PARSING_INTEGER,
26
+ State.PARSING_FLOAT,
27
+ State.PARSING_BOOLEAN,
28
+ State.PARSING_NULL,
29
+ }
30
+
31
+
32
+ class Mode(Enum):
33
+ ROOT = "$"
34
+ OBJECT = "object"
35
+ ARRAY = "array"
36
+
37
+
38
+ OBJECT_OPEN = set("{")
39
+ OBJECT_CLOSE = set("}")
40
+ COLON = set(":")
41
+ ARRAY_OPEN = set("[")
42
+ ARRAY_CLOSE = set("]")
43
+ COMMA = set(",")
44
+ QUOTE = set('"')
45
+
46
+ NUMBER_OPEN = set("0123456789-")
47
+ BOOLEAN_OPEN = set("tf")
48
+ NULL_OPEN = set("n")
49
+
50
+ INTERGER_ALLOWED = set("0123456789")
51
+ FLOAT_ALLOWED = set("0123456789-+eE.")
52
+ BOOLEAN_ALLOWED = set("truefals")
53
+ NULL_ALLOWED = set("nul")
54
+
55
+ JSON_FALSE = "false"
56
+ JSON_TRUE = "true"
57
+ JSON_NULL = "null"
@@ -0,0 +1,364 @@
1
+ Metadata-Version: 2.4
2
+ Name: jmux
3
+ Version: 0.0.1
4
+ Summary: JMux: A Python package for demultiplexing a JSON string into multiple awaitable variables.
5
+ Author-email: "Johannes A.I. Unruh" <johannes@unruh.ai>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Johannes A.I. Unruh
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, subject to the following conditions:
15
+
16
+ 1. The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ 2. You may use this software, including for commercial purposes, as long as
20
+ you do not sell, license, or otherwise distribute the original or
21
+ substantially similar versions of this software for a fee.
22
+
23
+ 3. This restriction does not apply to using this software as a dependency in
24
+ your own commercial applications or products, provided you are not selling
25
+ this software itself as a standalone product.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
30
+ Project-URL: Homepage, https://github.com/jaunruh/jmux
31
+ Project-URL: Repository, https://github.com/jaunruh/jmux
32
+ Keywords: demultiplexer,python,package,json
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Operating System :: OS Independent
35
+ Requires-Python: >=3.12
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: anyio>=4.0.0
39
+ Requires-Dist: pydantic>=2.0.0
40
+ Provides-Extra: test
41
+ Requires-Dist: pytest; extra == "test"
42
+ Requires-Dist: pytest-anyio; extra == "test"
43
+ Provides-Extra: dev
44
+ Requires-Dist: ruff; extra == "dev"
45
+ Requires-Dist: pytest; extra == "dev"
46
+ Requires-Dist: pytest-anyio; extra == "dev"
47
+ Requires-Dist: uv; extra == "dev"
48
+ Requires-Dist: build; extra == "dev"
49
+ Requires-Dist: twine; extra == "dev"
50
+ Requires-Dist: setuptools; extra == "dev"
51
+ Requires-Dist: setuptools_scm[toml]; extra == "dev"
52
+ Dynamic: license-file
53
+
54
+ # JMux: A Python package for demultiplexing a JSON string into multiple awaitable variables.
55
+
56
+ JMux is a powerful Python package that allows you to demultiplex a JSON stream into multiple awaitable variables. It is specifically designed for asynchronous applications that interact with Large Language Models (LLMs) using libraries like `litellm`. When an LLM streams a JSON response, `jmux` enables you to parse and use parts of the JSON object _before_ the complete response has been received, significantly improving responsiveness.
57
+
58
+ ## Inspiration
59
+
60
+ This package is inspired by `Snapshot Streaming` mentioned in the [`WWDC25: Meet the Foundation Models framework`](https://youtu.be/mJMvFyBvZEk?si=DVIvxzuJOA87lb7I&t=465) keynote by Apple.
61
+
62
+ ## Features
63
+
64
+ - **Asynchronous by Design**: Built on top of `asyncio`, JMux is perfect for modern, high-performance Python applications.
65
+ - **Pydantic Integration**: Validate your `JMux` classes against Pydantic models to ensure type safety and consistency.
66
+ - **Awaitable and Streamable Sinks**: Use `AwaitableValue` for single values and `StreamableValues` for streams of values.
67
+ - **Robust Error Handling**: JMux provides a comprehensive set of exceptions to handle parsing errors and other issues.
68
+ - **Lightweight**: JMux has only a few external dependencies, making it easy to integrate into any project.
69
+
70
+ ## Installation
71
+
72
+ You can install JMux from PyPI using pip:
73
+
74
+ ```bash
75
+ pip install jmux
76
+ ```
77
+
78
+ ## Usage with LLMs (e.g., `litellm`)
79
+
80
+ The primary use case for `jmux` is to process streaming JSON responses from LLMs. This allows you to react to parts of the data as it arrives, rather than waiting for the entire JSON object to be transmitted. While this should be obvious, I should mention, that **the order in which the pydantic model defines the properties, defines which stream is filled first**.
81
+
82
+ Here’s a conceptual example of how you might integrate `jmux` with an LLM call, such as one made with `litellm`:
83
+
84
+ ```python
85
+ import asyncio
86
+ from pydantic import BaseModel
87
+ from jmux import JMux, AwaitableValue, StreamableValues
88
+ # litellm is used conceptually here
89
+ # from litellm import acompletion
90
+
91
+ # 1. Define the Pydantic model for the expected JSON response
92
+ class LlmResponse(BaseModel):
93
+ thought: str # **This property is filled first**
94
+ tool_code: str
95
+
96
+ # 2. Define the corresponding JMux class
97
+ class LlmResponseMux(JMux):
98
+ thought: AwaitableValue[str]
99
+ tool_code: StreamableValues[str] # Stream the code as it's generated
100
+
101
+ # 3. Validate that the JMux class matches the Pydantic model
102
+ LlmResponseMux.assert_conforms_to(LlmResponse)
103
+
104
+ # A mock function that simulates a streaming LLM call
105
+ async def mock_llm_stream():
106
+ json_stream = '{"thought": "I need to write some code.", "tool_code": "print(\'Hello, World!\')"}'
107
+ for char in json_stream:
108
+ yield char
109
+ await asyncio.sleep(0.01) # Simulate network latency
110
+
111
+ # Main function to orchestrate the call and processing
112
+ async def process_llm_response():
113
+ jmux_instance = LlmResponseMux()
114
+
115
+ # This task will consume the LLM stream and feed it to jmux
116
+ async def feed_stream():
117
+ async for chunk in mock_llm_stream():
118
+ await jmux_instance.feed_chunks(chunk)
119
+
120
+ # These tasks will consume the demultiplexed data from jmux
121
+ async def consume_thought():
122
+ thought = await jmux_instance.thought
123
+ print(f"LLM's thought received: '{thought}'")
124
+ # You can act on the thought immediately
125
+ # without waiting for the tool_code to finish streaming.
126
+
127
+ async def consume_tool_code():
128
+ print("Receiving tool code...")
129
+ full_code = ""
130
+ async for code_fragment in jmux_instance.tool_code:
131
+ full_code += code_fragment
132
+ print(f" -> Received fragment: {code_fragment}")
133
+ print(f"Full tool code received: {full_code}")
134
+
135
+ # Run all tasks concurrently
136
+ await asyncio.gather(
137
+ feed_stream(),
138
+ consume_thought(),
139
+ consume_tool_code()
140
+ )
141
+
142
+ if __name__ == "__main__":
143
+ asyncio.run(process_llm_response())
144
+ ```
145
+
146
+ ## Example Implementation
147
+
148
+ <details>
149
+ <summary>Python Code</summary>
150
+
151
+ ```python
152
+ def create_json_streaming_completion[T: BaseModel, J: IJsonDemuxer](
153
+ self,
154
+ messages: List[ILlmMessage],
155
+ ReturnType: Type[T],
156
+ JMux: Type[J],
157
+ retries: int = 3,
158
+ ) -> StreamResponseTuple[T, J]:
159
+ try:
160
+ JMux.assert_conforms_to(ReturnType)
161
+ litellm_messages = self._convert_messages(messages)
162
+ jmux_instance: J = JMux()
163
+
164
+ async def stream_feeding_llm_call() -> T:
165
+ nonlocal jmux_instance
166
+ buffer = ""
167
+ stream: CustomStreamWrapper = await self._router.acompletion( # see litellm `router`
168
+ model=self._internal_model_name.value,
169
+ messages=litellm_messages,
170
+ stream=True,
171
+ num_retries=retries,
172
+ response_format=ReturnType,
173
+ **self._maybe_google_credentials_param,
174
+ **self._model_params.model_dump(exclude_none=True),
175
+ **self._additional_params,
176
+ )
177
+
178
+ async for chunk in stream:
179
+ content_fragment: str | None = None
180
+
181
+ tool_calls = chunk.choices[0].delta.tool_calls
182
+ if tool_calls:
183
+ content_fragment = tool_calls[0].function.arguments
184
+ elif chunk.choices[0].delta.content:
185
+ content_fragment = chunk.choices[0].delta.content
186
+
187
+ if content_fragment:
188
+ try:
189
+ buffer += content_fragment
190
+ await jmux_instance.feed_chunks(content_fragment)
191
+ except Exception as e:
192
+ logger.warning(f"error in JMux feed_chunks: {e}")
193
+ raise e
194
+
195
+ return ReturnType.model_validate_json(buffer)
196
+
197
+ awaitable_llm_result = create_task(stream_feeding_llm_call())
198
+ return (awaitable_llm_result, jmux_instance)
199
+ except Exception as e:
200
+ logger.warning(f"error in create_json_streaming_completion: {e}")
201
+ raise e
202
+ ```
203
+
204
+ The code above shows an example implementation that uses a `litellm` router for `acompletion`.
205
+
206
+ You can either `await awaitable_llm_result` if you need the full result, or use `await jmux_instance.your_awaitable_value` or `async for ele in jmux_instance.your_streamable_values` to access partial results.
207
+
208
+ </details>
209
+
210
+ ## Basic Usage
211
+
212
+ Here is a simple example of how to use JMux to parse a JSON stream:
213
+
214
+ ```python
215
+ import asyncio
216
+ from enum import Enum
217
+ from types import NoneType
218
+ from pydantic import BaseModel
219
+
220
+ from jmux import JMux, AwaitableValue, StreamableValues
221
+
222
+ # 1. Define your JMux class
223
+ class SObject(JMux):
224
+ class SNested(JMux):
225
+ key_str: AwaitableValue[str]
226
+
227
+ class SEnum(Enum):
228
+ VALUE1 = "value1"
229
+ VALUE2 = "value2"
230
+
231
+ key_str: AwaitableValue[str]
232
+ key_int: AwaitableValue[int]
233
+ key_float: AwaitableValue[float]
234
+ key_bool: AwaitableValue[bool]
235
+ key_none: AwaitableValue[NoneType]
236
+ key_stream: StreamableValues[str]
237
+ key_enum: AwaitableValue[SEnum]
238
+ key_nested: AwaitableValue[SNested]
239
+
240
+ # 2. (Optional) Define a Pydantic model for validation
241
+ class PObject(BaseModel):
242
+ class PNested(BaseModel):
243
+ key_str: str
244
+
245
+ class PEnum(Enum):
246
+ VALUE1 = "value1"
247
+ VALUE2 = "value2"
248
+
249
+ key_str: str
250
+ key_int: int
251
+ key_float: float
252
+ key_bool: bool
253
+ key_none: NoneType
254
+ key_stream: str
255
+ key_enum: PEnum
256
+ key_nested: PNested
257
+
258
+ # 3. Validate the JMux class against the Pydantic model
259
+ SObject.assert_conforms_to(PObject)
260
+
261
+ # 4. Create an instance of your JMux class
262
+ s_object = SObject()
263
+
264
+ # 5. Feed the JSON stream to the JMux instance
265
+ async def main():
266
+ json_stream = '{"key_str": "hello", "key_int": 42, "key_float": 3.14, "key_bool": true, "key_none": null, "key_stream": "world", "key_enum": "value1", "key_nested": {"key_str": "nested"}}'
267
+
268
+ async def produce():
269
+ for char in json_stream:
270
+ await s_object.feed_char(char)
271
+
272
+ async def consume():
273
+ key_str = await s_object.key_str
274
+ print(f"key_str: {key_str}")
275
+
276
+ key_int = await s_object.key_int
277
+ print(f"key_int: {key_int}")
278
+
279
+ key_float = await s_object.key_float
280
+ print(f"key_float: {key_float}")
281
+
282
+ key_bool = await s_object.key_bool
283
+ print(f"key_bool: {key_bool}")
284
+
285
+ key_none = await s_object.key_none
286
+ print(f"key_none: {key_none}")
287
+
288
+ key_stream = ""
289
+ async for char in s_object.key_stream:
290
+ key_stream += char
291
+ print(f"key_stream: {key_stream}")
292
+
293
+ key_enum = await s_object.key_enum
294
+ print(f"key_enum: {key_enum}")
295
+
296
+ key_nested = await s_object.key_nested
297
+ nested_key_str = await key_nested.key_str
298
+ print(f"nested_key_str: {nested_key_str}")
299
+
300
+ await asyncio.gather(produce(), consume())
301
+
302
+ if __name__ == "__main__":
303
+ asyncio.run(main())
304
+ ```
305
+
306
+ ## API Reference
307
+
308
+ ### Abstract Calss `jmux.JMux`
309
+
310
+ The abstract base class for creating JSON demultiplexers.
311
+
312
+ > `JMux.assert_conforms_to(pydantic_model: Type[BaseModel]) -> None`
313
+
314
+ Asserts that the JMux class conforms to a given Pydantic model.
315
+
316
+ > `async JMux.feed_char(ch: str) -> None`
317
+
318
+ Feeds a character to the JMux parser.
319
+
320
+ > `async JMux.feed_chunks(chunks: str) -> None`
321
+
322
+ Feeds a string of characters to the JMux parser.
323
+
324
+ ### Class `jmux.AwaitableValue[T]`
325
+
326
+ A class that represents a value that will be available in the future. You are awaiting the full value and do not get partial results.
327
+
328
+ Allowed types here are (they can all be combined with `Optional`):
329
+
330
+ - `int`, `float`, `str`, `bool`, `NoneType`
331
+ - `JMux`
332
+ - `Enum`
333
+
334
+ In all cases, the corresponding `pydantic.BaseModel` should **not** be `list`
335
+
336
+ ### Class `jmux.StreamableValues[T]`
337
+
338
+ A class that represents a stream of values that can be asynchronously iterated over.
339
+
340
+ Allowed types are listed below and should all be wrapped in a `list` on the pydantic model:
341
+
342
+ - `int`, `float`, `str`, `bool`, `NoneType`
343
+ - `JMux`
344
+ - `Enum`
345
+
346
+ Additionally the following type is supported without being wrapped into `list`:
347
+
348
+ - `str`
349
+
350
+ This allows you to fully stream strings directly to a sink.
351
+
352
+ ## License
353
+
354
+ This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file for details.
355
+
356
+ ## Planned Improvements
357
+
358
+ - Add support for older Python versions
359
+
360
+ ## Contributions
361
+
362
+ As you might see, this repo has only been created recently and so far I am the only developer working on it. If you want to contribute, reach out via `johannes@unruh.ai` or `johannes.a.unruh@gmail.com`.
363
+
364
+ If you have suggestions or find any errors in my implementation, feel free to create an issue or also reach out via email.
@@ -0,0 +1,13 @@
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=ouhu2PIJkaEIsToNfDhJWGQYjFYULjqFSEX5ElmAjPs,34861
5
+ jmux/error.py,sha256=VZJYivt8RPfjcF2bs-T7_UkH3dVA3xH-xGbZggQV14k,4665
6
+ jmux/helpers.py,sha256=6y33RohUGVvGGaAREyRQTmEWfgV4w295SyqDwIMnUNs,1982
7
+ jmux/pda.py,sha256=81gnh0eWGsgd_SrHkqjRQy_KkOSlBf5nor7pqKGgYjw,791
8
+ jmux/types.py,sha256=V63wMx1I7l_P83JAqzOQ7H7B-xKrNUuGshJ3NjKsdeQ,1192
9
+ jmux-0.0.1.dist-info/licenses/LICENSE,sha256=y0qnwaAe4bEqzNPyq4M_VZA2I2mQly8MawajyZhqw0k,1169
10
+ jmux-0.0.1.dist-info/METADATA,sha256=Y4E3GeCPbNYvUv6B877m_c7DZUXlZO5voWCbxXVtwSg,13330
11
+ jmux-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ jmux-0.0.1.dist-info/top_level.txt,sha256=TF2N6kHqLghfOkCiNlCueMDX4l5rPn_5MSPNtYrS1-o,5
13
+ jmux-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+