springbootAI 1.8.0__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.
Files changed (175) hide show
  1. spring/__init__.py +66 -0
  2. spring/ai/__init__.py +78 -0
  3. spring/ai/advisors.py +139 -0
  4. spring/ai/annotations.py +74 -0
  5. spring/ai/autoconfig.py +481 -0
  6. spring/ai/core.py +391 -0
  7. spring/ai/etl.py +188 -0
  8. spring/ai/memory.py +109 -0
  9. spring/ai/observability.py +129 -0
  10. spring/ai/providers.py +789 -0
  11. spring/ai/resilience.py +258 -0
  12. spring/ai/tools.py +106 -0
  13. spring/ai/vectorstore.py +303 -0
  14. spring/annotations/__init__.py +188 -0
  15. spring/annotations/cache.py +126 -0
  16. spring/annotations/cloud.py +207 -0
  17. spring/annotations/conditional.py +272 -0
  18. spring/annotations/core.py +864 -0
  19. spring/annotations/messaging.py +107 -0
  20. spring/aop/__init__.py +4 -0
  21. spring/aop/cloud_aop.py +404 -0
  22. spring/aop/comprehensive_aop.py +1015 -0
  23. spring/aop/method_interceptor.py +19 -0
  24. spring/aop/proxy_factory.py +55 -0
  25. spring/cloud/__init__.py +76 -0
  26. spring/cloud/discovery.py +364 -0
  27. spring/cloud/feign.py +469 -0
  28. spring/cloud/gateway.py +452 -0
  29. spring/cloud/load_balancer.py +149 -0
  30. spring/cloud/seata.py +557 -0
  31. spring/cloud/sentinel.py +525 -0
  32. spring/cloud/tracer.py +337 -0
  33. spring/config/__init__.py +21 -0
  34. spring/config/binding.py +206 -0
  35. spring/config/config_loader.py +405 -0
  36. spring/context/__init__.py +13 -0
  37. spring/context/application_context.py +589 -0
  38. spring/context/bean_definition.py +70 -0
  39. spring/context/bean_factory.py +1052 -0
  40. spring/context/registry.py +58 -0
  41. spring/context/scanner.py +106 -0
  42. spring/core/__init__.py +3 -0
  43. spring/core/graceful_shutdown.py +196 -0
  44. spring/core/typing_utils.py +50 -0
  45. spring/csv/__init__.py +52 -0
  46. spring/csv/annotations.py +402 -0
  47. spring/csv/converters.py +69 -0
  48. spring/csv/easy_csv.py +95 -0
  49. spring/csv/exceptions.py +27 -0
  50. spring/csv/reader.py +195 -0
  51. spring/csv/writer.py +155 -0
  52. spring/data/__init__.py +54 -0
  53. spring/data/page.py +181 -0
  54. spring/data/repository.py +274 -0
  55. spring/data/specification.py +228 -0
  56. spring/datasource/__init__.py +66 -0
  57. spring/datasource/annotations.py +133 -0
  58. spring/datasource/context.py +69 -0
  59. spring/datasource/dynamic.py +148 -0
  60. spring/event/__init__.py +7 -0
  61. spring/event/publisher.py +69 -0
  62. spring/excel/__init__.py +51 -0
  63. spring/excel/annotations.py +405 -0
  64. spring/excel/converters.py +231 -0
  65. spring/excel/easy_excel.py +94 -0
  66. spring/excel/exceptions.py +31 -0
  67. spring/excel/reader.py +254 -0
  68. spring/excel/style.py +95 -0
  69. spring/excel/writer.py +197 -0
  70. spring/i18n/__init__.py +97 -0
  71. spring/i18n/accessor.py +94 -0
  72. spring/i18n/auto_config.py +177 -0
  73. spring/i18n/holder.py +106 -0
  74. spring/i18n/locale.py +152 -0
  75. spring/i18n/locale_resolver.py +367 -0
  76. spring/i18n/message_source.py +250 -0
  77. spring/i18n/middleware.py +79 -0
  78. spring/i18n/properties.py +168 -0
  79. spring/i18n/sources.py +255 -0
  80. spring/logging/__init__.py +1 -0
  81. spring/logging/loguru_logger.py +228 -0
  82. spring/main.py +378 -0
  83. spring/messaging/__init__.py +1 -0
  84. spring/messaging/rabbitmq.py +302 -0
  85. spring/monitoring/__init__.py +1 -0
  86. spring/monitoring/prometheus.py +199 -0
  87. spring/orm/__init__.py +258 -0
  88. spring/orm/database.py +222 -0
  89. spring/orm/ddl_auto.py +1217 -0
  90. spring/orm/migration.py +419 -0
  91. spring/orm/mybatis_integration.py +400 -0
  92. spring/orm/pymybatis/__init__.py +86 -0
  93. spring/orm/pymybatis/annotations/__init__.py +30 -0
  94. spring/orm/pymybatis/annotations/annotations.py +332 -0
  95. spring/orm/pymybatis/cache/__init__.py +47 -0
  96. spring/orm/pymybatis/cache/cache.py +371 -0
  97. spring/orm/pymybatis/cache/redis_cache.py +434 -0
  98. spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
  99. spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
  100. spring/orm/pymybatis/configuration.py +525 -0
  101. spring/orm/pymybatis/core/__init__.py +10 -0
  102. spring/orm/pymybatis/core/sql_session.py +1382 -0
  103. spring/orm/pymybatis/core/sql_session_factory.py +76 -0
  104. spring/orm/pymybatis/dialect/__init__.py +9 -0
  105. spring/orm/pymybatis/dialect/dialect.py +445 -0
  106. spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
  107. spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
  108. spring/orm/pymybatis/interceptor/__init__.py +31 -0
  109. spring/orm/pymybatis/interceptor/interceptor.py +427 -0
  110. spring/orm/pymybatis/mapper/__init__.py +9 -0
  111. spring/orm/pymybatis/mapper/mapper.py +540 -0
  112. spring/orm/pymybatis/metrics/__init__.py +41 -0
  113. spring/orm/pymybatis/metrics/metrics.py +595 -0
  114. spring/orm/pymybatis/pool/__init__.py +9 -0
  115. spring/orm/pymybatis/pool/connection_pool.py +711 -0
  116. spring/orm/pymybatis/security/__init__.py +19 -0
  117. spring/orm/pymybatis/security/access_control.py +415 -0
  118. spring/orm/pymybatis/security/password_encoder.py +293 -0
  119. spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
  120. spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
  121. spring/orm/pymybatis/transaction/__init__.py +9 -0
  122. spring/orm/pymybatis/transaction/transaction.py +288 -0
  123. spring/orm/pymybatis/type_handler/__init__.py +37 -0
  124. spring/orm/pymybatis/type_handler/type_handler.py +473 -0
  125. spring/orm/pymybatis/version.py +9 -0
  126. spring/orm/pymybatis/xml_parser/__init__.py +9 -0
  127. spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
  128. spring/retry/__init__.py +12 -0
  129. spring/retry/retry_annotations.py +71 -0
  130. spring/retry/retry_decorator.py +155 -0
  131. spring/scheduling/__init__.py +3 -0
  132. spring/scheduling/scheduler.py +389 -0
  133. spring/security/__init__.py +39 -0
  134. spring/security/jwt_utils.py +281 -0
  135. spring/security/replay_protection.py +206 -0
  136. spring/security/secret_manager.py +226 -0
  137. spring/security/security_aop.py +248 -0
  138. spring/security/security_context.py +172 -0
  139. spring/test/__init__.py +45 -0
  140. spring/test/slicing.py +341 -0
  141. spring/tracing/__init__.py +11 -0
  142. spring/tracing/skywalking.py +229 -0
  143. spring/tx/__init__.py +52 -0
  144. spring/tx/events.py +172 -0
  145. spring/tx/synchronization.py +143 -0
  146. spring/utils/__init__.py +5 -0
  147. spring/utils/banner.py +32 -0
  148. spring/utils/logger.py +73 -0
  149. spring/utils/redis_client.py +526 -0
  150. spring/validation/__init__.py +55 -0
  151. spring/validation/aop.py +141 -0
  152. spring/validation/constraints.py +357 -0
  153. spring/validation/exceptions.py +55 -0
  154. spring/validation/validator.py +139 -0
  155. spring/web/__init__.py +12 -0
  156. spring/web/actuator.py +319 -0
  157. spring/web/exception_handler.py +61 -0
  158. spring/web/health.py +399 -0
  159. spring/web/interceptor.py +91 -0
  160. spring/web/result.py +44 -0
  161. spring/web/swagger.py +601 -0
  162. spring/web/web_context.py +755 -0
  163. spring/websocket/__init__.py +86 -0
  164. spring/websocket/annotations.py +169 -0
  165. spring/websocket/broker.py +238 -0
  166. spring/websocket/exceptions.py +26 -0
  167. spring/websocket/handler.py +243 -0
  168. spring/websocket/router.py +526 -0
  169. spring/websocket/session.py +216 -0
  170. springbootai-1.8.0.dist-info/METADATA +2796 -0
  171. springbootai-1.8.0.dist-info/RECORD +175 -0
  172. springbootai-1.8.0.dist-info/WHEEL +5 -0
  173. springbootai-1.8.0.dist-info/entry_points.txt +2 -0
  174. springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
  175. springbootai-1.8.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,86 @@
1
+ """SpringBootAI WebSocket 实时通信模块 —— 注解驱动的 WebSocket 端点与消息路由(对齐
2
+ JSR-356 ``@ServerEndpoint`` + Spring WebSocket ``@MessageMapping`` / ``@SendTo`` 体系)。
3
+
4
+ 模块组成:
5
+ - **session**: ``WebSocketSession`` 会话抽象 + ``WebSocketSessionRegistry`` 全局会话注册表
6
+ (对齐 Spring ``WebSocketSession`` / ``WebSocketHandlerRegistry``)
7
+ - **handler**: ``WebSocketHandler`` 接口 + ``TextWebSocketHandler`` / ``BinaryWebSocketHandler``
8
+ 便捷基类 + ``@ServerEndpoint`` 类级注解(JSR-356 风格生命周期钩子)
9
+ - **annotations**: ``@MessageMapping`` / ``@SendTo`` / ``@SendToUser`` / ``@SubscribeMapping``
10
+ 方法级注解(Spring STOMP 风格消息路由)
11
+ - **broker**: ``InMemoryBroker`` 主题式发布订阅代理(对齐 Spring ``SimpleBrokerMessageHandler``)
12
+ + ``SimpMessageSendingOperations`` 发送操作 API
13
+ - **router**: ``WebSocketRouter`` 把注解端点转换为 Starlette/FastAPI WebSocket 路由
14
+ - **exceptions**: ``WebSocketException`` 异常族
15
+
16
+ 设计原则:**复用项目既有范式,不重复造轮子**。
17
+ - ``@ServerEndpoint`` / ``@MessageMapping`` 等注解复用 ``SpringAnnotation`` 元数据描述符范式
18
+ (与 ``spring.excel`` / ``spring.csv`` / ``spring.validation`` 一致)。
19
+ - 不依赖第三方 WebSocket 库;直接基于 Starlette ``WebSocket`` 原生能力。
20
+ - 不实现完整 STOMP 协议(复杂度过高);采用简化 JSON 消息帧,对齐 Spring ``SimpleBroker``
21
+ 的核心语义(destination 路由 + topic 订阅)。
22
+
23
+ 与 Java 的差异:
24
+ - JSR-356 在 Java EE 容器内注册端点;本实现通过 ``WebSocketRouter.install(app)`` 挂载到
25
+ Starlette/FastAPI 应用。
26
+ - Spring STOMP 用 ``@EnableWebSocketMessageBroker`` + ``WebSocketMessageBrokerConfigurer``;
27
+ 本实现用 ``MessageBrokerConfigurer`` + ``WebSocketRouter`` 组合,更轻量。
28
+ - 不支持 STOMP ACK/NACK/事务(可按需扩展 ``InMemoryBroker``)。
29
+ """
30
+ from .exceptions import (
31
+ WebSocketException,
32
+ WebSocketConnectionException,
33
+ WebSocketHandlerException,
34
+ MessageBrokerException,
35
+ )
36
+ from .session import (
37
+ WebSocketSession,
38
+ WebSocketSessionRegistry,
39
+ global_session_registry,
40
+ )
41
+ from .handler import (
42
+ WebSocketHandler,
43
+ TextWebSocketHandler,
44
+ BinaryWebSocketHandler,
45
+ ServerEndpoint,
46
+ AnnotatedEndpointHandler,
47
+ discover_server_endpoints,
48
+ )
49
+ from .annotations import (
50
+ MessageMapping,
51
+ SendTo,
52
+ SendToUser,
53
+ SubscribeMapping,
54
+ MessageEndpoint,
55
+ collect_message_mappings,
56
+ MessageMappingModel,
57
+ )
58
+ from .broker import (
59
+ InMemoryBroker,
60
+ SimpMessageSendingOperations,
61
+ MessageBrokerConfigurer,
62
+ broker_registry,
63
+ )
64
+ from .router import WebSocketRouter, MessageEndpointDispatcher, install_websocket_routes
65
+
66
+ __version__ = "1.0.0"
67
+
68
+ __all__ = [
69
+ # 异常
70
+ "WebSocketException", "WebSocketConnectionException",
71
+ "WebSocketHandlerException", "MessageBrokerException",
72
+ # 会话
73
+ "WebSocketSession", "WebSocketSessionRegistry", "global_session_registry",
74
+ # Handler / @ServerEndpoint
75
+ "WebSocketHandler", "TextWebSocketHandler", "BinaryWebSocketHandler",
76
+ "ServerEndpoint", "AnnotatedEndpointHandler", "discover_server_endpoints",
77
+ # 注解
78
+ "MessageMapping", "SendTo", "SendToUser", "SubscribeMapping", "MessageEndpoint",
79
+ "MessageMappingModel", "collect_message_mappings",
80
+ # Broker
81
+ "InMemoryBroker", "SimpMessageSendingOperations",
82
+ "MessageBrokerConfigurer", "broker_registry",
83
+ # Router
84
+ "WebSocketRouter", "MessageEndpointDispatcher", "install_websocket_routes",
85
+ "__version__",
86
+ ]
@@ -0,0 +1,169 @@
1
+ """WebSocket 消息映射注解(对齐 Spring ``@MessageMapping`` / ``@SendTo`` / ``@SendToUser``
2
+ / ``@SubscribeMapping``)。
3
+
4
+ 注解语义(与 Spring STOMP 一致):
5
+ - ``@MessageMapping("/chat")`` 方法处理发往 ``/chat`` 的消息(``/app`` 前缀由配置剥离)。
6
+ - ``@SendTo("/topic/greetings")`` 方法返回值自动广播到 ``/topic/greetings``。
7
+ 不标注时默认回发给发送者(对齐 Spring ``@SendToUser`` 默认)。
8
+ - ``@SendToUser`` 方法返回值定向发给发送者(对齐 Spring ``@SendToUser``)。
9
+ - ``@SubscribeMapping("/topic/x")`` 客户端订阅时触发,方法返回值作为初始数据回发给订阅者。
10
+ - ``@MessageEndpoint`` 类级注解,标记一个类包含 ``@MessageMapping`` 方法
11
+ (对齐 Spring ``@MessageMapping``+``@Controller`` 组合)。
12
+
13
+ 注解本身只注册元数据;实际方法包装在 ``MessageEndpointDispatcher``(见 router.py)中完成。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Callable, List, Optional, Type, Union
18
+
19
+ from spring.annotations.core import SpringAnnotation
20
+
21
+
22
+ # ==================== 类级注解 ====================
23
+
24
+ class MessageEndpoint(SpringAnnotation):
25
+ """``@MessageEndpoint`` 类级注解,标记一个类为消息端点(含 ``@MessageMapping`` 方法)。
26
+
27
+ 等价于 Spring ``@Controller`` + ``@MessageMapping`` 的组合约定。
28
+ """
29
+
30
+ _annotation_type = "message_endpoint"
31
+
32
+
33
+ # ==================== 方法级注解 ====================
34
+
35
+ class MessageMapping(SpringAnnotation):
36
+ """``@MessageMapping("/chat")`` 方法级注解,声明消息处理方法。
37
+
38
+ 用法::
39
+
40
+ @MessageEndpoint
41
+ class GreetingController:
42
+ @MessageMapping("/greet")
43
+ @SendTo("/topic/greetings")
44
+ def greet(self, message):
45
+ return {"content": "Hello, " + message["name"]}
46
+
47
+ 客户端发送 ``{"destination": "/app/greet", "payload": {"name": "Tom"}}`` 时触发。
48
+ """
49
+
50
+ _annotation_type = "message_mapping"
51
+
52
+ def __init__(self, value: str = ""):
53
+ super().__init__(value=value)
54
+
55
+
56
+ class SendTo(SpringAnnotation):
57
+ """``@SendTo("/topic/greetings")`` 方法级注解,把返回值广播到指定 destination。
58
+
59
+ 不标注时,方法返回值默认回发给发送者(等价于 ``@SendToUser``)。
60
+ """
61
+
62
+ _annotation_type = "send_to"
63
+
64
+ def __init__(self, value: str = ""):
65
+ # 支持单 destination 或多 destination(逗号分隔)
66
+ dests = [v.strip() for v in value.split(",") if v.strip()] if value else []
67
+ super().__init__(value=value, destinations=dests)
68
+
69
+
70
+ class SendToUser(SpringAnnotation):
71
+ """``@SendToUser`` 方法级注解,把返回值定向发给发送者(不广播)。
72
+
73
+ 可指定 ``broadcast``(False 时只发给当前会话;True 时发给该用户所有会话)。
74
+ """
75
+
76
+ _annotation_type = "send_to_user"
77
+
78
+ def __init__(self, broadcast: bool = False):
79
+ super().__init__(broadcast=broadcast)
80
+
81
+
82
+ class SubscribeMapping(SpringAnnotation):
83
+ """``@SubscribeMapping("/topic/init")`` 方法级注解,订阅时触发并返回初始数据。
84
+
85
+ 与 ``@MessageMapping`` 区别:``@SubscribeMapping`` 在客户端订阅 destination 时触发,
86
+ 返回值直接回发给订阅者(不经过 broker 广播)。
87
+ """
88
+
89
+ _annotation_type = "subscribe_mapping"
90
+
91
+ def __init__(self, value: str = ""):
92
+ super().__init__(value=value)
93
+
94
+
95
+ # ==================== 元数据模型 ====================
96
+
97
+ class MessageMappingModel:
98
+ """解析后的消息映射元数据,供 ``router.MessageEndpointDispatcher`` 消费。"""
99
+
100
+ __slots__ = (
101
+ "method_name", "destination", "send_to", "send_to_user",
102
+ "send_to_user_broadcast", "subscribe_destination", "is_subscribe",
103
+ )
104
+
105
+ def __init__(
106
+ self,
107
+ method_name: str,
108
+ destination: str,
109
+ send_to: Optional[List[str]] = None,
110
+ send_to_user: bool = False,
111
+ send_to_user_broadcast: bool = False,
112
+ subscribe_destination: str = "",
113
+ is_subscribe: bool = False,
114
+ ):
115
+ self.method_name = method_name
116
+ self.destination = destination
117
+ self.send_to = send_to or []
118
+ self.send_to_user = send_to_user
119
+ self.send_to_user_broadcast = send_to_user_broadcast
120
+ self.subscribe_destination = subscribe_destination
121
+ self.is_subscribe = is_subscribe
122
+
123
+
124
+ # ==================== 元数据收集 ====================
125
+
126
+ def collect_message_mappings(cls: Type) -> List[MessageMappingModel]:
127
+ """收集类上所有 ``@MessageMapping`` / ``@SubscribeMapping`` 方法的元数据。
128
+
129
+ 遍历 ``cls.__dict__``(仅本类,不含继承),为每个标注方法构造 ``MessageMappingModel``。
130
+ 同时读取方法上的 ``@SendTo`` / ``@SendToUser`` 决定返回值去向。
131
+ """
132
+ models: List[MessageMappingModel] = []
133
+ for name, method in vars(cls).items():
134
+ if not callable(method):
135
+ continue
136
+ annotations = getattr(method, "__spring_annotations__", []) or []
137
+ # 优先 @MessageMapping
138
+ msg_mapping = next((a for a in annotations if isinstance(a, MessageMapping)), None)
139
+ sub_mapping = next((a for a in annotations if isinstance(a, SubscribeMapping)), None)
140
+ if msg_mapping is None and sub_mapping is None:
141
+ continue
142
+ send_to = next((a for a in annotations if isinstance(a, SendTo)), None)
143
+ send_to_user = next((a for a in annotations if isinstance(a, SendToUser)), None)
144
+
145
+ send_to_dests = send_to.destinations if send_to else []
146
+ is_subscribe = sub_mapping is not None
147
+ destination = (sub_mapping.value if is_subscribe else msg_mapping.value) or ""
148
+
149
+ models.append(MessageMappingModel(
150
+ method_name=name,
151
+ destination=destination,
152
+ send_to=send_to_dests,
153
+ send_to_user=bool(send_to_user) or (not send_to_dests and not is_subscribe),
154
+ send_to_user_broadcast=getattr(send_to_user, "broadcast", False) if send_to_user else False,
155
+ subscribe_destination=destination if is_subscribe else "",
156
+ is_subscribe=is_subscribe,
157
+ ))
158
+ return models
159
+
160
+
161
+ __all__ = [
162
+ "MessageEndpoint",
163
+ "MessageMapping",
164
+ "SendTo",
165
+ "SendToUser",
166
+ "SubscribeMapping",
167
+ "MessageMappingModel",
168
+ "collect_message_mappings",
169
+ ]
@@ -0,0 +1,238 @@
1
+ """内存消息代理 + 发送操作 API(对齐 Spring ``SimpleBrokerMessageHandler`` +
2
+ ``SimpMessageSendingOperations``)。
3
+
4
+ ``InMemoryBroker`` 主题式发布订阅代理:
5
+ - ``subscribe(destination, session)`` 会话订阅 destination
6
+ - ``unsubscribe(destination, session_id)`` 会话取消订阅(按 session_id)
7
+ - ``unsubscribe_all(session_id)`` 会话退出时取消其所有订阅
8
+ - ``publish(destination, message, exclude)`` 向 destination 的所有订阅者推送 JSON 消息
9
+ - ``subscribers(destination)`` 返回 destination 的订阅会话列表
10
+
11
+ destination 命名约定(对齐 Spring STOMP):
12
+ - ``/topic/*`` 主题广播(多客户端可订阅)
13
+ - ``/queue/*`` 队列(点对点,通常与 ``@SendToUser`` 配合)
14
+ - ``/app/*`` 应用消息(``@MessageMapping`` 入口,前缀由 ``MessageBrokerConfigurer`` 剥离)
15
+
16
+ ``SimpMessageSendingOperations`` 提供高阶 API:
17
+ - ``convert_and_send(destination, payload)`` 转换 payload 为 JSON 并发布到 destination
18
+ - ``convert_and_send_to_user(user, payload)`` 定向推送给用户
19
+
20
+ ``MessageBrokerConfigurer`` 配置入口:
21
+ - ``application_destination_prefixes``:``@MessageMapping`` 入口前缀(默认 ``["/app"]``)
22
+ - ``broker_prefixes``:broker 处理的前缀(默认 ``["/topic", "/queue"]``)
23
+ - ``user_destination_prefix``:用户私有目的地前缀(默认 ``/user``)
24
+
25
+ ``broker_registry`` 全局单例(``InMemoryBroker`` + ``SimpMessageSendingOperations``)。
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import logging
30
+ import threading
31
+ from typing import Any, Dict, Iterable, List, Optional, Set
32
+
33
+ from .exceptions import MessageBrokerException
34
+ from .session import WebSocketSession, global_session_registry
35
+
36
+ logger = logging.getLogger("Spring.WebSocket.Broker")
37
+
38
+
39
+ class InMemoryBroker:
40
+ """内存主题式消息代理(对齐 Spring ``SimpleBrokerMessageHandler``)。
41
+
42
+ 线程安全:所有订阅/发布操作加锁;推送是 ``async``,需事件循环驱动。
43
+ """
44
+
45
+ def __init__(self):
46
+ # destination -> {session_id: WebSocketSession}
47
+ self._subscriptions: Dict[str, Dict[str, WebSocketSession]] = {}
48
+ self._lock = threading.RLock()
49
+
50
+ def subscribe(self, destination: str, session: WebSocketSession) -> int:
51
+ """会话订阅 destination;返回该 destination 当前订阅数。"""
52
+ if not destination:
53
+ raise MessageBrokerException("destination 不能为空")
54
+ with self._lock:
55
+ bucket = self._subscriptions.setdefault(destination, {})
56
+ bucket[session.id] = session
57
+ return len(bucket)
58
+
59
+ def unsubscribe(self, destination: str, session_id: str) -> bool:
60
+ """会话取消订阅指定 destination;返回是否成功取消。"""
61
+ with self._lock:
62
+ bucket = self._subscriptions.get(destination)
63
+ if bucket is None:
64
+ return False
65
+ return bucket.pop(session_id, None) is not None
66
+
67
+ def unsubscribe_all(self, session_id: str) -> int:
68
+ """会话退出时取消其所有订阅;返回取消的订阅数。"""
69
+ n = 0
70
+ with self._lock:
71
+ for destination, bucket in list(self._subscriptions.items()):
72
+ if bucket.pop(session_id, None) is not None:
73
+ n += 1
74
+ if not bucket:
75
+ self._subscriptions.pop(destination, None)
76
+ return n
77
+
78
+ def subscribers(self, destination: str) -> List[WebSocketSession]:
79
+ """返回 destination 的订阅会话列表(拷贝)。"""
80
+ with self._lock:
81
+ bucket = self._subscriptions.get(destination, {})
82
+ return list(bucket.values())
83
+
84
+ def subscriber_count(self, destination: str) -> int:
85
+ with self._lock:
86
+ return len(self._subscriptions.get(destination, {}))
87
+
88
+ def destinations(self) -> List[str]:
89
+ with self._lock:
90
+ return list(self._subscriptions.keys())
91
+
92
+ def clear(self) -> None:
93
+ with self._lock:
94
+ self._subscriptions.clear()
95
+
96
+ async def publish(self, destination: str, message: Any,
97
+ exclude: Optional[Iterable[str]] = None) -> int:
98
+ """向 destination 的所有订阅者推送 JSON 消息;返回成功推送数。
99
+
100
+ - ``exclude``:要排除的 session_id 列表。
101
+ - 已关闭的会话自动跳过,并在推送后清理其订阅。
102
+ """
103
+ if not destination:
104
+ raise MessageBrokerException("destination 不能为空")
105
+ excluded: Set[str] = set(exclude or [])
106
+ sent = 0
107
+ stale: List[str] = []
108
+ with self._lock:
109
+ bucket = self._subscriptions.get(destination, {})
110
+ targets = [s for sid, s in bucket.items() if sid not in excluded]
111
+ for session in targets:
112
+ if not session.is_open:
113
+ stale.append(session.id)
114
+ continue
115
+ try:
116
+ await session.send_json(_wrap_message(destination, message))
117
+ sent += 1
118
+ except Exception as exc:
119
+ logger.warning("publish to session %s failed: %s", session.id, exc)
120
+ stale.append(session.id)
121
+ # 清理失效会话的订阅
122
+ if stale:
123
+ with self._lock:
124
+ bucket = self._subscriptions.get(destination)
125
+ if bucket is not None:
126
+ for sid in stale:
127
+ bucket.pop(sid, None)
128
+ if not bucket:
129
+ self._subscriptions.pop(destination, None)
130
+ return sent
131
+
132
+
133
+ def _wrap_message(destination: str, payload: Any) -> Dict[str, Any]:
134
+ """构造标准消息帧:``{"destination": ..., "payload": ...}``。"""
135
+ return {"destination": destination, "payload": payload}
136
+
137
+
138
+ # ==================== SimpMessageSendingOperations ====================
139
+
140
+ class SimpMessageSendingOperations:
141
+ """高阶消息发送 API(对齐 Spring ``SimpMessageSendingOperations``)。"""
142
+
143
+ def __init__(self, broker: InMemoryBroker,
144
+ session_registry=global_session_registry):
145
+ self._broker = broker
146
+ self._session_registry = session_registry
147
+
148
+ @property
149
+ def broker(self) -> InMemoryBroker:
150
+ return self._broker
151
+
152
+ async def convert_and_send(self, destination: str, payload: Any,
153
+ exclude: Optional[Iterable[str]] = None) -> int:
154
+ """转换 payload 为 JSON 并发布到 destination。"""
155
+ return await self._broker.publish(destination, payload, exclude=exclude)
156
+
157
+ async def convert_and_send_to_user(self, user: str, destination: str, payload: Any) -> int:
158
+ """定向推送给用户:消息发到该用户的所有会话,destination 作为元数据。
159
+
160
+ 实现:直接通过 ``session_registry.send_to_user`` 推送,不走 broker 订阅。
161
+ """
162
+ message = _wrap_message(destination, payload)
163
+ return await self._session_registry.send_to_user(user, message, as_json=True)
164
+
165
+
166
+ # ==================== MessageBrokerConfigurer ====================
167
+
168
+ class MessageBrokerConfigurer:
169
+ """消息代理配置器(对齐 Spring ``@EnableWebSocketMessageBroker`` +
170
+ ``WebSocketMessageBrokerConfigurer``)。
171
+
172
+ 配置项:
173
+ - ``application_destination_prefixes``:``@MessageMapping`` 入口前缀(默认 ``["/app"]``)。
174
+ 客户端发往 ``/app/greet`` 的消息被路由到 ``@MessageMapping("/greet")``。
175
+ - ``broker_prefixes``:broker 处理的前缀(默认 ``["/topic", "/queue"]``)。
176
+ ``@SendTo("/topic/x")`` 的消息直接由 broker 广播。
177
+ - ``user_destination_prefix``:用户私有目的地前缀(默认 ``/user``)。
178
+ """
179
+
180
+ def __init__(
181
+ self,
182
+ application_destination_prefixes: Optional[List[str]] = None,
183
+ broker_prefixes: Optional[List[str]] = None,
184
+ user_destination_prefix: str = "/user",
185
+ ):
186
+ self._app_prefixes: List[str] = list(application_destination_prefixes or ["/app"])
187
+ self._broker_prefixes: List[str] = list(broker_prefixes or ["/topic", "/queue"])
188
+ self._user_prefix: str = user_destination_prefix
189
+ self._broker = InMemoryBroker()
190
+ self._sending_ops = SimpMessageSendingOperations(self._broker)
191
+
192
+ @property
193
+ def broker(self) -> InMemoryBroker:
194
+ return self._broker
195
+
196
+ @property
197
+ def sending_operations(self) -> SimpMessageSendingOperations:
198
+ return self._sending_ops
199
+
200
+ @property
201
+ def application_destination_prefixes(self) -> List[str]:
202
+ return list(self._app_prefixes)
203
+
204
+ @property
205
+ def broker_prefixes(self) -> List[str]:
206
+ return list(self._broker_prefixes)
207
+
208
+ @property
209
+ def user_destination_prefix(self) -> str:
210
+ return self._user_prefix
211
+
212
+ def strip_app_prefix(self, destination: str) -> Optional[str]:
213
+ """剥离 ``/app`` 前缀,返回 ``@MessageMapping`` 匹配路径;非入口返回 None。"""
214
+ for prefix in self._app_prefixes:
215
+ if destination == prefix:
216
+ return ""
217
+ if destination.startswith(prefix + "/"):
218
+ return destination[len(prefix):]
219
+ return None
220
+
221
+ def is_broker_destination(self, destination: str) -> bool:
222
+ """destination 是否由 broker 直接处理(``/topic`` / ``/queue`` 等)。"""
223
+ for prefix in self._broker_prefixes:
224
+ if destination == prefix or destination.startswith(prefix + "/"):
225
+ return True
226
+ return False
227
+
228
+
229
+ # 全局单例(默认配置)
230
+ broker_registry = MessageBrokerConfigurer()
231
+
232
+
233
+ __all__ = [
234
+ "InMemoryBroker",
235
+ "SimpMessageSendingOperations",
236
+ "MessageBrokerConfigurer",
237
+ "broker_registry",
238
+ ]
@@ -0,0 +1,26 @@
1
+ """WebSocket 模块异常族(对齐 Spring ``WebSocketException`` 体系)。"""
2
+ from __future__ import annotations
3
+
4
+
5
+ class WebSocketException(Exception):
6
+ """WebSocket 模块根异常(对齐 Spring ``WebSocketException``)。"""
7
+
8
+
9
+ class WebSocketConnectionException(WebSocketException):
10
+ """连接级异常(握手失败、连接断开等)。"""
11
+
12
+
13
+ class WebSocketHandlerException(WebSocketException):
14
+ """处理器执行异常(``on_message`` 抛错、消息路由失败等)。"""
15
+
16
+
17
+ class MessageBrokerException(WebSocketException):
18
+ """消息代理异常(订阅失败、destination 解析失败等)。"""
19
+
20
+
21
+ __all__ = [
22
+ "WebSocketException",
23
+ "WebSocketConnectionException",
24
+ "WebSocketHandlerException",
25
+ "MessageBrokerException",
26
+ ]