mqttxx 2.0.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/config.py ADDED
@@ -0,0 +1,190 @@
1
+ # MQTT 客户端配置对象
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass
9
+ class TLSConfig:
10
+ """TLS/SSL 配置
11
+
12
+ 用于配置 MQTT 连接的加密传输层
13
+
14
+ Attributes:
15
+ enabled: 是否启用 TLS/SSL
16
+ ca_certs: CA 证书路径(用于验证服务器证书)
17
+ certfile: 客户端证书路径(双向认证)
18
+ keyfile: 客户端私钥路径(双向认证)
19
+ verify_mode: 证书验证模式("CERT_REQUIRED" | "CERT_OPTIONAL" | "CERT_NONE")
20
+ check_hostname: 是否验证服务器主机名
21
+
22
+ 示例:
23
+ # 单向 TLS(仅验证服务器)
24
+ tls = TLSConfig(
25
+ enabled=True,
26
+ ca_certs=Path("/path/to/ca.crt")
27
+ )
28
+
29
+ # 双向 TLS(客户端证书认证)
30
+ tls = TLSConfig(
31
+ enabled=True,
32
+ ca_certs=Path("/path/to/ca.crt"),
33
+ certfile=Path("/path/to/client.crt"),
34
+ keyfile=Path("/path/to/client.key")
35
+ )
36
+ """
37
+
38
+ enabled: bool = False
39
+ ca_certs: Optional[Path] = None
40
+ certfile: Optional[Path] = None
41
+ keyfile: Optional[Path] = None
42
+ verify_mode: str = "CERT_REQUIRED"
43
+ check_hostname: bool = True
44
+
45
+
46
+ @dataclass
47
+ class AuthConfig:
48
+ """MQTT 认证配置
49
+
50
+ 用于配置 MQTT Broker 的用户名密码认证
51
+
52
+ Attributes:
53
+ username: MQTT 用户名
54
+ password: MQTT 密码
55
+
56
+ 示例:
57
+ auth = AuthConfig(
58
+ username="device_123",
59
+ password="secret_password"
60
+ )
61
+ """
62
+
63
+ username: Optional[str] = None
64
+ password: Optional[str] = None
65
+
66
+
67
+ @dataclass
68
+ class ReconnectConfig:
69
+ """重连配置
70
+
71
+ 配置 MQTT 连接断开后的自动重连行为
72
+
73
+ Attributes:
74
+ enabled: 是否启用自动重连
75
+ interval: 初始重连间隔(秒)
76
+ max_attempts: 最大重连次数(0 = 无限重试)
77
+ backoff_multiplier: 指数退避倍数(每次重连失败后,间隔乘以此倍数)
78
+ max_interval: 最大重连间隔(秒)
79
+
80
+ 示例:
81
+ # 无限重试,指数退避
82
+ reconnect = ReconnectConfig(
83
+ enabled=True,
84
+ interval=5,
85
+ max_attempts=0,
86
+ backoff_multiplier=1.5,
87
+ max_interval=60
88
+ )
89
+
90
+ # 固定间隔,最多重试 10 次
91
+ reconnect = ReconnectConfig(
92
+ interval=5,
93
+ max_attempts=10,
94
+ backoff_multiplier=1.0 # 不使用指数退避
95
+ )
96
+ """
97
+
98
+ enabled: bool = True
99
+ interval: int = 5
100
+ max_attempts: int = 0
101
+ backoff_multiplier: float = 1.5
102
+ max_interval: int = 60
103
+
104
+
105
+ @dataclass
106
+ class MQTTConfig:
107
+ """MQTT 客户端完整配置
108
+
109
+ 所有 MQTT 连接相关的配置参数
110
+
111
+ Attributes:
112
+ broker_host: MQTT Broker 地址
113
+ broker_port: MQTT Broker 端口(1883=明文, 8883=TLS)
114
+ client_id: 客户端标识符(空字符串=自动生成)
115
+ keepalive: 心跳保活时间(秒)
116
+ clean_session: 是否清除会话(False=服务器保持订阅)
117
+ tls: TLS/SSL 配置
118
+ auth: 认证配置
119
+ reconnect: 重连配置
120
+ max_queued_messages: 最大排队消息数(0=无限)
121
+ max_payload_size: 最大消息载荷大小(字节,防止 DoS 攻击)
122
+ log_level: 日志级别(DEBUG|INFO|WARNING|ERROR)
123
+
124
+ 示例:
125
+ # 基础配置(明文连接)
126
+ config = MQTTConfig(
127
+ broker_host="localhost",
128
+ client_id="device_123"
129
+ )
130
+
131
+ # 生产配置(TLS + 认证 + 自动重连)
132
+ config = MQTTConfig(
133
+ broker_host="mqtt.example.com",
134
+ broker_port=8883,
135
+ client_id="device_123",
136
+ clean_session=False, # 保持会话
137
+ tls=TLSConfig(
138
+ enabled=True,
139
+ ca_certs=Path("/etc/ssl/ca.crt")
140
+ ),
141
+ auth=AuthConfig(
142
+ username="device_123",
143
+ password="secret"
144
+ ),
145
+ reconnect=ReconnectConfig(
146
+ interval=5,
147
+ max_attempts=0 # 无限重试
148
+ )
149
+ )
150
+ """
151
+
152
+ # 连接参数
153
+ broker_host: str
154
+ broker_port: int = 1883
155
+ client_id: str = ""
156
+ keepalive: int = 60
157
+ clean_session: bool = False
158
+
159
+ # 子配置对象
160
+ tls: TLSConfig = field(default_factory=TLSConfig)
161
+ auth: AuthConfig = field(default_factory=AuthConfig)
162
+ reconnect: ReconnectConfig = field(default_factory=ReconnectConfig)
163
+
164
+ # 消息限制
165
+ max_queued_messages: int = 0 # 0 = 无限
166
+ max_payload_size: int = 1024 * 1024 # 1MB
167
+
168
+ # 日志级别
169
+ log_level: str = "INFO"
170
+
171
+
172
+ @dataclass
173
+ class RPCConfig:
174
+ """RPC 配置
175
+
176
+ RPC 调用相关的配置参数
177
+
178
+ Attributes:
179
+ default_timeout: 默认超时时间(秒)
180
+ max_concurrent_calls: 最大并发 RPC 调用数
181
+
182
+ 示例:
183
+ rpc_config = RPCConfig(
184
+ default_timeout=30.0,
185
+ max_concurrent_calls=100
186
+ )
187
+ """
188
+
189
+ default_timeout: float = 30.0
190
+ max_concurrent_calls: int = 100
mqttxx/conventions.py ADDED
@@ -0,0 +1,145 @@
1
+ # 约定式 RPC 管理器 - 去角色设计
2
+
3
+ from typing import Any, Optional
4
+ from loguru import logger
5
+
6
+ from .client import MQTTClient
7
+ from .config import RPCConfig
8
+ from .rpc import RPCManager, AuthCallback
9
+
10
+
11
+ class ConventionalRPCManager(RPCManager):
12
+ """约定式 RPC 管理器
13
+
14
+ 设计原则:
15
+ - 约定优于配置
16
+ - 自动订阅本地 topic
17
+ - 自动注入 reply_to
18
+
19
+ 核心功能:
20
+ 1. 初始化时自动订阅 `my_topic`
21
+ 2. 调用时自动将 `my_topic` 注入到 `reply_to`
22
+
23
+ 适用场景:
24
+ - 边缘设备 ↔ 云端(edge/xxx ↔ cloud/xxx)
25
+ - 微服务之间(auth-service ↔ user-service)
26
+ - IoT 网关 ↔ 设备(gateway/001 ↔ device/123)
27
+ - 任何需要简化 RPC 调用的场景
28
+
29
+ 示例:
30
+ # 边缘设备
31
+ rpc = ConventionalRPCManager(client, my_topic="edge/device_123")
32
+
33
+ @rpc.register("get_status")
34
+ async def get_status(params):
35
+ return {"status": "online"}
36
+
37
+ # 调用云端(自动注入 reply_to="edge/device_123")
38
+ config = await rpc.call("cloud/config-service", "get_config")
39
+
40
+ # 云端服务
41
+ rpc = ConventionalRPCManager(client, my_topic="cloud/config-service")
42
+
43
+ # 调用边缘设备(自动注入 reply_to="cloud/config-service")
44
+ status = await rpc.call("edge/device_123", "execute_command")
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ client: MQTTClient,
50
+ my_topic: str,
51
+ config: Optional[RPCConfig] = None,
52
+ auth_callback: Optional[AuthCallback] = None,
53
+ ):
54
+ """初始化约定式 RPC 管理器
55
+
56
+ Args:
57
+ client: MQTTClient 实例
58
+ my_topic: 本节点的 topic(自动订阅,自动注入到 reply_to)
59
+ config: RPC 配置(可选)
60
+ auth_callback: 权限检查回调(可选)
61
+
62
+ 自动行为:
63
+ - 自动订阅 my_topic
64
+ - 自动绑定消息处理器
65
+
66
+ 示例:
67
+ # 边缘设备
68
+ rpc = ConventionalRPCManager(client, my_topic="edge/device_123")
69
+
70
+ # 云端服务
71
+ rpc = ConventionalRPCManager(client, my_topic="cloud/server_001")
72
+
73
+ # 微服务
74
+ rpc = ConventionalRPCManager(client, my_topic="auth-service")
75
+
76
+ # 多层级
77
+ rpc = ConventionalRPCManager(client, my_topic="region/zone/device")
78
+ """
79
+ super().__init__(client, config, auth_callback)
80
+
81
+ self._my_topic = my_topic
82
+
83
+ # 自动订阅
84
+ client.subscribe(my_topic, self.handle_rpc_message)
85
+
86
+ logger.info(f"ConventionalRPCManager 已初始化 - my_topic: {my_topic}")
87
+
88
+ async def call(
89
+ self,
90
+ topic: str,
91
+ method: str,
92
+ params: Any = None,
93
+ timeout: Optional[float] = None,
94
+ reply_to: Optional[str] = None,
95
+ ) -> Any:
96
+ """调用远程方法(自动注入 reply_to)
97
+
98
+ Args:
99
+ topic: 对方的 topic
100
+ method: 方法名
101
+ params: 参数(可选)
102
+ timeout: 超时时间(可选)
103
+ reply_to: 响应 topic(可选,默认使用 my_topic)
104
+
105
+ Returns:
106
+ 方法返回值
107
+
108
+ Raises:
109
+ MQTTXError: 客户端未连接
110
+ RPCTimeoutError: 调用超时
111
+ RPCRemoteError: 远程执行失败
112
+
113
+ 示例:
114
+ # 边缘设备调用云端
115
+ rpc = ConventionalRPCManager(client, my_topic="edge/device_123")
116
+ result = await rpc.call("cloud/server_001", "get_config")
117
+ # 等价于:await super().call("cloud/server_001", "get_config", reply_to="edge/device_123")
118
+
119
+ # 云端调用边缘设备
120
+ rpc = ConventionalRPCManager(client, my_topic="cloud/server_001")
121
+ result = await rpc.call("edge/device_123", "execute_command", params={"cmd": "restart"})
122
+
123
+ # 微服务调用
124
+ rpc = ConventionalRPCManager(client, my_topic="auth-service")
125
+ user = await rpc.call("user-service", "get_user", params={"id": 123})
126
+ """
127
+ # 自动注入 reply_to
128
+ reply_to = reply_to or self._my_topic
129
+
130
+ return await super().call(
131
+ topic=topic,
132
+ method=method,
133
+ params=params,
134
+ timeout=timeout,
135
+ reply_to=reply_to,
136
+ )
137
+
138
+ @property
139
+ def my_topic(self) -> str:
140
+ """获取当前节点的 topic
141
+
142
+ Returns:
143
+ 当前节点订阅的 topic
144
+ """
145
+ return self._my_topic
mqttxx/exceptions.py ADDED
@@ -0,0 +1,160 @@
1
+ # MQTTX 异常定义和错误码
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class ErrorCode(IntEnum):
7
+ """错误码定义(遵循 HTTP 风格分类)
8
+
9
+ 错误码范围:
10
+ - 1xxx: 连接错误
11
+ - 2xxx: 消息错误
12
+ - 3xxx: RPC 错误
13
+ - 4xxx: 权限错误
14
+ """
15
+
16
+ # 连接错误 (1xxx)
17
+ NOT_CONNECTED = 1001
18
+ CONNECTION_FAILED = 1002
19
+ CONNECTION_LOST = 1003
20
+ SUBSCRIBE_FAILED = 1004
21
+ PUBLISH_FAILED = 1005
22
+
23
+ # 消息错误 (2xxx)
24
+ INVALID_JSON = 2001
25
+ INVALID_UTF8 = 2002
26
+ PAYLOAD_TOO_LARGE = 2003
27
+ INVALID_MESSAGE_TYPE = 2004
28
+ MISSING_REQUIRED_FIELD = 2005
29
+
30
+ # RPC 错误 (3xxx)
31
+ METHOD_NOT_FOUND = 3001
32
+ RPC_TIMEOUT = 3002
33
+ RPC_EXECUTION_ERROR = 3003
34
+ TOO_MANY_PENDING_CALLS = 3004
35
+ INVALID_RPC_REQUEST = 3005
36
+ INVALID_RPC_RESPONSE = 3006
37
+
38
+ # 权限错误 (4xxx)
39
+ PERMISSION_DENIED = 4001
40
+ AUTHENTICATION_FAILED = 4002
41
+
42
+
43
+ class MQTTXError(Exception):
44
+ """MQTTX 基础异常
45
+
46
+ 所有 MQTTX 异常的基类,包含错误码和消息
47
+
48
+ Attributes:
49
+ message: 错误消息
50
+ code: 错误码(ErrorCode 枚举)
51
+
52
+ 示例:
53
+ raise MQTTXError("连接失败", ErrorCode.CONNECTION_FAILED)
54
+ """
55
+
56
+ def __init__(self, message: str, code: ErrorCode):
57
+ super().__init__(message)
58
+ self.code = code
59
+ self.message = message
60
+
61
+ def __str__(self) -> str:
62
+ return f"[{self.code.name}] {self.message}"
63
+
64
+ def __repr__(self) -> str:
65
+ return f"{self.__class__.__name__}(code={self.code}, message={self.message!r})"
66
+
67
+
68
+ class ConnectionError(MQTTXError):
69
+ """连接相关异常
70
+
71
+ 用于表示 MQTT 连接、订阅、发布等操作失败
72
+
73
+ 示例:
74
+ raise ConnectionError("客户端未连接", ErrorCode.NOT_CONNECTED)
75
+ """
76
+ pass
77
+
78
+
79
+ class MessageError(MQTTXError):
80
+ """消息处理异常
81
+
82
+ 用于表示消息解析、验证失败
83
+
84
+ 示例:
85
+ raise MessageError("JSON 解析失败", ErrorCode.INVALID_JSON)
86
+ """
87
+ pass
88
+
89
+
90
+ class RPCError(MQTTXError):
91
+ """RPC 基础异常
92
+
93
+ 所有 RPC 相关异常的基类
94
+ """
95
+ pass
96
+
97
+
98
+ class RPCTimeoutError(RPCError):
99
+ """RPC 超时异常
100
+
101
+ 当 RPC 调用超过指定时间未收到响应时抛出
102
+
103
+ 示例:
104
+ raise RPCTimeoutError("RPC 调用超时: get_status")
105
+ """
106
+
107
+ def __init__(self, message: str):
108
+ super().__init__(message, ErrorCode.RPC_TIMEOUT)
109
+
110
+
111
+ class RPCRemoteError(RPCError):
112
+ """远程执行失败异常
113
+
114
+ 当远程方法执行过程中抛出异常时,封装该异常并返回
115
+
116
+ 示例:
117
+ raise RPCRemoteError("远程方法执行失败: division by zero")
118
+ """
119
+
120
+ def __init__(self, message: str):
121
+ super().__init__(message, ErrorCode.RPC_EXECUTION_ERROR)
122
+
123
+
124
+ class RPCMethodNotFoundError(RPCError):
125
+ """方法未找到异常
126
+
127
+ 当调用的 RPC 方法未在远程节点注册时抛出
128
+
129
+ 示例:
130
+ raise RPCMethodNotFoundError("方法未找到: unknown_method")
131
+ """
132
+
133
+ def __init__(self, message: str):
134
+ super().__init__(message, ErrorCode.METHOD_NOT_FOUND)
135
+
136
+
137
+ class PermissionDeniedError(RPCError):
138
+ """权限拒绝异常
139
+
140
+ 当 RPC 调用未通过权限检查时抛出
141
+
142
+ 示例:
143
+ raise PermissionDeniedError("权限拒绝: delete_user")
144
+ """
145
+
146
+ def __init__(self, message: str):
147
+ super().__init__(message, ErrorCode.PERMISSION_DENIED)
148
+
149
+
150
+ class TooManyConcurrentCallsError(RPCError):
151
+ """并发调用过多异常
152
+
153
+ 当并发 RPC 调用数量超过限制时抛出
154
+
155
+ 示例:
156
+ raise TooManyConcurrentCallsError("并发调用超限: 100/100")
157
+ """
158
+
159
+ def __init__(self, message: str):
160
+ super().__init__(message, ErrorCode.TOO_MANY_PENDING_CALLS)
mqttxx/protocol.py ADDED
@@ -0,0 +1,201 @@
1
+ # MQTT RPC 消息协议定义
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Optional, Literal
5
+
6
+ from .exceptions import MessageError, ErrorCode
7
+
8
+
9
+ @dataclass
10
+ class RPCRequest:
11
+ """RPC 请求消息
12
+
13
+ 客户端发起 RPC 调用时构造的消息格式
14
+
15
+ Attributes:
16
+ request_id: 请求唯一标识符(UUID)
17
+ method: 远程方法名
18
+ type: 消息类型(固定为 "rpc_request")
19
+ params: 方法参数(任意类型)
20
+ reply_to: 响应主题(用于接收响应)
21
+ caller_id: 调用者标识符(用于权限检查)
22
+
23
+ 示例:
24
+ request = RPCRequest(
25
+ request_id="123e4567-e89b-12d3-a456-426614174000",
26
+ method="get_status",
27
+ params={"device_id": "dev_001"},
28
+ reply_to="client/response",
29
+ caller_id="client_123"
30
+ )
31
+
32
+ # 序列化为字典(用于 JSON 发送)
33
+ data = request.to_dict()
34
+ """
35
+
36
+ # 必填字段(无默认值)
37
+ request_id: str
38
+ method: str
39
+ # 可选字段(有默认值)
40
+ type: Literal["rpc_request"] = "rpc_request"
41
+ params: Any = None
42
+ reply_to: str = ""
43
+ caller_id: str = ""
44
+
45
+ def to_dict(self) -> dict:
46
+ """转为字典(用于 JSON 序列化)
47
+
48
+ Returns:
49
+ 包含所有字段的字典
50
+ """
51
+ return {
52
+ "type": self.type,
53
+ "request_id": self.request_id,
54
+ "method": self.method,
55
+ "params": self.params,
56
+ "reply_to": self.reply_to,
57
+ "caller_id": self.caller_id,
58
+ }
59
+
60
+ @classmethod
61
+ def from_dict(cls, data: dict) -> "RPCRequest":
62
+ """从字典构造(用于 JSON 反序列化)
63
+
64
+ Args:
65
+ data: 包含消息字段的字典
66
+
67
+ Returns:
68
+ RPCRequest 实例
69
+
70
+ Raises:
71
+ MessageError: 缺少必需字段时抛出
72
+ """
73
+ try:
74
+ return cls(
75
+ request_id=data["request_id"],
76
+ method=data["method"],
77
+ params=data.get("params"),
78
+ reply_to=data.get("reply_to", ""),
79
+ caller_id=data.get("caller_id", ""),
80
+ )
81
+ except KeyError as e:
82
+ raise MessageError(
83
+ f"RPC 请求缺少必需字段: {e}",
84
+ ErrorCode.MISSING_REQUIRED_FIELD
85
+ )
86
+
87
+
88
+ @dataclass
89
+ class RPCResponse:
90
+ """RPC 响应消息
91
+
92
+ 服务端处理 RPC 请求后返回的消息格式
93
+
94
+ Attributes:
95
+ type: 消息类型(固定为 "rpc_response")
96
+ request_id: 对应请求的唯一标识符
97
+ result: 方法返回值(成功时)
98
+ error: 错误消息(失败时)
99
+
100
+ 注意:
101
+ result 和 error 只能有一个非空
102
+
103
+ 示例:
104
+ # 成功响应
105
+ response = RPCResponse(
106
+ request_id="123e4567-e89b-12d3-a456-426614174000",
107
+ result={"status": "online", "temperature": 25.5}
108
+ )
109
+
110
+ # 错误响应
111
+ response = RPCResponse(
112
+ request_id="123e4567-e89b-12d3-a456-426614174000",
113
+ error="方法未找到: unknown_method"
114
+ )
115
+ """
116
+
117
+ type: Literal["rpc_response"] = "rpc_response"
118
+ request_id: str = ""
119
+ result: Any = None
120
+ error: Optional[str] = None
121
+
122
+ def to_dict(self) -> dict:
123
+ """转为字典(用于 JSON 序列化)
124
+
125
+ Returns:
126
+ 包含所有字段的字典
127
+ """
128
+ data = {
129
+ "type": self.type,
130
+ "request_id": self.request_id,
131
+ }
132
+
133
+ if self.error is not None:
134
+ data["error"] = self.error
135
+ else:
136
+ data["result"] = self.result
137
+
138
+ return data
139
+
140
+ @classmethod
141
+ def from_dict(cls, data: dict) -> "RPCResponse":
142
+ """从字典构造(用于 JSON 反序列化)
143
+
144
+ Args:
145
+ data: 包含消息字段的字典
146
+
147
+ Returns:
148
+ RPCResponse 实例
149
+
150
+ Raises:
151
+ MessageError: 缺少必需字段时抛出
152
+ """
153
+ try:
154
+ return cls(
155
+ request_id=data["request_id"],
156
+ result=data.get("result"),
157
+ error=data.get("error"),
158
+ )
159
+ except KeyError as e:
160
+ raise MessageError(
161
+ f"RPC 响应缺少必需字段: {e}",
162
+ ErrorCode.MISSING_REQUIRED_FIELD
163
+ )
164
+
165
+
166
+ def parse_message(data: dict) -> RPCRequest | RPCResponse:
167
+ """解析 RPC 消息(带类型验证)
168
+
169
+ 根据消息的 type 字段自动判断消息类型并解析
170
+
171
+ Args:
172
+ data: JSON 解析后的字典
173
+
174
+ Returns:
175
+ RPCRequest 或 RPCResponse 实例
176
+
177
+ Raises:
178
+ MessageError: 未知消息类型或解析失败
179
+
180
+ 示例:
181
+ data = json.loads(payload)
182
+ message = parse_message(data)
183
+
184
+ if isinstance(message, RPCRequest):
185
+ # 处理请求
186
+ pass
187
+ elif isinstance(message, RPCResponse):
188
+ # 处理响应
189
+ pass
190
+ """
191
+ msg_type = data.get("type")
192
+
193
+ if msg_type == "rpc_request":
194
+ return RPCRequest.from_dict(data)
195
+ elif msg_type == "rpc_response":
196
+ return RPCResponse.from_dict(data)
197
+ else:
198
+ raise MessageError(
199
+ f"未知消息类型: {msg_type}",
200
+ ErrorCode.INVALID_MESSAGE_TYPE
201
+ )