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