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/__init__.py +13 -5
- mqttxx/client.py +295 -152
- mqttxx/config.py +14 -0
- mqttxx/conventions.py +8 -5
- mqttxx/events.py +340 -0
- mqttxx/protocol.py +189 -14
- mqttxx/rpc.py +63 -30
- mqttxx-3.1.2.dist-info/METADATA +910 -0
- mqttxx-3.1.2.dist-info/RECORD +13 -0
- mqttxx-2.0.2.dist-info/METADATA +0 -490
- mqttxx-2.0.2.dist-info/RECORD +0 -12
- {mqttxx-2.0.2.dist-info → mqttxx-3.1.2.dist-info}/LICENSE +0 -0
- {mqttxx-2.0.2.dist-info → mqttxx-3.1.2.dist-info}/WHEEL +0 -0
- {mqttxx-2.0.2.dist-info → mqttxx-3.1.2.dist-info}/top_level.txt +0 -0
mqttxx/rpc.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# MQTT RPC 模块 - 基于 aiomqtt 的双向对等 RPC 调用
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import json
|
|
5
4
|
import uuid
|
|
6
|
-
from typing import Any, Callable, Optional
|
|
5
|
+
from typing import Any, Callable, Optional, Type
|
|
7
6
|
from loguru import logger
|
|
8
7
|
|
|
9
8
|
from .client import MQTTClient
|
|
@@ -14,8 +13,9 @@ from .exceptions import (
|
|
|
14
13
|
TooManyConcurrentCallsError,
|
|
15
14
|
MQTTXError,
|
|
16
15
|
ErrorCode,
|
|
16
|
+
MessageError,
|
|
17
17
|
)
|
|
18
|
-
from .protocol import RPCRequest, RPCResponse
|
|
18
|
+
from .protocol import RPCRequest, RPCResponse, parse_message_from_bytes, Codec, JSONCodec
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
# 权限检查回调类型
|
|
@@ -75,7 +75,8 @@ class RPCManager:
|
|
|
75
75
|
self,
|
|
76
76
|
client: MQTTClient,
|
|
77
77
|
config: Optional[RPCConfig] = None,
|
|
78
|
-
auth_callback: Optional[AuthCallback] = None
|
|
78
|
+
auth_callback: Optional[AuthCallback] = None,
|
|
79
|
+
codec: Type[Codec] = JSONCodec, # 新增:可插拔编解码器
|
|
79
80
|
):
|
|
80
81
|
"""初始化 RPC 管理器
|
|
81
82
|
|
|
@@ -85,6 +86,7 @@ class RPCManager:
|
|
|
85
86
|
auth_callback: 权限检查回调函数(可选)
|
|
86
87
|
签名:async def auth_callback(caller_id: str, method: str, request: RPCRequest) -> bool
|
|
87
88
|
返回:True = 允许,False = 拒绝
|
|
89
|
+
codec: 编解码器(默认 JSONCodec)
|
|
88
90
|
|
|
89
91
|
使用示例:
|
|
90
92
|
client = MQTTClient(...)
|
|
@@ -92,6 +94,7 @@ class RPCManager:
|
|
|
92
94
|
|
|
93
95
|
# 基础用法(无权限控制)
|
|
94
96
|
rpc = RPCManager(client)
|
|
97
|
+
rpc.setup("my/rpc/responses") # 设置响应主题
|
|
95
98
|
|
|
96
99
|
# 带权限控制
|
|
97
100
|
async def auth_check(caller_id, method, request):
|
|
@@ -102,14 +105,51 @@ class RPCManager:
|
|
|
102
105
|
self._client = client
|
|
103
106
|
self.config = config or RPCConfig()
|
|
104
107
|
self._auth_callback = auth_callback
|
|
108
|
+
self._codec = codec # 保存编解码器
|
|
105
109
|
|
|
106
110
|
# RPC 状态
|
|
107
111
|
self._pending_calls: dict[str, asyncio.Future] = {} # request_id → Future
|
|
108
112
|
self._handlers: dict[str, Callable] = {} # method_name → handler
|
|
109
|
-
self._pending_calls_lock =
|
|
113
|
+
self._pending_calls_lock = (
|
|
114
|
+
asyncio.Lock()
|
|
115
|
+
) # 修复 P0-1:保护 _pending_calls 并发访问
|
|
110
116
|
|
|
111
117
|
logger.info("RPCManager 已初始化")
|
|
112
118
|
|
|
119
|
+
def setup(self, reply_topic: str):
|
|
120
|
+
"""设置 RPC 响应主题并自动订阅
|
|
121
|
+
|
|
122
|
+
这个方法会:
|
|
123
|
+
1. 订阅 reply_topic(接收 RPC 响应)
|
|
124
|
+
2. 注册消息处理器(自动解码 + 分发)
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
reply_topic: 响应主题(例如:server/rpc_responses)
|
|
128
|
+
|
|
129
|
+
示例:
|
|
130
|
+
rpc = RPCManager(client)
|
|
131
|
+
rpc.setup("my/rpc/responses")
|
|
132
|
+
"""
|
|
133
|
+
async def handle_bytes(topic: str, payload: bytes):
|
|
134
|
+
"""bytes → RPC message → handle"""
|
|
135
|
+
try:
|
|
136
|
+
# 解码
|
|
137
|
+
message = parse_message_from_bytes(payload, self._codec)
|
|
138
|
+
|
|
139
|
+
# 路由
|
|
140
|
+
if isinstance(message, RPCRequest):
|
|
141
|
+
await self._handle_request(topic, message)
|
|
142
|
+
elif isinstance(message, RPCResponse):
|
|
143
|
+
await self._handle_response(topic, message)
|
|
144
|
+
except MessageError as e:
|
|
145
|
+
logger.debug(f"非 RPC 消息 - topic: {topic}, reason: {e}")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.exception(f"RPC 消息处理失败: {e}")
|
|
148
|
+
|
|
149
|
+
# 订阅 raw bytes
|
|
150
|
+
self._client.raw.subscribe(reply_topic, handle_bytes)
|
|
151
|
+
|
|
152
|
+
|
|
113
153
|
def register(self, method_name: str):
|
|
114
154
|
"""装饰器:注册本地 RPC 方法供远程调用
|
|
115
155
|
|
|
@@ -129,9 +169,10 @@ class RPCManager:
|
|
|
129
169
|
# 同步方法也支持
|
|
130
170
|
return {"result": "ok"}
|
|
131
171
|
"""
|
|
172
|
+
|
|
132
173
|
def decorator(func: Callable):
|
|
133
174
|
self._handlers[method_name] = func
|
|
134
|
-
logger.
|
|
175
|
+
# logger.debug(f"RPC 方法已注册: {method_name}") 无需输出
|
|
135
176
|
return func
|
|
136
177
|
|
|
137
178
|
return decorator
|
|
@@ -171,12 +212,12 @@ class RPCManager:
|
|
|
171
212
|
if isinstance(message, RPCRequest):
|
|
172
213
|
asyncio.create_task(
|
|
173
214
|
self._handle_request(topic, message),
|
|
174
|
-
name=f"rpc_req_{message.request_id[:8]}"
|
|
215
|
+
name=f"rpc_req_{message.request_id[:8]}",
|
|
175
216
|
)
|
|
176
217
|
elif isinstance(message, RPCResponse):
|
|
177
218
|
asyncio.create_task(
|
|
178
219
|
self._handle_response(topic, message),
|
|
179
|
-
name=f"rpc_resp_{message.request_id[:8]}"
|
|
220
|
+
name=f"rpc_resp_{message.request_id[:8]}",
|
|
180
221
|
)
|
|
181
222
|
|
|
182
223
|
async def call(
|
|
@@ -254,8 +295,9 @@ class RPCManager:
|
|
|
254
295
|
future = asyncio.get_event_loop().create_future()
|
|
255
296
|
self._pending_calls[request_id] = future
|
|
256
297
|
|
|
257
|
-
#
|
|
258
|
-
|
|
298
|
+
# 发送请求(使用编解码器)
|
|
299
|
+
payload = request.encode(self._codec)
|
|
300
|
+
await self._client.raw.publish(topic, payload, qos=1)
|
|
259
301
|
logger.debug(f"RPC 请求已发送 - method: {method}, request_id: {request_id[:8]}")
|
|
260
302
|
|
|
261
303
|
try:
|
|
@@ -316,8 +358,7 @@ class RPCManager:
|
|
|
316
358
|
f"RPC 权限拒绝 - caller: {message.caller_id}, method: {message.method}"
|
|
317
359
|
)
|
|
318
360
|
response = RPCResponse(
|
|
319
|
-
request_id=message.request_id,
|
|
320
|
-
error="Permission denied"
|
|
361
|
+
request_id=message.request_id, error="Permission denied"
|
|
321
362
|
)
|
|
322
363
|
await self._send_response(message.reply_to, response)
|
|
323
364
|
return
|
|
@@ -328,8 +369,7 @@ class RPCManager:
|
|
|
328
369
|
if not handler:
|
|
329
370
|
logger.warning(f"方法未找到 - method: {message.method}")
|
|
330
371
|
response = RPCResponse(
|
|
331
|
-
request_id=message.request_id,
|
|
332
|
-
error=f"方法未找到: {message.method}"
|
|
372
|
+
request_id=message.request_id, error=f"方法未找到: {message.method}"
|
|
333
373
|
)
|
|
334
374
|
else:
|
|
335
375
|
# 执行方法
|
|
@@ -353,22 +393,20 @@ class RPCManager:
|
|
|
353
393
|
try:
|
|
354
394
|
if asyncio.iscoroutinefunction(self._auth_callback):
|
|
355
395
|
allowed = await self._auth_callback(
|
|
356
|
-
message.caller_id,
|
|
357
|
-
message.method,
|
|
358
|
-
message
|
|
396
|
+
message.caller_id, message.method, message
|
|
359
397
|
)
|
|
360
398
|
else:
|
|
361
399
|
allowed = self._auth_callback(
|
|
362
|
-
message.caller_id,
|
|
363
|
-
message.method,
|
|
364
|
-
message
|
|
400
|
+
message.caller_id, message.method, message
|
|
365
401
|
)
|
|
366
402
|
return bool(allowed)
|
|
367
403
|
except Exception as e:
|
|
368
404
|
logger.exception(f"权限检查失败: {e}")
|
|
369
405
|
return False # 默认拒绝
|
|
370
406
|
|
|
371
|
-
async def _execute_handler(
|
|
407
|
+
async def _execute_handler(
|
|
408
|
+
self, handler: Callable, message: RPCRequest
|
|
409
|
+
) -> RPCResponse:
|
|
372
410
|
"""执行 RPC 方法处理器
|
|
373
411
|
|
|
374
412
|
Args:
|
|
@@ -390,10 +428,7 @@ class RPCManager:
|
|
|
390
428
|
result = handler(message.params)
|
|
391
429
|
|
|
392
430
|
logger.debug(f"RPC 方法执行成功 - method: {message.method}")
|
|
393
|
-
return RPCResponse(
|
|
394
|
-
request_id=message.request_id,
|
|
395
|
-
result=result
|
|
396
|
-
)
|
|
431
|
+
return RPCResponse(request_id=message.request_id, result=result)
|
|
397
432
|
|
|
398
433
|
except asyncio.CancelledError:
|
|
399
434
|
# 任务被取消,向上传播
|
|
@@ -401,10 +436,7 @@ class RPCManager:
|
|
|
401
436
|
|
|
402
437
|
except Exception as e:
|
|
403
438
|
logger.exception(f"RPC 方法执行失败 - method: {message.method}")
|
|
404
|
-
return RPCResponse(
|
|
405
|
-
request_id=message.request_id,
|
|
406
|
-
error=str(e)
|
|
407
|
-
)
|
|
439
|
+
return RPCResponse(request_id=message.request_id, error=str(e))
|
|
408
440
|
|
|
409
441
|
async def _send_response(self, topic: str, response: RPCResponse):
|
|
410
442
|
"""发送 RPC 响应
|
|
@@ -413,5 +445,6 @@ class RPCManager:
|
|
|
413
445
|
topic: 响应主题
|
|
414
446
|
response: RPC 响应消息
|
|
415
447
|
"""
|
|
416
|
-
|
|
448
|
+
payload = response.encode(self._codec)
|
|
449
|
+
await self._client.raw.publish(topic, payload, qos=1)
|
|
417
450
|
logger.debug(f"RPC 响应已发送 - request_id: {response.request_id[:8]}")
|