pookiepy 0.0.12__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.
Files changed (110) hide show
  1. pookiepy-0.0.12/LICENSE.txt +28 -0
  2. pookiepy-0.0.12/PKG-INFO +460 -0
  3. pookiepy-0.0.12/examples/__init__.py +0 -0
  4. pookiepy-0.0.12/examples/interactive_streaming/GrpcServerExample.py +16 -0
  5. pookiepy-0.0.12/examples/interactive_streaming/LMProxyClient.py +100 -0
  6. pookiepy-0.0.12/examples/interactive_streaming/TextClient.py +102 -0
  7. pookiepy-0.0.12/examples/interactive_streaming/__init__.py +0 -0
  8. pookiepy-0.0.12/examples/interactive_streaming/_lm_http.py +144 -0
  9. pookiepy-0.0.12/examples/interactive_streaming/readme.md +50 -0
  10. pookiepy-0.0.12/examples/interactive_streaming/run_server_proxy.py +26 -0
  11. pookiepy-0.0.12/examples/interactive_streaming/run_text_client.py +14 -0
  12. pookiepy-0.0.12/examples/mcp_server/FileOperationClient.py +200 -0
  13. pookiepy-0.0.12/examples/mcp_server/GrpcServer.py +22 -0
  14. pookiepy-0.0.12/examples/mcp_server/LlmBridgeClient.py +370 -0
  15. pookiepy-0.0.12/examples/mcp_server/RunnerClient.py +218 -0
  16. pookiepy-0.0.12/examples/mcp_server/__init__.py +0 -0
  17. pookiepy-0.0.12/examples/mcp_server/_llm_utils.py +92 -0
  18. pookiepy-0.0.12/examples/mcp_server/_task.py +131 -0
  19. pookiepy-0.0.12/examples/mcp_server/readme.md +86 -0
  20. pookiepy-0.0.12/examples/mcp_server/run_example.py +96 -0
  21. pookiepy-0.0.12/examples/readme.md +3 -0
  22. pookiepy-0.0.12/examples/watchdog/__init__.py +0 -0
  23. pookiepy-0.0.12/examples/watchdog/readme.md +42 -0
  24. pookiepy-0.0.12/examples/watchdog/server_watchdog.py +160 -0
  25. pookiepy-0.0.12/examples/watchdog/watchdog_ui.py +1016 -0
  26. pookiepy-0.0.12/pdm_build.py +49 -0
  27. pookiepy-0.0.12/pookiepy/__init__.py +1 -0
  28. pookiepy-0.0.12/pookiepy/__main__.py +675 -0
  29. pookiepy-0.0.12/pookiepy/assets/HOW_TO.md +473 -0
  30. pookiepy-0.0.12/pookiepy/baseclient.py +647 -0
  31. pookiepy-0.0.12/pookiepy/baseserver.py +491 -0
  32. pookiepy-0.0.12/pookiepy/custom_interface.py +200 -0
  33. pookiepy-0.0.12/pookiepy/data_register.py +204 -0
  34. pookiepy-0.0.12/pookiepy/exceptions.py +25 -0
  35. pookiepy-0.0.12/pookiepy/logger.py +169 -0
  36. pookiepy-0.0.12/pookiepy/message.proto +62 -0
  37. pookiepy-0.0.12/pookiepy/message_pb2.py +50 -0
  38. pookiepy-0.0.12/pookiepy/message_pb2.pyi +79 -0
  39. pookiepy-0.0.12/pookiepy/message_pb2_grpc.py +101 -0
  40. pookiepy-0.0.12/pookiepy/schema_version.py +28 -0
  41. pookiepy-0.0.12/pookiepy/timer.py +302 -0
  42. pookiepy-0.0.12/pookiepy/tools.py +218 -0
  43. pookiepy-0.0.12/pyproject.toml +59 -0
  44. pookiepy-0.0.12/readme.md +437 -0
  45. pookiepy-0.0.12/requirements.txt +6 -0
  46. pookiepy-0.0.12/tests/__init__.py +7 -0
  47. pookiepy-0.0.12/tests/integration/__init__.py +0 -0
  48. pookiepy-0.0.12/tests/integration/_interface.py +48 -0
  49. pookiepy-0.0.12/tests/integration/_server_base.py +106 -0
  50. pookiepy-0.0.12/tests/integration/basic/__init__.py +0 -0
  51. pookiepy-0.0.12/tests/integration/basic/clients_basic.py +70 -0
  52. pookiepy-0.0.12/tests/integration/basic/server_basic.py +18 -0
  53. pookiepy-0.0.12/tests/integration/broadcast/__init__.py +0 -0
  54. pookiepy-0.0.12/tests/integration/broadcast/clients_broadcast.py +112 -0
  55. pookiepy-0.0.12/tests/integration/broadcast/server_broadcast.py +72 -0
  56. pookiepy-0.0.12/tests/integration/compression/__init__.py +0 -0
  57. pookiepy-0.0.12/tests/integration/compression/clients_compression.py +74 -0
  58. pookiepy-0.0.12/tests/integration/compression/server_compression.py +29 -0
  59. pookiepy-0.0.12/tests/integration/config_client/__init__.py +0 -0
  60. pookiepy-0.0.12/tests/integration/config_client/clients_config_client.py +117 -0
  61. pookiepy-0.0.12/tests/integration/config_client/server_config_client.py +77 -0
  62. pookiepy-0.0.12/tests/integration/custom_interface/__init__.py +0 -0
  63. pookiepy-0.0.12/tests/integration/custom_interface/_proto_setup.py +52 -0
  64. pookiepy-0.0.12/tests/integration/custom_interface/clients_custom_interface.py +67 -0
  65. pookiepy-0.0.12/tests/integration/custom_interface/custom_if/.gitignore +3 -0
  66. pookiepy-0.0.12/tests/integration/custom_interface/custom_if/message.proto +52 -0
  67. pookiepy-0.0.12/tests/integration/custom_interface/server_custom_interface.py +29 -0
  68. pookiepy-0.0.12/tests/integration/exception_handling/__init__.py +0 -0
  69. pookiepy-0.0.12/tests/integration/exception_handling/clients_exception_handling.py +123 -0
  70. pookiepy-0.0.12/tests/integration/exception_handling/server_exception_handling.py +24 -0
  71. pookiepy-0.0.12/tests/integration/high_fire/__init__.py +0 -0
  72. pookiepy-0.0.12/tests/integration/high_fire/clients_high_fire.py +114 -0
  73. pookiepy-0.0.12/tests/integration/high_fire/server_high_fire.py +11 -0
  74. pookiepy-0.0.12/tests/integration/history/__init__.py +0 -0
  75. pookiepy-0.0.12/tests/integration/history/clients_history.py +116 -0
  76. pookiepy-0.0.12/tests/integration/history/server_history.py +38 -0
  77. pookiepy-0.0.12/tests/integration/multi_client_request_response/clients_multi_client_request_response.py +132 -0
  78. pookiepy-0.0.12/tests/integration/multi_client_request_response/server_multi_client_request_response.py +65 -0
  79. pookiepy-0.0.12/tests/integration/password/__init__.py +0 -0
  80. pookiepy-0.0.12/tests/integration/password/clients_password.py +115 -0
  81. pookiepy-0.0.12/tests/integration/password/server_password.py +46 -0
  82. pookiepy-0.0.12/tests/integration/payload_limits_sweep/__init__.py +0 -0
  83. pookiepy-0.0.12/tests/integration/payload_limits_sweep/clients_payload_limits_sweep.py +728 -0
  84. pookiepy-0.0.12/tests/integration/payload_limits_sweep/server_payload_limits_sweep.py +62 -0
  85. pookiepy-0.0.12/tests/integration/request_response/__init__.py +0 -0
  86. pookiepy-0.0.12/tests/integration/request_response/clients_request_response.py +168 -0
  87. pookiepy-0.0.12/tests/integration/request_response/server_request_response.py +17 -0
  88. pookiepy-0.0.12/tests/integration/run_integration_tests.py +316 -0
  89. pookiepy-0.0.12/tests/integration/server_disconnect/__init__.py +0 -0
  90. pookiepy-0.0.12/tests/integration/server_disconnect/clients_server_disconnect.py +85 -0
  91. pookiepy-0.0.12/tests/integration/server_disconnect/server_server_disconnect.py +50 -0
  92. pookiepy-0.0.12/tests/integration/server_off/__init__.py +0 -0
  93. pookiepy-0.0.12/tests/integration/server_off/clients_server_off.py +46 -0
  94. pookiepy-0.0.12/tests/integration/ssl_certificate_handling/clients_ssl_certificate_handling.py +110 -0
  95. pookiepy-0.0.12/tests/integration/ssl_certificate_handling/server_ssl_certificate_handling.py +79 -0
  96. pookiepy-0.0.12/tests/integration/static_data/__init__.py +0 -0
  97. pookiepy-0.0.12/tests/integration/static_data/clients_static_data.py +81 -0
  98. pookiepy-0.0.12/tests/integration/static_data/server_static_data.py +99 -0
  99. pookiepy-0.0.12/tests/integration/timer/__init__.py +0 -0
  100. pookiepy-0.0.12/tests/integration/timer/clients_timer.py +111 -0
  101. pookiepy-0.0.12/tests/integration/timer/server_timer.py +11 -0
  102. pookiepy-0.0.12/tests/integration/wait_for_clients/__init__.py +0 -0
  103. pookiepy-0.0.12/tests/integration/wait_for_clients/clients_wait_for_clients.py +126 -0
  104. pookiepy-0.0.12/tests/integration/wait_for_clients/server_wait_for_clients.py +92 -0
  105. pookiepy-0.0.12/tests/test_base_client.py +322 -0
  106. pookiepy-0.0.12/tests/test_base_server.py +249 -0
  107. pookiepy-0.0.12/tests/test_cli_installed.py +450 -0
  108. pookiepy-0.0.12/tests/test_data_register.py +225 -0
  109. pookiepy-0.0.12/tests/test_logger.py +169 -0
  110. pookiepy-0.0.12/tests/test_tools.py +252 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Fabian Krumm
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,460 @@
1
+ Metadata-Version: 2.1
2
+ Name: pookiepy
3
+ Version: 0.0.12
4
+ Summary: pookiepy (grpc + hook) is an asynchronous Python gRPC bidirectional-streaming framework. Subclass BaseServer/BaseClient, override hooks --- the base handles all gRPC plumbing.
5
+ Author-Email: Fabian Krumm <ghub_fk@gmx.de>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Classifier: Topic :: System :: Networking
15
+ Project-URL: homepage, https://github.com/fwkrumm/pookiepy
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: grpcio>=1.81.0
18
+ Requires-Dist: grpcio-tools>=1.81.0
19
+ Requires-Dist: protobuf>=6.0.0
20
+ Requires-Dist: coloredlogs>=15.0
21
+ Requires-Dist: psutil>=5.0.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # pookiepy
25
+
26
+ **pookiepy** is a Python framework for building asynchronous gRPC bidirectional-streaming services. Subclass `BaseServer` and `BaseClient`, override the hooks you need --- the framework handles all gRPC plumbing.
27
+
28
+ [![PyPI](https://img.shields.io/pypi/v/pookiepy)](https://pypi.org/project/pookiepy/)
29
+ [![Python](https://img.shields.io/pypi/pyversions/pookiepy)](https://pypi.org/project/pookiepy/)
30
+ [![License](https://img.shields.io/badge/license-BSD%203--Clause-blue)](https://github.com/fwkrumm/pookiepy/blob/master/LICENSE.txt)
31
+
32
+ > **Status: Work in Progress.**
33
+ > The project is open source and will remain open source.
34
+ > Treat with caution. If you depend on it, **pin your version**.
35
+ > Semantic versioning will only begin with the first official release, starting at version **1.0.0**.
36
+
37
+ ---
38
+
39
+ ## Table of Contents
40
+
41
+ - [Disclaimer](#disclaimer)
42
+ - [When to Use and When Not to Use pookiepy](#when-to-use-and-when-not-to-use-pookiepy)
43
+ - [Requirements](#requirements)
44
+ - [Installation](#installation)
45
+ - [From PyPI](#from-pypi)
46
+ - [From Source](#from-source)
47
+ - [Quick Start](#quick-start)
48
+ - [Parameters](#parameters)
49
+ - [Minimal Examples](#minimal-examples)
50
+ - [Examples](#examples)
51
+ - [Testing](#testing)
52
+ - [Extend default Configuration](#extend-default-configuration)
53
+ - [Regenerating the gRPC Interface](#regenerating-the-grpc-interface)
54
+ - [ToDos and Roadmap](#todos-and-roadmap)
55
+ - [Known Issues and Troubleshooting](#known-issues-and-troubleshooting)
56
+ - [Contributing](#contributing)
57
+ - [License](#license)
58
+ - [Release History](#release-history)
59
+
60
+ ---
61
+ <a name="disclaimer"></a>
62
+ <a id="disclaimer"></a>
63
+
64
+ ## Disclaimer
65
+
66
+ Core architecture and design were created by a human developer. AI was used extensively for unit and integration test creation, examples, documentation, refinements, and selected code sections. Core logic was human-reviewed, but the full test suite has not been fully audited --- AI-introduced oversights may still exist. Please report any issues you find.
67
+
68
+ This software is provided **"as is"**, without warranty of any kind. The developer is not responsible for any damage, data loss, security vulnerabilities, or other issues that may arise from using this software. **You use it at your own risk.** See [LICENSE.txt](https://github.com/fwkrumm/pookiepy/blob/master/LICENSE.txt) for the full BSD 3-Clause terms.
69
+
70
+
71
+ ---
72
+ <a name="when-to-use-and-when-not-to-use-pookiepy"></a>
73
+ <a id="when-to-use-and-when-not-to-use-pookiepy"></a>
74
+ ## When to Use and When Not to Use pookiepy
75
+
76
+ ### When to Use pookiepy
77
+ - You need a simple, Python-based gRPC bidirectional streaming server and client.
78
+ - You want a data exchange blueprint for developers or AI agents to build on top of.
79
+ - You want a framework that can be extended with custom hooks for specific events.
80
+ - You want to distribute clients to many different machines (e.g. voice recorder, voice to text, text to LLM, and vice versa until the final response is replayed)
81
+
82
+ **Example --- four clients on four machines, all routed through one pookiepy server:**
83
+
84
+ > 💡 Diagram requires the [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) extension to render in VS Code.
85
+
86
+ ```mermaid
87
+ flowchart LR
88
+ subgraph M1["📦 Machine 1"]
89
+ VR["🎤 Voice Recorder"]
90
+ end
91
+ subgraph M2["📦 Machine 2"]
92
+ STT["📝 Speech-to-Text"]
93
+ end
94
+ subgraph M3["📦 Machine 3"]
95
+ LLM["🤖 LLM Processor"]
96
+ end
97
+ subgraph M4["📦 Machine 4"]
98
+ RP["🔊 Voice Replay"]
99
+ end
100
+
101
+ SRV(["⚙️ pookiepy Server"])
102
+
103
+ VR -->|"① audio"| SRV
104
+ SRV -->|"① audio"| STT
105
+ STT -->|"② transcript"| SRV
106
+ SRV -->|"② transcript"| LLM
107
+ LLM -->|"③ llm_response"| SRV
108
+ SRV -->|"③ llm_response"| RP
109
+ ```
110
+
111
+ ### When Not to Use pookiepy
112
+ - When you need a very large number of clients; the threading model may introduce overhead.
113
+ - When you need direct peer-to-peer communication without a server intermediary; pookiepy routes all messages through a central server.
114
+ - You want a framework that supports multiple programming languages out of the box; pookiepy is (currently) Python-only.
115
+
116
+ ---
117
+ <a name="requirements"></a>
118
+ <a id="requirements"></a>
119
+
120
+ ## Requirements
121
+
122
+ - Python 3.10 or later
123
+ - A dedicated virtual environment is **strongly recommended** --- gRPC version conflicts with other packages are common when using pookiepy.
124
+
125
+ ---
126
+ <a name="installation"></a>
127
+ <a id="installation"></a>
128
+
129
+ ## Installation
130
+
131
+ <a name="from-pypi"></a>
132
+ <a id="from-pypi"></a>
133
+ ### From PyPI
134
+
135
+ ```bash
136
+ pip install pookiepy
137
+ ```
138
+
139
+ <a name="from-source"></a>
140
+ <a id="from-source"></a>
141
+ ### From Source
142
+
143
+ ```bash
144
+ git clone https://github.com/fwkrumm/pookiepy.git
145
+ cd pookiepy
146
+ pip install -e .
147
+ ```
148
+
149
+ ---
150
+ <a name="quick-start"></a>
151
+ <a id="quick-start"></a>
152
+
153
+ ## Quick Start
154
+
155
+ Refer to [HOW_TO.md](pookiepy/assets/HOW_TO.md) for the full API reference and code examples.
156
+ Alternatively run
157
+
158
+ ```bash
159
+ python -m pookiepy --generate-skeletons
160
+ ```
161
+
162
+ to generate a very basic server and client skeleton in the current directory.
163
+ Use
164
+
165
+ ```bash
166
+ python -m pookiepy --generate-interface-with-skeletons
167
+ ```
168
+
169
+ to generate the skeletons along with a copy of the `message.proto` interface file in the current directory to modify which is then used by the skeletons.
170
+
171
+ ---
172
+
173
+ <a name="parameters"></a>
174
+ <a id="parameters"></a>
175
+
176
+ ## Parameters
177
+
178
+ You can print the following text via `python -m pookiepy --help`:
179
+
180
+ ```bash
181
+ usage: python -m pookiepy [-h] [--generate] [--generate-skeletons] [--generate-server] [--generate-client] [--generate-how-to] [--generate-interface] [--generate-interface-with-skeletons]
182
+
183
+ pookiepy scaffolding tool.
184
+
185
+ Generates skeleton server/client files and the HOW_TO reference
186
+ document into the current working directory.
187
+
188
+ options:
189
+ -h, --help show this help message and exit
190
+ --generate Generate server_skeleton.py, client_skeleton.py, and HOW_TO.md
191
+ --generate-skeletons Generate server_skeleton.py and client_skeleton.py
192
+ --generate-server Generate server_skeleton.py only
193
+ --generate-client Generate client_skeleton.py only
194
+ --generate-how-to Copy HOW_TO.md into the current directory
195
+ --generate-interface Copy message.proto into the current directory and print customisation instructions
196
+ --generate-interface-with-skeletons
197
+ Copy message.proto and write server_skeleton.py + client_skeleton.py that load the custom interface at startup via compile_and_register()
198
+
199
+ examples:
200
+ python -m pookiepy --generate # skeleton + HOW_TO
201
+ python -m pookiepy --generate-skeletons # server + client only
202
+ python -m pookiepy --generate-server # server only
203
+ python -m pookiepy --generate-client # client only
204
+ python -m pookiepy --generate-how-to # HOW_TO.md only
205
+ python -m pookiepy --generate-interface # message.proto + instructions
206
+ python -m pookiepy --generate-interface-with-skeletons # proto + matching skeletons
207
+ ```
208
+
209
+
210
+
211
+
212
+ ---
213
+ <a name="minimal-examples"></a>
214
+ <a id="minimal-examples"></a>
215
+
216
+ ## Minimal Examples
217
+
218
+ ### Ultra-minimal --- no subclassing required
219
+
220
+ The simplest possible working setup: start a server, connect two clients, exchange a message.
221
+ Everything runs in a single script --- no subclassing or hook overrides needed.
222
+
223
+ ```python
224
+ # example_minimal.py
225
+ import threading
226
+ from pookiepy.baseserver import BaseServer
227
+ from pookiepy.baseclient import BaseClient
228
+ from pookiepy.tools import generate_message
229
+
230
+ # start the server in a background thread
231
+ server = BaseServer(port=50051, name="server")
232
+ threading.Thread(target=server.serve_forever, daemon=True).start()
233
+
234
+ # both clients declare the same channel name
235
+ # fan-out skips the sender, so client_b receives what client_a sends
236
+ client_a = BaseClient(port=50051, name="A", provides=["ping"], requires=["ping"])
237
+ client_b = BaseClient(port=50051, name="B", provides=["ping"], requires=["ping"])
238
+
239
+ client_a.send_data(generate_message("ping", byte_payload=b"hello"))
240
+
241
+ msg = client_b.get_data(timeout=5.0)
242
+ client_a.logger.info(msg.payload.bytePayload) # b"hello"
243
+ client_b.logger.info(msg.payload.bytePayload) # b"hello"
244
+
245
+ client_a.disconnect()
246
+ client_b.disconnect()
247
+ server.shutdown()
248
+ ```
249
+
250
+ ### Request / response --- subclass with hooks
251
+
252
+ For real workloads, subclass `BaseServer` to control routing and `BaseClient` to react to messages
253
+ via the `on_receive` hook.
254
+
255
+ Design note (important): request/response in pookiepy is intentionally minimalistic.
256
+ There is no dedicated `request()` helper in the core API by default; correlation is done via
257
+ `messageId` and `responseToId` in normal hook/polling flow. This keeps the framework lean,
258
+ transparent, and robust for mixed traffic patterns.
259
+
260
+ Need full reference flow?
261
+ - `tests/integration/request_response/server_request_response.py`
262
+ - `tests/integration/request_response/clients_request_response.py`
263
+
264
+ **`server.py`**
265
+
266
+ ```python
267
+ from pookiepy.baseserver import BaseServer, Peer
268
+ from pookiepy.tools import generate_message
269
+ import pookiepy.message_pb2 as pb2
270
+
271
+
272
+ class EchoServer(BaseServer):
273
+ def __init__(self):
274
+ super().__init__(port=50051, name="echo-server")
275
+
276
+ def on_receive(self, peer: Peer, request: pb2.Message) -> bool:
277
+ if request.metaInfo.messageName == "request":
278
+ reply = generate_message("response", byte_payload=request.payload.bytePayload)
279
+ self._data_register.add_data_for_message_name(
280
+ peer.client_id, "response", reply,
281
+ target_client_id=peer.client_id, # unicast back to sender
282
+ )
283
+ return False # skip default fan-out; routing handled above
284
+ return True
285
+
286
+
287
+ EchoServer().serve_forever()
288
+ ```
289
+
290
+ **`client.py`**
291
+
292
+ ```python
293
+ from pookiepy.baseclient import BaseClient
294
+ from pookiepy.tools import generate_message
295
+ import pookiepy.message_pb2 as pb2
296
+
297
+
298
+ class EchoClient(BaseClient):
299
+ def __init__(self):
300
+ super().__init__(port=50051, name="echo-client",
301
+ provides=["request"], requires=["response"])
302
+
303
+ def on_receive(self, data: pb2.Message):
304
+ print(f"Server replied: {data.payload.bytePayload.decode()}")
305
+
306
+
307
+ client = EchoClient()
308
+ client.send_data(generate_message("request", byte_payload=b"hello, pookiepy!"))
309
+ client.spin(timeout=5.0) # calls on_receive() per message; returns on timeout/disconnect
310
+ client.disconnect()
311
+ ```
312
+
313
+ Run the server first, then the client:
314
+
315
+ ```bash
316
+ # terminal 1
317
+ python server.py
318
+
319
+ # terminal 2
320
+ python client.py
321
+ ```
322
+
323
+ ---
324
+ <a name="examples"></a>
325
+ <a id="examples"></a>
326
+
327
+ ## Examples
328
+
329
+ Runnable examples are available in two locations:
330
+
331
+ - `examples/` --- self-contained, scenario-focused examples
332
+ - `tests/integration/` --- integration test scenarios covering a broad range of use cases
333
+
334
+ Run them on a machine with adequate resources; some scenarios are resource-intensive.
335
+
336
+ ---
337
+ <a name="testing"></a>
338
+ <a id="testing"></a>
339
+
340
+ ## Testing
341
+
342
+ Install dev dependencies and run the unit tests:
343
+
344
+ ```bash
345
+ pip install -r requirements_dev.txt
346
+ python -m unittest discover -s tests
347
+ ```
348
+
349
+ Integration tests are in `tests/integration/` and can be run via:
350
+
351
+ ```bash
352
+ python tests/integration/run_integration_tests.py
353
+ ```
354
+
355
+ ---
356
+ <a name="extend-default-configuration"></a>
357
+ <a id="extend-default-configuration"></a>
358
+
359
+ ## Extend default Configuration
360
+
361
+ Example for a client to use the default configuration but disable proxy forwarding:
362
+
363
+
364
+ ```python
365
+ from pookiepy.baseclient import BaseClient, ClientConfig
366
+
367
+ class TestClient(BaseClient):
368
+ def __init__(self):
369
+ config = ClientConfig()
370
+ config.grpc_options += [("grpc.enable_http_proxy", 0)]
371
+ super().__init__(port=50051, name="test-client", config=config)
372
+
373
+ ```
374
+
375
+ ---
376
+ <a name="regenerating-the-grpc-interface"></a>
377
+ <a id="regenerating-the-grpc-interface"></a>
378
+
379
+
380
+ ## Regenerating the gRPC Interface
381
+
382
+ If you modify `pookiepy/message.proto` after cloning the repository, regenerate the Python bindings with:
383
+
384
+ ```bash
385
+ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. --pyi_out=. pookiepy/message.proto
386
+ ```
387
+
388
+ Note that all clients which connect to a server have to use the same proto schema version i.e. the same proto file. The different signals for the clients must be used in substructures:
389
+
390
+ ```proto
391
+ message Payload {
392
+ // For client A
393
+ SomeTypeA payloadClientA = 1;
394
+
395
+ // For client B
396
+ SomeTypeB payloadClientB = 2;
397
+
398
+ ...
399
+ }
400
+
401
+ ```
402
+
403
+ ---
404
+ <a name="todos-and-roadmap"></a>
405
+ <a id="todos-and-roadmap"></a>
406
+
407
+ ## ToDos and Roadmap
408
+
409
+ ### Performance & Stability
410
+ - Evaluate replacing the threading model with `asyncio` if the performance gain justifies the API tradeoff.
411
+ - Verify behavior when connections are interrupted mid-stream; ensure no ghost threads or queue deadlocks occur.
412
+
413
+ ### Planned Features
414
+ - Multi-language client example (e.g., C++ or Rust).
415
+
416
+ ---
417
+ <a name="known-issues-and-troubleshooting"></a>
418
+ <a id="known-issues-and-troubleshooting"></a>
419
+
420
+ ## Known Issues and Troubleshooting
421
+
422
+ TBD
423
+
424
+ ---
425
+ <a name="contributing"></a>
426
+ <a id="contributing"></a>
427
+
428
+ ## Contributing
429
+
430
+ Contributions are welcome. Please open an issue first for major changes so the approach can be discussed. For bug fixes and small improvements, a pull request is sufficient.
431
+
432
+ ---
433
+ <a name="license"></a>
434
+ <a id="license"></a>
435
+
436
+ ## License
437
+
438
+ BSD 3-Clause --- see [LICENSE.txt](https://github.com/fwkrumm/pookiepy/blob/master/LICENSE.txt).
439
+
440
+ ---
441
+ <a name="release-history"></a>
442
+ <a id="release-history"></a>
443
+
444
+ ## Release History
445
+
446
+
447
+ | Version / Git Tag on Master | Description |
448
+ |----------------------------|-------------|
449
+ | 0.0.1 | Unpublished. |
450
+ | 0.0.2 | Initial public release. |
451
+ | 0.0.3 | Add ms timestamp resolution to log output and minor adjustments to readme. |
452
+ | 0.0.4 | Add executor for server and wait for shutdown. |
453
+ | 0.0.5 | Fix readme on pypi page. |
454
+ | 0.0.6 | Update how-to markdown to include custom interface description. |
455
+ | 0.0.7 | Fix race condition which allowed clients to put data before welcome message. |
456
+ | 0.0.8 | Fix link in readme for pypi page. |
457
+ | 0.0.9 | Minor performance adjustments, adding compression parameter, changing logging parameters. |
458
+ | 0.0.10 | Add on_data_yield hook, added responseToId field, more explicit logging for startup |
459
+ | 0.0.11 | Add deprecation warning because of project rename. |
460
+ | 0.0.12 | Project renamed to pookiepy |
File without changes
@@ -0,0 +1,16 @@
1
+ """Minimal gRPC server for the interactive streaming example."""
2
+ from pookiepy.baseserver import BaseServer
3
+
4
+
5
+ class GrpcServer(BaseServer):
6
+ """
7
+ Minimal server for interactive streaming example.
8
+ """
9
+
10
+ def __init__(self, port: int = 49999):
11
+ super().__init__(port)
12
+
13
+
14
+ if __name__ == "__main__":
15
+ s = GrpcServer(49999)
16
+ s.serve_forever()
@@ -0,0 +1,100 @@
1
+ """LM Studio proxy client: receives lm_request, streams lm_response_stream chunks."""
2
+ import threading
3
+ import time
4
+
5
+ from pookiepy import message_pb2
6
+ from pookiepy.baseclient import BaseClient
7
+ from pookiepy.tools import json_to_struct, struct_to_json
8
+ from examples.interactive_streaming import _lm_http
9
+ from examples.interactive_streaming._lm_http import (
10
+ _iter_stream, _fetch_sync, _offline_stream, make_http_session,
11
+ )
12
+
13
+
14
+ try:
15
+ import requests
16
+ except ImportError:
17
+ requests = None
18
+
19
+
20
+ class LMProxyClient(BaseClient):
21
+ """Proxy: receives lm_request, queries LM Studio, streams lm_response_stream chunks."""
22
+
23
+ def __init__(self, name: str, port: int, lmstudio_base: str | None = None,
24
+ model: str = "gemma-4e2b"):
25
+ super().__init__(port, name=name, provides=["lm_response_stream"], requires=["lm_request"])
26
+ self.lmstudio_base = lmstudio_base or "http://127.0.0.1:1234/v1"
27
+ self.model = model
28
+
29
+ if requests:
30
+ sess = make_http_session()
31
+ if sess:
32
+ _lm_http._session = sess
33
+ self.logger.debug("persistent HTTP session created")
34
+ else:
35
+ self.logger.warning("requests not installed --- HTTP calls will fail")
36
+
37
+ threading.Thread(target=self.spin_forever, daemon=True).start()
38
+
39
+ def _send_chunk(self, request: message_pb2.Message, text: str,
40
+ done: bool = False):
41
+ msg = message_pb2.Message(
42
+ metaInfo=message_pb2.MetaInformation(messageName="lm_response_stream"),
43
+ payload=message_pb2.Payload(
44
+ structPayload=json_to_struct({"chunk": text, "done": done})
45
+ ),
46
+ )
47
+ msg.metaInfo.responseToId = request.metaInfo.messageId
48
+ self.send_data(msg)
49
+
50
+ def _handle_request(self, request: message_pb2.Message):
51
+ """Forward an lm_request to LM Studio and stream back the response."""
52
+ try:
53
+ prompt = struct_to_json(request.payload.structPayload).get("text", "")
54
+ except (ValueError, TypeError, AttributeError):
55
+ prompt = ""
56
+
57
+ if not prompt:
58
+ self._send_chunk(request, "", done=True)
59
+ return
60
+
61
+ mid = request.metaInfo.messageId
62
+ self.logger.info("Forwarding to LM Studio %s messageId=%s", self.lmstudio_base, mid)
63
+
64
+ had_chunks = False
65
+ try:
66
+ for chunk in _iter_stream(prompt, self.lmstudio_base, self.model):
67
+ if not self.run_event.is_set():
68
+ self.logger.debug("proxy shutting down, aborting stream messageId=%s", mid)
69
+ return
70
+ self._send_chunk(request, chunk)
71
+ had_chunks = True
72
+ except OSError:
73
+ self.logger.exception("stream failed messageId=%s", mid)
74
+
75
+ if not had_chunks:
76
+ try:
77
+ self._send_chunk(request, _fetch_sync(prompt, self.lmstudio_base, self.model))
78
+ had_chunks = True
79
+ except OSError:
80
+ self.logger.exception("sync fetch failed messageId=%s", mid)
81
+
82
+ if not had_chunks:
83
+ self.logger.warning("LM Studio unreachable, using offline stream messageId=%s", mid)
84
+ for chunk in _offline_stream(prompt):
85
+ self._send_chunk(request, chunk)
86
+
87
+ self._send_chunk(request, "", done=True)
88
+
89
+ def on_receive(self, data: message_pb2.Message) -> bool:
90
+ threading.Thread(target=self._handle_request, args=(data,), daemon=True).start()
91
+ return True
92
+
93
+
94
+ if __name__ == "__main__":
95
+ proxy = LMProxyClient("lm-proxy", 49999)
96
+ try:
97
+ while True:
98
+ time.sleep(1)
99
+ except KeyboardInterrupt:
100
+ proxy.disconnect()