mqttxx 2.0.3__py3-none-any.whl → 3.2.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.
mqttxx/events.py ADDED
@@ -0,0 +1,359 @@
1
+ # Event Channel 层 - 高吞吐、低耦合的事件广播通道
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import Callable, Any, Optional, overload
6
+ from dataclasses import dataclass
7
+ from loguru import logger
8
+
9
+ from .client import MQTTClient
10
+ import json
11
+
12
+
13
+ @dataclass
14
+ class EventMessage:
15
+ """事件消息(可选的结构化格式)
16
+
17
+ 这是一个**可选**的辅助工具类,用户可以选择:
18
+ 1. 使用 EventMessage 发布(带时间戳、事件类型)
19
+ 2. 直接发布原始字典(零开销)
20
+
21
+ 订阅者会根据消息是否包含 "type": "event" 自动区分
22
+
23
+ Attributes:
24
+ type: 消息类型标识(固定为 "event")
25
+ event_type: 事件类型(如 "sensor.temperature", "user.login")
26
+ data: 事件数据(任意 JSON 可序列化对象)
27
+ timestamp: Unix 时间戳(秒,自动填充)
28
+ source: 事件源(可选,发布者标识)
29
+
30
+ 示例:
31
+ # 创建事件消息
32
+ msg = EventMessage(
33
+ event_type="temperature.changed",
34
+ data={"value": 25.5, "unit": "C"},
35
+ source="sensor_001"
36
+ )
37
+
38
+ # 序列化
39
+ data = msg.to_dict()
40
+ # {
41
+ # "type": "event",
42
+ # "event_type": "temperature.changed",
43
+ # "data": {"value": 25.5, "unit": "C"},
44
+ # "timestamp": 1673456789.123,
45
+ # "source": "sensor_001"
46
+ # }
47
+ """
48
+
49
+ # 固定字段(用于区分事件消息和 RPC 消息)
50
+ type: str = "event"
51
+
52
+ # 事件核心字段
53
+ event_type: str = "" # 事件类型
54
+ data: Any = None # 事件数据
55
+
56
+ # 元数据(自动填充)
57
+ timestamp: float = 0.0 # Unix 时间戳(秒)
58
+ source: str = "" # 事件源
59
+
60
+ def __post_init__(self):
61
+ """自动填充时间戳"""
62
+ if self.timestamp == 0.0:
63
+ self.timestamp = time.time()
64
+
65
+ def to_dict(self) -> dict:
66
+ """序列化为字典
67
+
68
+ Returns:
69
+ 包含所有字段的字典
70
+ """
71
+ return {
72
+ "type": self.type,
73
+ "event_type": self.event_type,
74
+ "data": self.data,
75
+ "timestamp": self.timestamp,
76
+ "source": self.source,
77
+ }
78
+
79
+ @classmethod
80
+ def from_dict(cls, data: dict) -> "EventMessage":
81
+ """从字典反序列化
82
+
83
+ Args:
84
+ data: 包含事件字段的字典
85
+
86
+ Returns:
87
+ EventMessage 实例
88
+ """
89
+ return cls(
90
+ event_type=data.get("event_type", ""),
91
+ data=data.get("data"),
92
+ timestamp=data.get("timestamp", 0.0),
93
+ source=data.get("source", ""),
94
+ )
95
+
96
+ def encode(self) -> bytes:
97
+ """编码为 bytes(JSON 格式)
98
+
99
+ Returns:
100
+ UTF-8 编码的 JSON bytes
101
+ """
102
+ return json.dumps(self.to_dict()).encode('utf-8')
103
+
104
+ @classmethod
105
+ def decode(cls, data: bytes) -> "EventMessage":
106
+ """从 bytes 解码(JSON 格式)
107
+
108
+ Args:
109
+ data: UTF-8 编码的 JSON bytes
110
+
111
+ Returns:
112
+ EventMessage 对象
113
+
114
+ Raises:
115
+ UnicodeDecodeError: UTF-8 解码失败
116
+ json.JSONDecodeError: JSON 解析失败
117
+ """
118
+ obj = json.loads(data.decode('utf-8'))
119
+ return cls.from_dict(obj)
120
+
121
+
122
+ # 事件处理器类型定义
123
+ # 参数:(topic: str, message: dict)
124
+ EventHandler = Callable[[str, dict], Any]
125
+
126
+
127
+ class EventChannelManager:
128
+ """Event Channel 管理器 - 极薄的发布订阅层
129
+
130
+ 核心特性:
131
+ 1. 发布:零包装,直接转发到 MQTT(可选 EventMessage 格式化)
132
+ 2. 订阅:支持通配符,自动区分结构化/原始消息
133
+ 3. 过滤:基于 topic 模式匹配(MQTT 原生支持)
134
+ 4. 无返回值:明确告诉使用者"这不是 RPC"
135
+
136
+ 与 RPC 的共存:
137
+ - RPC 消息:type = "rpc_request" | "rpc_response"
138
+ - Event 消息:type = "event" | 原始字典(无 type 字段)
139
+ - 通过 type 字段自动分流(在 MQTTClient._handle_message 中)
140
+
141
+ 使用示例:
142
+ # 创建管理器
143
+ events = EventChannelManager(client)
144
+
145
+ # 订阅事件(支持通配符)
146
+ @events.subscribe("sensors/+/temperature")
147
+ async def on_temperature(topic, message):
148
+ print(f"温度更新: {topic} -> {message}")
149
+
150
+ # 发布结构化事件
151
+ await events.publish(
152
+ "sensors/room1/temperature",
153
+ EventMessage(
154
+ event_type="temperature.changed",
155
+ data={"value": 25.5, "unit": "C"}
156
+ )
157
+ )
158
+
159
+ # 发布原始事件(零开销)
160
+ await events.publish(
161
+ "sensors/room1/humidity",
162
+ {"value": 60.2, "unit": "%"}
163
+ )
164
+ """
165
+
166
+ def __init__(self, client: MQTTClient):
167
+ """初始化 Event Channel 管理器
168
+
169
+ Args:
170
+ client: MQTTClient 实例(必须已连接或准备连接)
171
+ """
172
+ self._client = client
173
+ self._patterns: dict[str, list[EventHandler]] = {} # pattern → handlers
174
+ self._dispatchers: dict[str, Callable] = {} # 保存 dispatcher 引用(修复 P0-B)
175
+
176
+ logger.info("EventChannelManager 已初始化")
177
+
178
+ @overload
179
+ def subscribe(
180
+ self,
181
+ pattern: str,
182
+ handler: None = None
183
+ ) -> Callable[[EventHandler], EventHandler]:
184
+ """装饰器模式:返回装饰器函数"""
185
+ ...
186
+
187
+ @overload
188
+ def subscribe(
189
+ self,
190
+ pattern: str,
191
+ handler: EventHandler
192
+ ) -> EventHandler:
193
+ """直接注册模式:返回处理器本身"""
194
+ ...
195
+
196
+ def subscribe(self, pattern: str, handler: Optional[EventHandler] = None):
197
+ """订阅事件主题(支持通配符)
198
+
199
+ 核心设计:
200
+ 1. 支持 MQTT 通配符(+ 和 #)
201
+ 2. 一个 pattern 可以有多个处理器(广播模式)
202
+ 3. 自动在 MQTTClient 层注册订阅
203
+
204
+ Args:
205
+ pattern: MQTT 主题模式(支持通配符)
206
+ - "+": 单级通配符(sensors/+/temperature)
207
+ - "#": 多级通配符(sensors/#)
208
+ handler: 事件处理器(可选,也可以用作装饰器)
209
+
210
+ 返回:
211
+ 装饰器函数(如果 handler 为 None)
212
+
213
+ 使用示例:
214
+ # 方式 1: 装饰器
215
+ @events.subscribe("sensors/+/temperature")
216
+ async def on_temp(topic, message):
217
+ pass
218
+
219
+ # 方式 2: 直接注册
220
+ events.subscribe("sensors/+/temperature", on_temp)
221
+ """
222
+
223
+ def decorator(func: EventHandler):
224
+ # 添加到处理器列表
225
+ if pattern not in self._patterns:
226
+ self._patterns[pattern] = []
227
+
228
+ # 第一次订阅这个 pattern,创建专属 dispatcher(修复 P0-B:避免闭包泄漏)
229
+ async def dispatcher(topic: str, payload: bytes):
230
+ """专属 dispatcher(bytes → dict → handler)"""
231
+ try:
232
+ # 解码(JSON 格式)
233
+ data = json.loads(payload.decode('utf-8'))
234
+
235
+ # 分发到所有 handlers(使用 get 避免 KeyError)
236
+ for h in self._patterns.get(pattern, []):
237
+ try:
238
+ if asyncio.iscoroutinefunction(h):
239
+ await h(topic, data)
240
+ else:
241
+ h(topic, data)
242
+ except Exception as e:
243
+ logger.exception(
244
+ f"Event handler 异常 - pattern: {pattern}, error: {e}"
245
+ )
246
+ except Exception as e:
247
+ logger.error(f"事件消息解码失败 - topic: {topic}, error: {e}")
248
+
249
+ # 保存 dispatcher 引用(修复 P0-B)
250
+ self._dispatchers[pattern] = dispatcher
251
+
252
+ # 注册到 MQTTClient 层
253
+ self._client.subscribe(pattern, dispatcher)
254
+
255
+ self._patterns[pattern].append(func)
256
+ logger.debug(
257
+ f"事件订阅成功 - pattern: {pattern}, handlers: {len(self._patterns[pattern])}"
258
+ )
259
+ return func
260
+
261
+ if handler is None:
262
+ return decorator
263
+ else:
264
+ return decorator(handler)
265
+
266
+ def unsubscribe(self, pattern: str, handler: Optional[EventHandler] = None):
267
+ """取消订阅(修复 P0-B:真正清理底层订阅)
268
+
269
+ Args:
270
+ pattern: MQTT 主题模式
271
+ handler: 要移除的处理器(None = 移除所有)
272
+
273
+ 改进:
274
+ 现在会真正调用 MQTTClient.unsubscribe 来清理底层 MQTT 订阅
275
+ 避免内存泄漏
276
+ """
277
+ if pattern not in self._patterns:
278
+ return
279
+
280
+ if handler is None:
281
+ # 移除所有处理器
282
+ del self._patterns[pattern]
283
+
284
+ # 清理 dispatcher(修复 P0-B)
285
+ dispatcher = self._dispatchers.pop(pattern, None)
286
+ if dispatcher:
287
+ self._client.unsubscribe(pattern, dispatcher)
288
+
289
+ logger.info(f"已取消订阅 - pattern: {pattern}")
290
+ else:
291
+ # 移除指定处理器
292
+ if handler in self._patterns[pattern]:
293
+ self._patterns[pattern].remove(handler)
294
+
295
+ # 如果没有处理器了,清理 dispatcher(修复 P0-B)
296
+ if not self._patterns[pattern]:
297
+ del self._patterns[pattern]
298
+
299
+ dispatcher = self._dispatchers.pop(pattern, None)
300
+ if dispatcher:
301
+ self._client.unsubscribe(pattern, dispatcher)
302
+
303
+ logger.info(f"已取消订阅(最后一个 handler)- pattern: {pattern}")
304
+ else:
305
+ logger.debug(
306
+ f"已移除处理器 - pattern: {pattern}, 剩余 {len(self._patterns[pattern])} 个"
307
+ )
308
+
309
+ async def publish(
310
+ self,
311
+ topic: str,
312
+ message: EventMessage | dict | Any,
313
+ qos: int = 0,
314
+ ):
315
+ """发布事件(极薄包装)
316
+
317
+ 设计原则:
318
+ - 不创建 Future(无返回值)
319
+ - 不等待确认(fire-and-forget)
320
+ - 直接调用 client.publish()
321
+
322
+ Args:
323
+ topic: 目标主题
324
+ message: 事件消息
325
+ - EventMessage: 自动序列化为 JSON
326
+ - dict: 直接序列化为 JSON
327
+ - 其他类型: 包装为 {"data": message}
328
+ qos: QoS 等级(0 = 最多一次,1 = 至少一次,2 = 恰好一次)
329
+
330
+ 使用示例:
331
+ # 结构化事件
332
+ await events.publish(
333
+ "sensors/room1/temperature",
334
+ EventMessage(event_type="temp.changed", data={"value": 25.5})
335
+ )
336
+
337
+ # 原始字典
338
+ await events.publish(
339
+ "sensors/room1/humidity",
340
+ {"value": 60.2, "unit": "%"}
341
+ )
342
+
343
+ # 简单值(自动包装)
344
+ await events.publish(
345
+ "alerts/fire",
346
+ "Fire detected in room 3!"
347
+ )
348
+ """
349
+ # 编码消息
350
+ if isinstance(message, EventMessage):
351
+ payload = message.encode()
352
+ elif isinstance(message, dict):
353
+ payload = json.dumps(message).encode('utf-8')
354
+ else:
355
+ # 其他类型自动包装
356
+ payload = json.dumps({"data": message}).encode('utf-8')
357
+
358
+ # 直接发布(零开销)
359
+ await self._client.raw.publish(topic, payload, qos=qos)
mqttxx/protocol.py CHANGED
@@ -1,11 +1,34 @@
1
1
  # MQTT RPC 消息协议定义
2
2
 
3
+ import json
3
4
  from dataclasses import dataclass
5
+ from dataclasses import asdict, is_dataclass
4
6
  from typing import Any, Optional, Literal
5
-
6
7
  from .exceptions import MessageError, ErrorCode
7
8
 
8
9
 
10
+ def _to_jsonable(obj: Any) -> Any:
11
+ if obj is None:
12
+ return None
13
+
14
+ if is_dataclass(obj):
15
+ return _to_jsonable(asdict(obj))
16
+
17
+ if isinstance(obj, dict):
18
+ return {k: _to_jsonable(v) for k, v in obj.items()}
19
+
20
+ if isinstance(obj, (list, tuple, set)):
21
+ return [_to_jsonable(v) for v in obj]
22
+
23
+ if hasattr(obj, "model_dump") and callable(getattr(obj, "model_dump")):
24
+ return _to_jsonable(obj.model_dump(mode="json"))
25
+
26
+ if hasattr(obj, "dict") and callable(getattr(obj, "dict")):
27
+ return _to_jsonable(obj.dict())
28
+
29
+ return obj
30
+
31
+
9
32
  @dataclass
10
33
  class RPCRequest:
11
34
  """RPC 请求消息
@@ -52,7 +75,7 @@ class RPCRequest:
52
75
  "type": self.type,
53
76
  "request_id": self.request_id,
54
77
  "method": self.method,
55
- "params": self.params,
78
+ "params": _to_jsonable(self.params),
56
79
  "reply_to": self.reply_to,
57
80
  "caller_id": self.caller_id,
58
81
  }
@@ -80,10 +103,35 @@ class RPCRequest:
80
103
  )
81
104
  except KeyError as e:
82
105
  raise MessageError(
83
- f"RPC 请求缺少必需字段: {e}",
84
- ErrorCode.MISSING_REQUIRED_FIELD
106
+ f"RPC 请求缺少必需字段: {e}", ErrorCode.MISSING_REQUIRED_FIELD
85
107
  )
86
108
 
109
+ def encode(self) -> bytes:
110
+ """编码为 bytes(JSON 格式)
111
+
112
+ Returns:
113
+ UTF-8 编码的 JSON bytes
114
+ """
115
+ return json.dumps(self.to_dict()).encode("utf-8")
116
+
117
+ @classmethod
118
+ def decode(cls, data: bytes) -> "RPCRequest":
119
+ """从 bytes 解码(JSON 格式)
120
+
121
+ Args:
122
+ data: UTF-8 编码的 JSON bytes
123
+
124
+ Returns:
125
+ RPCRequest 对象
126
+
127
+ Raises:
128
+ MessageError: 解码失败或缺少必需字段
129
+ UnicodeDecodeError: UTF-8 解码失败
130
+ json.JSONDecodeError: JSON 解析失败
131
+ """
132
+ obj = json.loads(data.decode("utf-8"))
133
+ return cls.from_dict(obj)
134
+
87
135
 
88
136
  @dataclass
89
137
  class RPCResponse:
@@ -133,7 +181,7 @@ class RPCResponse:
133
181
  if self.error is not None:
134
182
  data["error"] = self.error
135
183
  else:
136
- data["result"] = self.result
184
+ data["result"] = _to_jsonable(self.result)
137
185
 
138
186
  return data
139
187
 
@@ -158,28 +206,53 @@ class RPCResponse:
158
206
  )
159
207
  except KeyError as e:
160
208
  raise MessageError(
161
- f"RPC 响应缺少必需字段: {e}",
162
- ErrorCode.MISSING_REQUIRED_FIELD
209
+ f"RPC 响应缺少必需字段: {e}", ErrorCode.MISSING_REQUIRED_FIELD
163
210
  )
164
211
 
212
+ def encode(self) -> bytes:
213
+ """编码为 bytes(JSON 格式)
165
214
 
166
- def parse_message(data: dict) -> RPCRequest | RPCResponse:
167
- """解析 RPC 消息(带类型验证)
215
+ Returns:
216
+ UTF-8 编码的 JSON bytes
217
+ """
218
+ return json.dumps(self.to_dict()).encode("utf-8")
219
+
220
+ @classmethod
221
+ def decode(cls, data: bytes) -> "RPCResponse":
222
+ """从 bytes 解码(JSON 格式)
168
223
 
169
- 根据消息的 type 字段自动判断消息类型并解析
224
+ Args:
225
+ data: UTF-8 编码的 JSON bytes
226
+
227
+ Returns:
228
+ RPCResponse 对象
229
+
230
+ Raises:
231
+ MessageError: 解码失败或缺少必需字段
232
+ UnicodeDecodeError: UTF-8 解码失败
233
+ json.JSONDecodeError: JSON 解析失败
234
+ """
235
+ obj = json.loads(data.decode("utf-8"))
236
+ return cls.from_dict(obj)
237
+
238
+
239
+ def parse_message_from_bytes(data: bytes) -> RPCRequest | RPCResponse:
240
+ """从 bytes 解析 RPC 消息(JSON 格式)
170
241
 
171
242
  Args:
172
- data: JSON 解析后的字典
243
+ data: UTF-8 编码的 JSON bytes
173
244
 
174
245
  Returns:
175
- RPCRequest 或 RPCResponse 实例
246
+ RPCRequest 或 RPCResponse 对象
176
247
 
177
248
  Raises:
178
- MessageError: 未知消息类型或解析失败
249
+ MessageError: 解码失败或消息类型无效
250
+ UnicodeDecodeError: UTF-8 解码失败
251
+ json.JSONDecodeError: JSON 解析失败
179
252
 
180
253
  示例:
181
- data = json.loads(payload)
182
- message = parse_message(data)
254
+ payload = b'{"type":"rpc_request","request_id":"123",...}'
255
+ message = parse_message_from_bytes(payload)
183
256
 
184
257
  if isinstance(message, RPCRequest):
185
258
  # 处理请求
@@ -188,14 +261,12 @@ def parse_message(data: dict) -> RPCRequest | RPCResponse:
188
261
  # 处理响应
189
262
  pass
190
263
  """
191
- msg_type = data.get("type")
264
+ obj = json.loads(data.decode("utf-8"))
265
+ msg_type = obj.get("type")
192
266
 
193
267
  if msg_type == "rpc_request":
194
- return RPCRequest.from_dict(data)
268
+ return RPCRequest.from_dict(obj)
195
269
  elif msg_type == "rpc_response":
196
- return RPCResponse.from_dict(data)
270
+ return RPCResponse.from_dict(obj)
197
271
  else:
198
- raise MessageError(
199
- f"未知消息类型: {msg_type}",
200
- ErrorCode.INVALID_MESSAGE_TYPE
201
- )
272
+ raise MessageError(f"未知消息类型: {msg_type}", ErrorCode.INVALID_MESSAGE_TYPE)