jmux 0.0.1__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.1/.github/workflows/ci.yml +64 -0
- jmux-0.0.1/.gitignore +79 -0
- jmux-0.0.1/LICENSE +24 -0
- jmux-0.0.1/PKG-INFO +364 -0
- jmux-0.0.1/README.md +311 -0
- jmux-0.0.1/pyproject.toml +52 -0
- jmux-0.0.1/setup.cfg +4 -0
- jmux-0.0.1/src/jmux/__init__.py +0 -0
- jmux-0.0.1/src/jmux/awaitable.py +288 -0
- jmux-0.0.1/src/jmux/decoder.py +66 -0
- jmux-0.0.1/src/jmux/demux.py +869 -0
- jmux-0.0.1/src/jmux/error.py +141 -0
- jmux-0.0.1/src/jmux/helpers.py +60 -0
- jmux-0.0.1/src/jmux/pda.py +32 -0
- jmux-0.0.1/src/jmux/types.py +57 -0
- jmux-0.0.1/src/jmux.egg-info/PKG-INFO +364 -0
- jmux-0.0.1/src/jmux.egg-info/SOURCES.txt +26 -0
- jmux-0.0.1/src/jmux.egg-info/dependency_links.txt +1 -0
- jmux-0.0.1/src/jmux.egg-info/requires.txt +16 -0
- jmux-0.0.1/src/jmux.egg-info/top_level.txt +1 -0
- jmux-0.0.1/tests/conftest.py +6 -0
- jmux-0.0.1/tests/test_awaitables.py +78 -0
- jmux-0.0.1/tests/test_decoder.py +32 -0
- jmux-0.0.1/tests/test_demux__parse.py +380 -0
- jmux-0.0.1/tests/test_demux__stream.py +1131 -0
- jmux-0.0.1/tests/test_demux__validate.py +125 -0
- jmux-0.0.1/tests/test_helpers.py +73 -0
- jmux-0.0.1/uv.lock +696 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Run CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
tags:
|
|
8
|
+
- "v*.*.*"
|
|
9
|
+
pull_request: {}
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
UV_FROZEN: true
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
check:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.12", "3.13"]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: astral-sh/setup-uv@v6
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: |
|
|
31
|
+
uv sync --all-extras
|
|
32
|
+
|
|
33
|
+
- name: Lint with ruff
|
|
34
|
+
run: |
|
|
35
|
+
uv run ruff check .
|
|
36
|
+
|
|
37
|
+
- name: Test with pytest
|
|
38
|
+
run: |
|
|
39
|
+
uv run pytest
|
|
40
|
+
release:
|
|
41
|
+
needs: [check]
|
|
42
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
environment: release
|
|
45
|
+
permissions:
|
|
46
|
+
id-token: write
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: "3.12"
|
|
53
|
+
|
|
54
|
+
- name: Install 'build' library
|
|
55
|
+
run: pip install -U build
|
|
56
|
+
|
|
57
|
+
- name: Build package
|
|
58
|
+
run: python -m build
|
|
59
|
+
|
|
60
|
+
- name: Check build with twine
|
|
61
|
+
run: pip install twine && twine check dist/*
|
|
62
|
+
|
|
63
|
+
- name: Publish package to PyPI
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
jmux-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
.installed.cfg
|
|
24
|
+
*.egg
|
|
25
|
+
|
|
26
|
+
# Virtual environment
|
|
27
|
+
venv/
|
|
28
|
+
ENV/
|
|
29
|
+
env/
|
|
30
|
+
.venv/
|
|
31
|
+
.python-version
|
|
32
|
+
|
|
33
|
+
# PyInstaller
|
|
34
|
+
*.manifest
|
|
35
|
+
*.spec
|
|
36
|
+
|
|
37
|
+
# Installer logs
|
|
38
|
+
pip-log.txt
|
|
39
|
+
pip-delete-this-directory.txt
|
|
40
|
+
|
|
41
|
+
# Unit test / coverage reports
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.coverage
|
|
46
|
+
.coverage.*
|
|
47
|
+
.cache
|
|
48
|
+
nosetests.xml
|
|
49
|
+
coverage.xml
|
|
50
|
+
*.cover
|
|
51
|
+
*.py,cover
|
|
52
|
+
.hypothesis/
|
|
53
|
+
.pytest_cache/
|
|
54
|
+
|
|
55
|
+
# MyPy
|
|
56
|
+
.mypy_cache/
|
|
57
|
+
.dmypy.json
|
|
58
|
+
dmypy.json
|
|
59
|
+
|
|
60
|
+
# Pyre type checker
|
|
61
|
+
.pyre/
|
|
62
|
+
|
|
63
|
+
# Cython build artifacts
|
|
64
|
+
*.c
|
|
65
|
+
*.cpp
|
|
66
|
+
*.pyd
|
|
67
|
+
*.pyo
|
|
68
|
+
|
|
69
|
+
# Editors and OS files
|
|
70
|
+
*.DS_Store
|
|
71
|
+
Thumbs.db
|
|
72
|
+
.idea/
|
|
73
|
+
.vscode/
|
|
74
|
+
*.swp
|
|
75
|
+
*~
|
|
76
|
+
|
|
77
|
+
# Local project configs
|
|
78
|
+
.env
|
|
79
|
+
.env.*
|
jmux-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Johannes A.I. Unruh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
1. The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
2. You may use this software, including for commercial purposes, as long as
|
|
15
|
+
you do not sell, license, or otherwise distribute the original or
|
|
16
|
+
substantially similar versions of this software for a fee.
|
|
17
|
+
|
|
18
|
+
3. This restriction does not apply to using this software as a dependency in
|
|
19
|
+
your own commercial applications or products, provided you are not selling
|
|
20
|
+
this software itself as a standalone product.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
jmux-0.0.1/PKG-INFO
ADDED
|
@@ -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.
|