mqttxx 3.1.2__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/protocol.py CHANGED
@@ -2,106 +2,31 @@
2
2
 
3
3
  import json
4
4
  from dataclasses import dataclass
5
- from typing import Any, Optional, Literal, Protocol as TypingProtocol, Type, runtime_checkable
6
-
5
+ from dataclasses import asdict, is_dataclass
6
+ from typing import Any, Optional, Literal
7
7
  from .exceptions import MessageError, ErrorCode
8
8
 
9
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
10
+ def _to_jsonable(obj: Any) -> Any:
11
+ if obj is None:
12
+ return None
74
13
 
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)}")
14
+ if is_dataclass(obj):
15
+ return _to_jsonable(asdict(obj))
86
16
 
87
- return json.dumps(data).encode('utf-8')
17
+ if isinstance(obj, dict):
18
+ return {k: _to_jsonable(v) for k, v in obj.items()}
88
19
 
89
- @staticmethod
90
- def decode(data: bytes) -> dict:
91
- """bytes → dict
20
+ if isinstance(obj, (list, tuple, set)):
21
+ return [_to_jsonable(v) for v in obj]
92
22
 
93
- Args:
94
- data: UTF-8 编码的 JSON bytes
23
+ if hasattr(obj, "model_dump") and callable(getattr(obj, "model_dump")):
24
+ return _to_jsonable(obj.model_dump(mode="json"))
95
25
 
96
- Returns:
97
- 解码后的 dict
26
+ if hasattr(obj, "dict") and callable(getattr(obj, "dict")):
27
+ return _to_jsonable(obj.dict())
98
28
 
99
- Raises:
100
- UnicodeDecodeError: UTF-8 解码失败
101
- json.JSONDecodeError: JSON 解析失败
102
- """
103
- text = data.decode('utf-8')
104
- return json.loads(text)
29
+ return obj
105
30
 
106
31
 
107
32
  @dataclass
@@ -146,16 +71,11 @@ class RPCRequest:
146
71
  Returns:
147
72
  包含所有字段的字典
148
73
  """
149
- # 处理 Pydantic 模型
150
- params = self.params
151
- if hasattr(params, 'model_dump'):
152
- params = params.model_dump()
153
-
154
74
  return {
155
75
  "type": self.type,
156
76
  "request_id": self.request_id,
157
77
  "method": self.method,
158
- "params": params,
78
+ "params": _to_jsonable(self.params),
159
79
  "reply_to": self.reply_to,
160
80
  "caller_id": self.caller_id,
161
81
  }
@@ -183,46 +103,33 @@ class RPCRequest:
183
103
  )
184
104
  except KeyError as e:
185
105
  raise MessageError(
186
- f"RPC 请求缺少必需字段: {e}",
187
- ErrorCode.MISSING_REQUIRED_FIELD
106
+ f"RPC 请求缺少必需字段: {e}", ErrorCode.MISSING_REQUIRED_FIELD
188
107
  )
189
108
 
190
- def encode(self, codec: Type[Codec] = JSONCodec) -> bytes:
191
- """编码为 bytes
192
-
193
- Args:
194
- codec: 编解码器(默认 JSONCodec)
109
+ def encode(self) -> bytes:
110
+ """编码为 bytes(JSON 格式)
195
111
 
196
112
  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)
113
+ UTF-8 编码的 JSON bytes
204
114
  """
205
- return codec.encode(self)
115
+ return json.dumps(self.to_dict()).encode("utf-8")
206
116
 
207
117
  @classmethod
208
- def decode(cls, data: bytes, codec: Type[Codec] = JSONCodec) -> "RPCRequest":
209
- """从 bytes 解码
118
+ def decode(cls, data: bytes) -> "RPCRequest":
119
+ """从 bytes 解码(JSON 格式)
210
120
 
211
121
  Args:
212
- data: 原始 bytes 数据
213
- codec: 编解码器(默认 JSONCodec)
122
+ data: UTF-8 编码的 JSON bytes
214
123
 
215
124
  Returns:
216
125
  RPCRequest 对象
217
126
 
218
127
  Raises:
219
128
  MessageError: 解码失败或缺少必需字段
220
-
221
- 示例:
222
- payload = b'{"type":"rpc_request","request_id":"123",...}'
223
- request = RPCRequest.decode(payload)
129
+ UnicodeDecodeError: UTF-8 解码失败
130
+ json.JSONDecodeError: JSON 解析失败
224
131
  """
225
- obj = codec.decode(data)
132
+ obj = json.loads(data.decode("utf-8"))
226
133
  return cls.from_dict(obj)
227
134
 
228
135
 
@@ -274,11 +181,7 @@ class RPCResponse:
274
181
  if self.error is not None:
275
182
  data["error"] = self.error
276
183
  else:
277
- # 处理 Pydantic 模型
278
- result = self.result
279
- if hasattr(result, 'model_dump'):
280
- result = result.model_dump()
281
- data["result"] = result
184
+ data["result"] = _to_jsonable(self.result)
282
185
 
283
186
  return data
284
187
 
@@ -303,45 +206,41 @@ class RPCResponse:
303
206
  )
304
207
  except KeyError as e:
305
208
  raise MessageError(
306
- f"RPC 响应缺少必需字段: {e}",
307
- ErrorCode.MISSING_REQUIRED_FIELD
209
+ f"RPC 响应缺少必需字段: {e}", ErrorCode.MISSING_REQUIRED_FIELD
308
210
  )
309
211
 
310
- def encode(self, codec: Type[Codec] = JSONCodec) -> bytes:
311
- """编码为 bytes
312
-
313
- Args:
314
- codec: 编解码器(默认 JSONCodec)
212
+ def encode(self) -> bytes:
213
+ """编码为 bytes(JSON 格式)
315
214
 
316
215
  Returns:
317
- 编码后的 bytes
216
+ UTF-8 编码的 JSON bytes
318
217
  """
319
- return codec.encode(self)
218
+ return json.dumps(self.to_dict()).encode("utf-8")
320
219
 
321
220
  @classmethod
322
- def decode(cls, data: bytes, codec: Type[Codec] = JSONCodec) -> "RPCResponse":
323
- """从 bytes 解码
221
+ def decode(cls, data: bytes) -> "RPCResponse":
222
+ """从 bytes 解码(JSON 格式)
324
223
 
325
224
  Args:
326
- data: 原始 bytes 数据
327
- codec: 编解码器(默认 JSONCodec)
225
+ data: UTF-8 编码的 JSON bytes
328
226
 
329
227
  Returns:
330
228
  RPCResponse 对象
331
229
 
332
230
  Raises:
333
231
  MessageError: 解码失败或缺少必需字段
232
+ UnicodeDecodeError: UTF-8 解码失败
233
+ json.JSONDecodeError: JSON 解析失败
334
234
  """
335
- obj = codec.decode(data)
235
+ obj = json.loads(data.decode("utf-8"))
336
236
  return cls.from_dict(obj)
337
237
 
338
238
 
339
- def parse_message_from_bytes(data: bytes, codec: Type[Codec] = JSONCodec) -> RPCRequest | RPCResponse:
340
- """从 bytes 解析 RPC 消息
239
+ def parse_message_from_bytes(data: bytes) -> RPCRequest | RPCResponse:
240
+ """从 bytes 解析 RPC 消息(JSON 格式)
341
241
 
342
242
  Args:
343
- data: 原始 bytes 数据
344
- codec: 编解码器(默认 JSONCodec)
243
+ data: UTF-8 编码的 JSON bytes
345
244
 
346
245
  Returns:
347
246
  RPCRequest 或 RPCResponse 对象
@@ -362,7 +261,7 @@ def parse_message_from_bytes(data: bytes, codec: Type[Codec] = JSONCodec) -> RPC
362
261
  # 处理响应
363
262
  pass
364
263
  """
365
- obj = codec.decode(data)
264
+ obj = json.loads(data.decode("utf-8"))
366
265
  msg_type = obj.get("type")
367
266
 
368
267
  if msg_type == "rpc_request":
@@ -370,7 +269,4 @@ def parse_message_from_bytes(data: bytes, codec: Type[Codec] = JSONCodec) -> RPC
370
269
  elif msg_type == "rpc_response":
371
270
  return RPCResponse.from_dict(obj)
372
271
  else:
373
- raise MessageError(
374
- f"未知消息类型: {msg_type}",
375
- ErrorCode.INVALID_MESSAGE_TYPE
376
- )
272
+ raise MessageError(f"未知消息类型: {msg_type}", ErrorCode.INVALID_MESSAGE_TYPE)
mqttxx/rpc.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import uuid
5
- from typing import Any, Callable, Optional, Type
5
+ from typing import Any, Callable, Optional
6
6
  from loguru import logger
7
7
 
8
8
  from .client import MQTTClient
@@ -15,7 +15,11 @@ from .exceptions import (
15
15
  ErrorCode,
16
16
  MessageError,
17
17
  )
18
- from .protocol import RPCRequest, RPCResponse, parse_message_from_bytes, Codec, JSONCodec
18
+ from .protocol import (
19
+ RPCRequest,
20
+ RPCResponse,
21
+ parse_message_from_bytes,
22
+ )
19
23
 
20
24
 
21
25
  # 权限检查回调类型
@@ -74,14 +78,15 @@ class RPCManager:
74
78
  def __init__(
75
79
  self,
76
80
  client: MQTTClient,
81
+ my_topic: Optional[str] = None,
77
82
  config: Optional[RPCConfig] = None,
78
83
  auth_callback: Optional[AuthCallback] = None,
79
- codec: Type[Codec] = JSONCodec, # 新增:可插拔编解码器
80
84
  ):
81
85
  """初始化 RPC 管理器
82
86
 
83
87
  Args:
84
88
  client: MQTTClient 实例(用于底层消息收发)
89
+ my_topic: 本节点的响应主题(可选,提供后自动订阅并注入到 reply_to)
85
90
  config: RPC 配置(可选,默认使用标准配置)
86
91
  auth_callback: 权限检查回调函数(可选)
87
92
  签名:async def auth_callback(caller_id: str, method: str, request: RPCRequest) -> bool
@@ -92,29 +97,38 @@ class RPCManager:
92
97
  client = MQTTClient(...)
93
98
  await client.connect()
94
99
 
95
- # 基础用法(无权限控制)
100
+ # 约定式用法(推荐)
101
+ rpc = RPCManager(client, my_topic="edge/device_123")
102
+ # 自动订阅 edge/device_123,调用时自动注入 reply_to
103
+
104
+ # 手动设置响应主题
96
105
  rpc = RPCManager(client)
97
- rpc.setup("my/rpc/responses") # 设置响应主题
106
+ rpc.setup("my/rpc/responses")
98
107
 
99
108
  # 带权限控制
100
109
  async def auth_check(caller_id, method, request):
101
110
  return caller_id in ALLOWED_CLIENTS
102
111
 
103
- rpc = RPCManager(client, auth_callback=auth_check)
112
+ rpc = RPCManager(client, my_topic="server/node", auth_callback=auth_check)
104
113
  """
105
114
  self._client = client
115
+ self._my_topic = my_topic
106
116
  self.config = config or RPCConfig()
107
117
  self._auth_callback = auth_callback
108
- self._codec = codec # 保存编解码器
109
118
 
110
119
  # RPC 状态
111
120
  self._pending_calls: dict[str, asyncio.Future] = {} # request_id → Future
112
121
  self._handlers: dict[str, Callable] = {} # method_name → handler
113
- self._pending_calls_lock = (
114
- asyncio.Lock()
115
- ) # 修复 P0-1:保护 _pending_calls 并发访问
122
+ self._pending_calls_lock = asyncio.Lock() # 保护 _pending_calls 并发访问
116
123
 
117
- logger.info("RPCManager 已初始化")
124
+ # 如果提供了 my_topic,自动订阅
125
+ if my_topic:
126
+ self.setup(my_topic)
127
+
128
+ @property
129
+ def my_topic(self) -> Optional[str]:
130
+ """获取本节点的响应主题"""
131
+ return self._my_topic
118
132
 
119
133
  def setup(self, reply_topic: str):
120
134
  """设置 RPC 响应主题并自动订阅
@@ -130,11 +144,12 @@ class RPCManager:
130
144
  rpc = RPCManager(client)
131
145
  rpc.setup("my/rpc/responses")
132
146
  """
147
+
133
148
  async def handle_bytes(topic: str, payload: bytes):
134
149
  """bytes → RPC message → handle"""
135
150
  try:
136
151
  # 解码
137
- message = parse_message_from_bytes(payload, self._codec)
152
+ message = parse_message_from_bytes(payload)
138
153
 
139
154
  # 路由
140
155
  if isinstance(message, RPCRequest):
@@ -147,8 +162,7 @@ class RPCManager:
147
162
  logger.exception(f"RPC 消息处理失败: {e}")
148
163
 
149
164
  # 订阅 raw bytes
150
- self._client.raw.subscribe(reply_topic, handle_bytes)
151
-
165
+ self._client.subscribe(reply_topic, handle_bytes)
152
166
 
153
167
  def register(self, method_name: str):
154
168
  """装饰器:注册本地 RPC 方法供远程调用
@@ -226,52 +240,57 @@ class RPCManager:
226
240
  method: str,
227
241
  params: Any = None,
228
242
  timeout: Optional[float] = None,
229
- reply_to: str = None,
243
+ reply_to: Optional[str] = None,
230
244
  ) -> Any:
231
245
  """远程调用 RPC 方法
232
246
 
233
247
  修复点:
234
248
  - ✅ 新增并发限制检查
249
+ - ✅ 自动注入 reply_to(如果初始化时提供了 my_topic)
235
250
 
236
251
  Args:
237
252
  topic: 目标 MQTT 主题(例如:bots/456)
238
253
  method: 远程方法名
239
254
  params: 方法参数(可选)
240
255
  timeout: 超时时间(秒,None 则使用配置的默认值)
241
- reply_to: 响应主题(必需,例如:server/device_123
256
+ reply_to: 响应主题(可选,默认使用初始化时的 my_topic
242
257
 
243
258
  Returns:
244
259
  远程方法的返回值
245
260
 
246
261
  Raises:
247
262
  MQTTXError: 客户端未连接
248
- ValueError: reply_to 参数缺失
263
+ ValueError: reply_to 参数缺失且初始化时未提供 my_topic
249
264
  TooManyConcurrentCallsError: 并发调用超限
250
265
  RPCTimeoutError: 调用超时
251
266
  RPCRemoteError: 远程执行失败
252
267
 
253
268
  使用示例:
254
- # 基础调用
255
- result = await rpc.call(
256
- topic="bots/456",
257
- method="get_status",
258
- reply_to="server/device_123"
259
- )
269
+ # 约定式用法(推荐)
270
+ rpc = RPCManager(client, my_topic="server/device_123")
271
+ result = await rpc.call("bots/456", "get_status")
272
+ # reply_to 自动注入为 "server/device_123"
260
273
 
261
- # 带参数调用
274
+ # 手动指定 reply_to
262
275
  result = await rpc.call(
263
276
  topic="bots/456",
264
277
  method="process_command",
265
278
  params={"command": "restart"},
266
- reply_to="server/device_123",
279
+ reply_to="custom/reply/topic",
267
280
  timeout=60.0
268
281
  )
269
282
  """
270
283
  if not self._client.is_connected:
271
284
  raise MQTTXError("MQTT 客户端未连接", ErrorCode.NOT_CONNECTED)
272
285
 
286
+ # 自动注入 reply_to
273
287
  if reply_to is None:
274
- raise ValueError("reply_to 参数是必需的,不能为 None")
288
+ reply_to = self._my_topic
289
+
290
+ if reply_to is None:
291
+ raise ValueError(
292
+ "reply_to 参数是必需的,或在初始化时提供 my_topic"
293
+ )
275
294
 
276
295
  # 生成请求
277
296
  request_id = str(uuid.uuid4())
@@ -295,8 +314,8 @@ class RPCManager:
295
314
  future = asyncio.get_event_loop().create_future()
296
315
  self._pending_calls[request_id] = future
297
316
 
298
- # 发送请求(使用编解码器)
299
- payload = request.encode(self._codec)
317
+ # 发送请求
318
+ payload = request.encode()
300
319
  await self._client.raw.publish(topic, payload, qos=1)
301
320
  logger.debug(f"RPC 请求已发送 - method: {method}, request_id: {request_id[:8]}")
302
321
 
@@ -445,6 +464,6 @@ class RPCManager:
445
464
  topic: 响应主题
446
465
  response: RPC 响应消息
447
466
  """
448
- payload = response.encode(self._codec)
467
+ payload = response.encode()
449
468
  await self._client.raw.publish(topic, payload, qos=1)
450
469
  logger.debug(f"RPC 响应已发送 - request_id: {response.request_id[:8]}")