stinger-python-utils 0.1.9__py3-none-any.whl → 0.3.0__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.
@@ -16,27 +16,73 @@ class MessageCreator:
16
16
 
17
17
  @classmethod
18
18
  def signal_message(cls, topic: str, payload: BaseModel) -> Message:
19
+ return cls.binary_signal_message(
20
+ topic,
21
+ payload.model_dump_json(by_alias=True).encode("utf-8"),
22
+ "application/json",
23
+ )
24
+
25
+ @classmethod
26
+ def binary_signal_message(cls, topic: str, payload: bytes, content_type: str) -> Message:
19
27
  cls._validate_topic(topic)
20
28
  return Message(
21
29
  topic=topic,
22
- payload=payload.model_dump_json(by_alias=True).encode("utf-8"),
30
+ payload=payload,
23
31
  qos=1,
24
32
  retain=False,
25
- content_type="application/json",
33
+ content_type=content_type,
34
+ )
35
+
36
+ @classmethod
37
+ def command_message(cls, topic: str, payload: BaseModel, qos: int = 2) -> Message:
38
+ return cls.binary_command_message(
39
+ topic,
40
+ payload.model_dump_json(by_alias=True).encode("utf-8"),
41
+ "application/json",
42
+ qos=qos,
43
+ )
44
+
45
+ @classmethod
46
+ def binary_command_message(
47
+ cls, topic: str, payload: bytes, content_type: str, qos: int = 2
48
+ ) -> Message:
49
+ """
50
+ A command is a signal in reverse: the client publishes it to the server and never
51
+ learns whether it was acted on. There is therefore no response topic and no
52
+ correlation data on the message.
53
+ """
54
+ cls._validate_topic(topic)
55
+ return Message(
56
+ topic=topic,
57
+ payload=payload,
58
+ qos=qos,
59
+ retain=False,
60
+ content_type=content_type,
26
61
  )
27
62
 
28
63
  @classmethod
29
64
  def status_message(
30
- cls, topic, status_message: BaseModel, expiry_seconds: int
65
+ cls, topic: str, status_message: BaseModel, expiry_seconds: int
66
+ ) -> Message:
67
+ return cls.binary_status_message(
68
+ topic,
69
+ status_message.model_dump_json(by_alias=True).encode("utf-8"),
70
+ "application/json",
71
+ expiry_seconds=expiry_seconds,
72
+ )
73
+
74
+ @classmethod
75
+ def binary_status_message(
76
+ cls, topic: str, payload: bytes, content_type: str, expiry_seconds: int
31
77
  ) -> Message:
32
78
  cls._validate_topic(topic)
33
79
  return Message(
34
80
  topic=topic,
35
- payload=status_message.model_dump_json(by_alias=True).encode("utf-8"),
81
+ payload=payload,
36
82
  qos=1,
37
83
  retain=True,
38
84
  message_expiry_interval=expiry_seconds,
39
- content_type="application/json",
85
+ content_type=content_type,
40
86
  )
41
87
 
42
88
  @classmethod
@@ -46,9 +92,14 @@ class MessageCreator:
46
92
  return_code: Union[int, MethodReturnCode],
47
93
  correlation_id: Union[str, bytes, None] = None,
48
94
  debug_info: Optional[str] = None,
95
+ content_type: str = "application/json",
49
96
  ) -> Message:
50
97
  """
51
98
  This could be used for a response to a request, but where there was an error fulfilling the request.
99
+
100
+ An error carries no data -- the return code and any debug info travel in MQTT user
101
+ properties -- so the body is an empty JSON object for a JSON response and empty
102
+ bytes for any other content type, since a protobuf consumer would reject "{}".
52
103
  """
53
104
  cls._validate_topic(topic)
54
105
  rc = (
@@ -58,7 +109,7 @@ class MessageCreator:
58
109
  )
59
110
  msg_obj = Message(
60
111
  topic=topic,
61
- payload=b"{}",
112
+ payload=b"{}" if content_type == "application/json" else b"",
62
113
  qos=1,
63
114
  retain=False,
64
115
  correlation_data=(
@@ -67,7 +118,7 @@ class MessageCreator:
67
118
  else correlation_id
68
119
  ),
69
120
  user_properties={"ReturnCode": str(rc)},
70
- content_type="application/json",
121
+ content_type=content_type,
71
122
  )
72
123
  if (
73
124
  debug_info is not None and msg_obj.user_properties is not None
@@ -86,19 +137,32 @@ class MessageCreator:
86
137
  """
87
138
  This could be used for a successful response to a request.
88
139
  """
89
- cls._validate_topic(response_topic, "response_topic")
90
140
  if isinstance(response_obj, BaseModel):
91
141
  payload = response_obj.model_dump_json(by_alias=True).encode("utf-8")
92
142
  elif isinstance(response_obj, str):
93
143
  payload = response_obj.encode("utf-8")
94
144
  else:
95
145
  payload = response_obj
146
+ return cls.binary_response_message(
147
+ response_topic, payload, "application/json", return_code, correlation_id
148
+ )
149
+
150
+ @classmethod
151
+ def binary_response_message(
152
+ cls,
153
+ response_topic: str,
154
+ payload: bytes,
155
+ content_type: str,
156
+ return_code: Union[int, MethodReturnCode],
157
+ correlation_id: Union[str, bytes, None] = None,
158
+ ) -> Message:
159
+ cls._validate_topic(response_topic, "response_topic")
96
160
  rc = (
97
161
  return_code.value
98
162
  if isinstance(return_code, MethodReturnCode)
99
163
  else return_code
100
164
  )
101
- msg_obj = Message(
165
+ return Message(
102
166
  topic=response_topic,
103
167
  payload=payload,
104
168
  qos=1,
@@ -109,9 +173,8 @@ class MessageCreator:
109
173
  else correlation_id
110
174
  ),
111
175
  user_properties={"ReturnCode": str(rc)},
112
- content_type="application/json",
176
+ content_type=content_type,
113
177
  )
114
- return msg_obj
115
178
 
116
179
  @classmethod
117
180
  def property_state_message(
@@ -120,13 +183,24 @@ class MessageCreator:
120
183
  """
121
184
  Creates a retained message representing the state/value of a property.
122
185
  """
186
+ return cls.binary_property_state_message(
187
+ topic,
188
+ state_obj.model_dump_json(by_alias=True).encode("utf-8"),
189
+ "application/json",
190
+ state_version,
191
+ )
192
+
193
+ @classmethod
194
+ def binary_property_state_message(
195
+ cls, topic: str, payload: bytes, content_type: str, state_version: Optional[int] = None
196
+ ) -> Message:
123
197
  cls._validate_topic(topic)
124
198
  msg_obj = Message(
125
199
  topic=topic,
126
- payload=state_obj.model_dump_json(by_alias=True).encode("utf-8"),
200
+ payload=payload,
127
201
  qos=1,
128
202
  retain=True,
129
- content_type="application/json",
203
+ content_type=content_type,
130
204
  )
131
205
  if state_version is not None:
132
206
  msg_obj.user_properties = {"PropertyVersion": str(state_version)}
@@ -137,37 +211,57 @@ class MessageCreator:
137
211
  cls,
138
212
  topic: str,
139
213
  property_obj: BaseModel,
140
- version: str,
214
+ version: Optional[int],
141
215
  response_topic: str,
142
216
  correlation_id: Union[str, bytes, None] = None,
143
217
  ) -> Message:
144
218
  """
145
219
  Creates a message representing a request to update a property.
146
220
  """
221
+ return cls.binary_property_update_request_message(
222
+ topic,
223
+ property_obj.model_dump_json(by_alias=True).encode("utf-8"),
224
+ "application/json",
225
+ version,
226
+ response_topic,
227
+ correlation_id,
228
+ )
229
+
230
+ @classmethod
231
+ def binary_property_update_request_message(
232
+ cls,
233
+ topic: str,
234
+ payload: bytes,
235
+ content_type: str,
236
+ version: Optional[int],
237
+ response_topic: str,
238
+ correlation_id: Union[str, bytes, None] = None,
239
+ ) -> Message:
147
240
  cls._validate_topic(topic)
148
241
  cls._validate_topic(response_topic, "response_topic")
149
- msg_obj = Message(
242
+ msg = Message(
150
243
  topic=topic,
151
- payload=property_obj.model_dump_json(by_alias=True).encode("utf-8"),
244
+ payload=payload,
152
245
  qos=1,
153
246
  retain=False,
154
- content_type="application/json",
247
+ content_type=content_type,
155
248
  response_topic=response_topic,
156
249
  correlation_data=(
157
250
  correlation_id.encode("utf-8")
158
251
  if isinstance(correlation_id, str)
159
252
  else correlation_id
160
253
  ),
161
- user_properties={"PropertyVersion": str(version)},
162
254
  )
163
- return msg_obj
255
+ if version is not None:
256
+ msg.user_properties = {"PropertyVersion": str(version)}
257
+ return msg
164
258
 
165
259
  @classmethod
166
260
  def property_response_message(
167
261
  cls,
168
262
  response_topic: str,
169
263
  property_obj: BaseModel,
170
- version: str,
264
+ version: Optional[int],
171
265
  return_code: Union[int, MethodReturnCode],
172
266
  correlation_id: Union[str, bytes, None] = None,
173
267
  debug_info: Optional[str] = None,
@@ -175,6 +269,27 @@ class MessageCreator:
175
269
  """
176
270
  Creates a message representing a response to a property update request.
177
271
  """
272
+ return cls.binary_property_response_message(
273
+ response_topic,
274
+ property_obj.model_dump_json(by_alias=True).encode("utf-8"),
275
+ "application/json",
276
+ version,
277
+ return_code,
278
+ correlation_id,
279
+ debug_info,
280
+ )
281
+
282
+ @classmethod
283
+ def binary_property_response_message(
284
+ cls,
285
+ response_topic: str,
286
+ payload: bytes,
287
+ content_type: str,
288
+ version: Optional[int],
289
+ return_code: Union[int, MethodReturnCode],
290
+ correlation_id: Union[str, bytes, None] = None,
291
+ debug_info: Optional[str] = None,
292
+ ) -> Message:
178
293
  cls._validate_topic(response_topic, "response_topic")
179
294
  rc = (
180
295
  return_code.value
@@ -183,10 +298,10 @@ class MessageCreator:
183
298
  )
184
299
  msg_obj = Message(
185
300
  topic=response_topic,
186
- payload=property_obj.model_dump_json(by_alias=True).encode("utf-8"),
301
+ payload=payload,
187
302
  qos=1,
188
303
  retain=False,
189
- content_type="application/json",
304
+ content_type=content_type,
190
305
  correlation_data=(
191
306
  correlation_id.encode("utf-8")
192
307
  if isinstance(correlation_id, str)
@@ -194,13 +309,13 @@ class MessageCreator:
194
309
  ),
195
310
  user_properties={
196
311
  "ReturnCode": str(rc),
197
- "PropertyVersion": str(version),
198
312
  },
199
313
  )
200
- if (
201
- debug_info is not None and msg_obj.user_properties is not None
202
- ): # user_properties should never be None here, but checking to satisfy type checker
203
- msg_obj.user_properties["DebugInfo"] = debug_info
314
+ if msg_obj.user_properties is not None: # user_properties should never be None here, but checking to satisfy type checker
315
+ if version is not None:
316
+ msg_obj.user_properties["PropertyVersion"] = str(version)
317
+ if debug_info is not None:
318
+ msg_obj.user_properties["DebugInfo"] = debug_info
204
319
  return msg_obj
205
320
 
206
321
  @classmethod
@@ -210,22 +325,38 @@ class MessageCreator:
210
325
  request_obj: BaseModel,
211
326
  response_topic: str,
212
327
  correlation_id: Union[str, bytes, None] = None,
328
+ ) -> Message:
329
+ return cls.binary_request_message(
330
+ topic,
331
+ request_obj.model_dump_json(by_alias=True).encode("utf-8"),
332
+ "application/json",
333
+ response_topic,
334
+ correlation_id,
335
+ )
336
+
337
+ @classmethod
338
+ def binary_request_message(
339
+ cls,
340
+ topic: str,
341
+ payload: bytes,
342
+ content_type: str,
343
+ response_topic: str,
344
+ correlation_id: Union[str, bytes, None] = None,
213
345
  ) -> Message:
214
346
  cls._validate_topic(topic)
215
347
  cls._validate_topic(response_topic, "response_topic")
216
348
  if correlation_id is None:
217
349
  correlation_id = str(uuid.uuid4())
218
- msg_obj = Message(
350
+ return Message(
219
351
  topic=topic,
220
- payload=request_obj.model_dump_json(by_alias=True).encode("utf-8"),
352
+ payload=payload,
221
353
  qos=1,
222
354
  retain=False,
223
355
  response_topic=response_topic,
224
- content_type="application/json",
356
+ content_type=content_type,
225
357
  correlation_data=(
226
358
  correlation_id.encode("utf-8")
227
359
  if isinstance(correlation_id, str)
228
360
  else correlation_id
229
361
  ),
230
362
  )
231
- return msg_obj
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.5
2
+ Name: stinger-python-utils
3
+ Version: 0.3.0
4
+ Summary: Common utilities for Stinger Python services.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.7
8
+ Requires-Dist: pydantic>=2.5.3
9
+ Requires-Dist: pyqttier>=0.2.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # stinger-python-utils
13
+
14
+ Shared utilities for Stinger Python services, providing convenient message creation for MQTT communication.
15
+
16
+
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ uv add stinger-python-utils
22
+ ```
23
+
24
+ ## MessageCreator
25
+
26
+ `MessageCreator` is a utility class for creating MQTT messages with standardized properties and payloads.
27
+
28
+ ### Basic Usage
29
+
30
+ ```python
31
+ from pydantic import BaseModel
32
+ from stinger_python_utils.message_creator import MessageCreator
33
+
34
+ class MyPayload(BaseModel):
35
+ name: str
36
+ value: int
37
+
38
+ payload = MyPayload(name="test", value=42)
39
+ message = MessageCreator.signal_message("my/topic", payload)
40
+ ```
41
+
42
+ ### Methods
43
+
44
+ | Method | Purpose | Return Code |
45
+ |--------|---------|-------------|
46
+ | `signal_message(topic, payload)` | Send a signal with one-time delivery | QoS 1, no retain |
47
+ | `status_message(topic, payload, expiry_seconds)` | Send status that expires | QoS 1, retained, with expiry |
48
+ | `error_response_message(topic, return_code, correlation_id, debug_info)` | Error response to a request | QoS 1, user properties: `ReturnCode` |
49
+ | `response_message(topic, payload, return_code, correlation_id)` | Successful response to a request | QoS 1, user properties: `ReturnCode` |
50
+ | `property_state_message(topic, payload, state_version)` | Publish property state | QoS 1, retained, JSON content type |
51
+ | `property_update_request_message(topic, payload, version, response_topic, correlation_id)` | Request property update | QoS 1, user property: `PropertyVersion` |
52
+ | `property_response_message(topic, payload, version, return_code, correlation_id, debug_info)` | Respond to property update | QoS 1, user properties: `ReturnCode`, `PropertyVersion` |
53
+ | `request_message(topic, payload, response_topic, correlation_id)` | Send a request (auto-generates UUID if no correlation_id) | QoS 1, auto correlation ID |
54
+
55
+ ### Example: Request/Response Pattern
56
+
57
+ ```python
58
+ from pydantic import BaseModel
59
+ from stinger_python_utils.message_creator import MessageCreator
60
+
61
+ class Request(BaseModel):
62
+ action: str
63
+
64
+ request = Request(action="start")
65
+ msg = MessageCreator.request_message(
66
+ "devices/cmd",
67
+ request,
68
+ response_topic="devices/response"
69
+ )
70
+ # Returns a Message with auto-generated correlation ID
71
+ ```
72
+
73
+ ### Example: Error Response
74
+
75
+ ```python
76
+ msg = MessageCreator.error_response_message(
77
+ "devices/response",
78
+ return_code=500,
79
+ correlation_id="req-123",
80
+ debug_info="Device not found"
81
+ )
82
+ # User properties include: ReturnCode=500, DebugInfo=Device not found
83
+ ```
84
+
@@ -0,0 +1,7 @@
1
+ stinger_python_utils/__init__.py,sha256=IjHRV0k2DNwvFrEHebmsXiBvmITE8nQUnsR07h9tVkU,7
2
+ stinger_python_utils/message_creator.py,sha256=fA7ZICVB6K2UVmq1u9bEiJ_GorlR8BnFpan3E9_ALh4,11737
3
+ stinger_python_utils/return_codes.py,sha256=AAwshvHu3SBoctwMX35k3j666J4a7DQ3V8AnR7QBjC8,5114
4
+ stinger_python_utils-0.3.0.dist-info/METADATA,sha256=10X4jhrUNe5v8cBG2RsZcYEaxY7W4GJSWOCp3eyOiIE,2769
5
+ stinger_python_utils-0.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
+ stinger_python_utils-0.3.0.dist-info/licenses/LICENSE,sha256=_T-8ExmblJbhv-1AxC6XDVEHg1JdJA-126NvRiMqS-I,1070
7
+ stinger_python_utils-0.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,20 +0,0 @@
1
- """Stinger MCP server – plugin-based MCP interface to stinger-ipc services.
2
-
3
- Public API re-exported here for convenience::
4
-
5
- from stinger_python_utils.mcp import StingerMCPPlugin, SignalDefinition, ...
6
- """
7
-
8
- from .plugin import (
9
- MethodDefinition,
10
- PropertyDefinition,
11
- SignalDefinition,
12
- StingerMCPPlugin,
13
- )
14
-
15
- __all__ = [
16
- "MethodDefinition",
17
- "PropertyDefinition",
18
- "SignalDefinition",
19
- "StingerMCPPlugin",
20
- ]
@@ -1,58 +0,0 @@
1
- """Entry-point for ``python -m stinger_python_utils.mcp``."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import asyncio
7
- import logging
8
-
9
-
10
- def main() -> None:
11
- parser = argparse.ArgumentParser(
12
- prog="stinger-mcp-server",
13
- description="Stinger MCP Server – expose stinger-ipc services over MCP",
14
- )
15
- parser.add_argument(
16
- "--transport",
17
- choices=["stdio", "sse", "streamable-http"],
18
- default="stdio",
19
- help="MCP transport to use (default: stdio)",
20
- )
21
- parser.add_argument(
22
- "--host",
23
- default="0.0.0.0",
24
- help="Bind address for SSE/streamable-http transport (default: 0.0.0.0)",
25
- )
26
- parser.add_argument(
27
- "--port",
28
- type=int,
29
- default=8000,
30
- help="Port for SSE/streamable-http transport (default: 8000)",
31
- )
32
- parser.add_argument(
33
- "--log-level",
34
- default="INFO",
35
- choices=["DEBUG", "INFO", "WARNING", "ERROR"],
36
- help="Logging level (default: INFO)",
37
- )
38
- args = parser.parse_args()
39
-
40
- logging.basicConfig(
41
- level=getattr(logging, args.log_level),
42
- format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
43
- )
44
-
45
- from .server import StingerMCPServer
46
-
47
- server = StingerMCPServer()
48
-
49
- if args.transport == "stdio":
50
- asyncio.run(server.run_stdio())
51
- elif args.transport == "sse":
52
- asyncio.run(server.run_sse(host=args.host, port=args.port))
53
- elif args.transport == "streamable-http":
54
- asyncio.run(server.run_streamable_http(host=args.host, port=args.port))
55
-
56
-
57
- if __name__ == "__main__":
58
- main()