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
spring/test/slicing.py ADDED
@@ -0,0 +1,341 @@
1
+ """Spring Boot 风格测试切片(对齐 ``@SpringBootTest`` / ``@WebMvcTest`` / ``@DataJpaTest``)。
2
+
3
+ 提供三种测试上下文切片,复用既有 ``ApplicationContext`` / ``WebApplicationContext`` /
4
+ ``DdlAutoManager`` / ``PagingAndSortingRepository`` 基础设施:
5
+
6
+ - **``SpringBootTest``**:全量应用上下文(扫描+装配所有 Bean),对齐 ``@SpringBootTest``。
7
+ - **``WebMvcTest``**:仅 Web 切片——只注册指定 Controller(依赖用 ``MagicMock`` 注入),
8
+ 返回 FastAPI ``TestClient``,对齐 ``@WebMvcTest``。
9
+ - **``DataJpaTest``**:仅数据切片——内存 SQLite + ``DdlAutoManager`` 建表 +
10
+ ``PagingAndSortingRepository`` 工厂,对齐 ``@DataJpaTest``。
11
+
12
+ 设计原则:**复用既有范式**,不重复造轮子;切片上下文提供 ``close()`` 清理,便于 pytest 夹具使用。
13
+
14
+ 与 Java 的差异:
15
+ - Spring Boot 切片用专用 ``ApplicationContextInitializer`` 裁剪自动配置;本实现通过手动注册
16
+ 指定 Bean + Mock 依赖实现等价裁剪,更轻量。
17
+ - ``@WebMvcTest`` 在 Spring 中自动 Mock ``@Service``/``@Repository``;本实现 Mock 构造函数依赖。
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import inspect
22
+ import os
23
+ import sqlite3
24
+ import tempfile
25
+ from typing import Any, Dict, List, Optional, Type
26
+ from unittest.mock import MagicMock
27
+
28
+ import yaml
29
+
30
+
31
+ # ==================== 通用:内存连接池(DataJpaTest 复用)====================
32
+
33
+ class _PooledConn:
34
+ """连接池包装:``close()`` 为 no-op(池化语义),其余委托底层连接。"""
35
+
36
+ def __init__(self, conn):
37
+ self._conn = conn
38
+
39
+ def cursor(self, *a, **k):
40
+ return self._conn.cursor(*a, **k)
41
+
42
+ def commit(self):
43
+ return self._conn.commit()
44
+
45
+ def rollback(self):
46
+ return self._conn.rollback()
47
+
48
+ def close(self):
49
+ return None
50
+
51
+ def __getattr__(self, item):
52
+ return getattr(self._conn, item)
53
+
54
+
55
+ class _DbutilsPooled:
56
+ """DBUtils 风格池化连接:``.connection`` 暴露底层连接(``DdlAutoManager`` 期望)。"""
57
+
58
+ def __init__(self, conn):
59
+ self.connection = conn
60
+
61
+
62
+ class TestPool:
63
+ """三接口内存连接池:
64
+ - ``get_connection``/``return_connection`` + ``.connection``:``DdlAutoManager``
65
+ - ``connection()``:``PagingAndSortingRepository``
66
+ """
67
+
68
+ def __init__(self, conn):
69
+ self._conn = conn
70
+
71
+ def get_connection(self):
72
+ return _DbutilsPooled(self._conn)
73
+
74
+ def return_connection(self, pooled):
75
+ return None
76
+
77
+ def connection(self):
78
+ return _PooledConn(self._conn)
79
+
80
+ def get_pool_stats(self):
81
+ return {"dialect": "sqlite"}
82
+
83
+
84
+ # ==================== 工具 ====================
85
+
86
+ def _write_temp_config(config: Dict[str, Any]) -> str:
87
+ """把配置字典写到临时 yml 文件,返回路径。"""
88
+ fd, path = tempfile.mkstemp(suffix=".yml", prefix="springboot_test_")
89
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
90
+ yaml.safe_dump(config or {}, f, allow_unicode=True)
91
+ return path
92
+
93
+
94
+ def _instantiate_with_mocks(cls: Type) -> Any:
95
+ """实例化类,所有非 self 构造参数用 ``MagicMock`` 注入(``@WebMvcTest`` Mock 依赖)。
96
+
97
+ 无论参数是否有默认值都注入 Mock,确保 ``@Autowired`` 依赖被替换为可控桩对象,
98
+ 对齐 Spring ``@WebMvcTest`` 自动 Mock ``@Service``/``@Repository`` 的语义。
99
+ """
100
+ sig = inspect.signature(cls.__init__)
101
+ kwargs = {}
102
+ for name, param in sig.parameters.items():
103
+ if name == "self":
104
+ continue
105
+ # *args / **kwargs 不注入
106
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
107
+ continue
108
+ kwargs[name] = MagicMock()
109
+ return cls(**kwargs)
110
+
111
+
112
+ # ==================== SpringBootTest(全量上下文)====================
113
+
114
+ class SpringBootTest:
115
+ """全量应用上下文测试切片(对齐 ``@SpringBootTest``)。
116
+
117
+ Args:
118
+ main_class: ``@SpringBootApplication`` 标注的入口类。
119
+ config: 可选配置字典,写入临时 yml;未提供则用入口类所在目录的 application.yml。
120
+ """
121
+
122
+ def __init__(self, main_class: Type, config: Optional[Dict[str, Any]] = None):
123
+ from spring.context.application_context import ApplicationContext
124
+ from spring.config.config_loader import ConfigLoader
125
+
126
+ self._temp_config_path: Optional[str] = None
127
+ if config is not None:
128
+ self._temp_config_path = _write_temp_config(config)
129
+ loader = ConfigLoader(config_path=self._temp_config_path)
130
+ else:
131
+ loader = None
132
+ self._context = ApplicationContext(main_class, config_loader=loader)
133
+ self._context.refresh()
134
+
135
+ def get_context(self):
136
+ return self._context
137
+
138
+ def get_bean(self, name: str) -> Any:
139
+ return self._context.get_bean(name)
140
+
141
+ def get_bean_by_type(self, bean_type: Type) -> Any:
142
+ return self._context.get_bean_by_type(bean_type)
143
+
144
+ def publish_event(self, event: Any) -> Any:
145
+ return self._context.publish_event(event)
146
+
147
+ def close(self) -> None:
148
+ try:
149
+ self._context.destroy()
150
+ finally:
151
+ if self._temp_config_path and os.path.exists(self._temp_config_path):
152
+ os.unlink(self._temp_config_path)
153
+
154
+ def __enter__(self):
155
+ return self
156
+
157
+ def __exit__(self, exc_type, exc_val, exc_tb):
158
+ self.close()
159
+ return False
160
+
161
+
162
+ # ==================== WebMvcTest(Web 切片)====================
163
+
164
+ class WebMvcTest:
165
+ """Web 切片测试(对齐 ``@WebMvcTest``):只注册指定 Controller,依赖 Mock 注入。
166
+
167
+ Args:
168
+ controllers: 要注册的 Controller 类列表。
169
+ config: 可选配置字典。
170
+ mock_dependencies: 是否用 ``MagicMock`` 注入构造函数依赖(默认 True)。
171
+ controller_advice: 可选的 ``@ControllerAdvice`` 类列表,注册全局异常处理。
172
+
173
+ 用法::
174
+
175
+ with WebMvcTest(controllers=[UserController]) as mvc:
176
+ resp = mvc.get_client().get("/users")
177
+ """
178
+
179
+ def __init__(
180
+ self,
181
+ controllers: List[Type],
182
+ config: Optional[Dict[str, Any]] = None,
183
+ mock_dependencies: bool = True,
184
+ controller_advice: Optional[List[Type]] = None,
185
+ ):
186
+ from spring.context.application_context import ApplicationContext
187
+ from spring.context.bean_definition import BeanDefinition
188
+ from spring.config.config_loader import ConfigLoader
189
+ from spring.web.web_context import WebApplicationContext
190
+
191
+ if not controllers:
192
+ raise ValueError("controllers 不能为空")
193
+
194
+ self._temp_config_path: Optional[str] = None
195
+ loader = ConfigLoader(
196
+ config_path=(_write_temp_config(config) if config is not None else _write_temp_config({}))
197
+ )
198
+ self._temp_config_path = loader.config_path
199
+ # 构造一个不 refresh 的最小上下文,手动注册 Controller Bean
200
+ self._context = ApplicationContext(_MinimalApp, config_loader=loader)
201
+
202
+ for ctrl_cls in controllers:
203
+ instance = _instantiate_with_mocks(ctrl_cls) if mock_dependencies else ctrl_cls()
204
+ bean_name = self._generate_bean_name(ctrl_cls)
205
+ definition = BeanDefinition(bean_class=ctrl_cls, bean_name=bean_name)
206
+ # 复制类上的注解到定义(@RestController 等)
207
+ for ann in getattr(ctrl_cls, '__spring_annotations__', []):
208
+ definition.add_annotation(ann)
209
+ self._context.bean_factory.register_bean_definition(bean_name, definition)
210
+ self._context.bean_factory.register_instance(bean_name, instance)
211
+
212
+ # 注册 ControllerAdvice(全局异常处理)
213
+ for advice_cls in (controller_advice or []):
214
+ advice_instance = advice_cls()
215
+ advice_name = self._generate_bean_name(advice_cls)
216
+ advice_def = BeanDefinition(bean_class=advice_cls, bean_name=advice_name)
217
+ for ann in getattr(advice_cls, '__spring_annotations__', []):
218
+ advice_def.add_annotation(ann)
219
+ self._context.bean_factory.register_bean_definition(advice_name, advice_def)
220
+ self._context.bean_factory.register_instance(advice_name, advice_instance)
221
+
222
+ # 构建 WebApplicationContext 并初始化路由
223
+ self._web_context = WebApplicationContext(self._context)
224
+ self._web_context.init()
225
+ self._client = None
226
+
227
+ @staticmethod
228
+ def _generate_bean_name(cls: Type) -> str:
229
+ name = cls.__name__
230
+ return name[0].lower() + name[1:] if name else name
231
+
232
+ def get_app(self):
233
+ return self._web_context.get_app()
234
+
235
+ def get_context(self):
236
+ return self._context
237
+
238
+ def get_client(self):
239
+ from fastapi.testclient import TestClient
240
+ if self._client is None:
241
+ self._client = TestClient(self.get_app())
242
+ return self._client
243
+
244
+ def get_controller(self, ctrl_cls: Type) -> Any:
245
+ return self._context.get_bean_by_type(ctrl_cls)
246
+
247
+ def close(self) -> None:
248
+ try:
249
+ self._context.destroy()
250
+ finally:
251
+ if self._temp_config_path and os.path.exists(self._temp_config_path):
252
+ os.unlink(self._temp_config_path)
253
+
254
+ def __enter__(self):
255
+ return self
256
+
257
+ def __exit__(self, exc_type, exc_val, exc_tb):
258
+ self.close()
259
+ return False
260
+
261
+
262
+ # ==================== DataJpaTest(数据切片)====================
263
+
264
+ class DataJpaTest:
265
+ """数据切片测试(对齐 ``@DataJpaTest``):内存 SQLite + 建表 + Repository 工厂。
266
+
267
+ Args:
268
+ entities: 实体类列表(``@entity`` 标注)。
269
+ dialect: 数据库方言(默认 ``sqlite``,内存库)。
270
+
271
+ 用法::
272
+
273
+ with DataJpaTest(entities=[User]) as jpa:
274
+ repo = jpa.repository_for(User)
275
+ repo.save(User(name="tom"))
276
+ """
277
+
278
+ def __init__(self, entities: List[Type], dialect: str = "sqlite"):
279
+ if not entities:
280
+ raise ValueError("entities 不能为空")
281
+ from spring.orm.ddl_auto import DdlAutoManager
282
+
283
+ self._entities = list(entities)
284
+ self._dialect = dialect
285
+ self._conn = sqlite3.connect(":memory:")
286
+ self._conn.row_factory = None
287
+ self._pool = TestPool(self._conn)
288
+ self._mgr = DdlAutoManager(self._pool, dialect=dialect, mode="create")
289
+ for entity_cls in self._entities:
290
+ self._mgr.register_entity(entity_cls)
291
+ self._mgr.execute()
292
+
293
+ def get_connection(self):
294
+ return self._conn
295
+
296
+ def get_pool(self) -> TestPool:
297
+ return self._pool
298
+
299
+ def repository_for(self, entity_class: Type):
300
+ """为指定实体构造 ``PagingAndSortingRepository``(复用 Spring Data 抽象)。"""
301
+ from spring.data import PagingAndSortingRepository
302
+ return PagingAndSortingRepository(self._pool, entity_class, dialect=self._dialect)
303
+
304
+ def get_entities(self) -> List[Type]:
305
+ return list(self._entities)
306
+
307
+ def close(self) -> None:
308
+ try:
309
+ self._conn.close()
310
+ except Exception:
311
+ pass
312
+
313
+ def __enter__(self):
314
+ return self
315
+
316
+ def __exit__(self, exc_type, exc_val, exc_tb):
317
+ self.close()
318
+ return False
319
+
320
+
321
+ # ==================== 最小入口类 ====================
322
+
323
+ class _MinimalApp:
324
+ """``WebMvcTest`` 用的最小 ``@SpringBootApplication`` 入口(空扫描基包)。"""
325
+ pass
326
+
327
+
328
+ # 应用 @SpringBootApplication 注解到最小入口,满足 ApplicationContext 构造
329
+ try:
330
+ from spring.annotations.core import SpringBootApplication
331
+ _MinimalApp = SpringBootApplication(scan_base_packages=[])(_MinimalApp)
332
+ except ImportError: # pragma: no cover
333
+ pass
334
+
335
+
336
+ __all__ = [
337
+ "TestPool",
338
+ "SpringBootTest",
339
+ "WebMvcTest",
340
+ "DataJpaTest",
341
+ ]
@@ -0,0 +1,11 @@
1
+ """
2
+ 分布式追踪模块
3
+ 提供SkyWalking集成和链路追踪功能
4
+ """
5
+ from .skywalking import SkyWalkingTracer, skywalking_tracer, init_skywalking
6
+
7
+ __all__ = [
8
+ 'SkyWalkingTracer',
9
+ 'skywalking_tracer',
10
+ 'init_skywalking',
11
+ ]
@@ -0,0 +1,229 @@
1
+ """
2
+ SkyWalking分布式追踪模块
3
+ 集成SkyWalking实现分布式链路追踪
4
+ """
5
+ import logging
6
+ import threading
7
+ import time
8
+ import uuid
9
+ from typing import Dict, Optional
10
+
11
+ logger = logging.getLogger("Spring.Tracing.SkyWalking")
12
+
13
+ # 可选导入SkyWalking
14
+ try:
15
+ from skywalking import agent, config
16
+ from skywalking.trace import context, tag
17
+ from skywalking.trace.carrier import Carrier
18
+ _skywalking_available = True
19
+ except ImportError:
20
+ agent = None
21
+ config = None
22
+ context = None
23
+ tag = None
24
+ Carrier = None
25
+ _skywalking_available = False
26
+
27
+
28
+ class SkyWalkingTracer:
29
+ """SkyWalking追踪器"""
30
+
31
+ _instance = None
32
+ _lock = threading.Lock()
33
+
34
+ def __new__(cls, *args, **kwargs):
35
+ if cls._instance is None:
36
+ with cls._lock:
37
+ if cls._instance is None:
38
+ cls._instance = super().__new__(cls)
39
+ return cls._instance
40
+
41
+ def __init__(self, service_name: str = "spring-python-app",
42
+ collector_address: str = "127.0.0.1:11800"):
43
+ if hasattr(self, '_initialized'):
44
+ return
45
+ self.service_name = service_name
46
+ self.collector_address = collector_address
47
+ self._initialized = False
48
+ self._local_context = threading.local()
49
+
50
+ # 如果SkyWalking可用,尝试初始化
51
+ if _skywalking_available:
52
+ self._init_skywalking()
53
+
54
+ def _init_skywalking(self):
55
+ """初始化SkyWalking Agent"""
56
+ try:
57
+ # 配置SkyWalking
58
+ config.service_name = self.service_name
59
+ config.collector_address = self.collector_address
60
+ config.protocol = 'grpc'
61
+
62
+ # 启动SkyWalking Agent
63
+ agent.start()
64
+
65
+ self._initialized = True
66
+ logger.info(f"[SkyWalking] Agent started, service: {self.service_name}, collector: {self.collector_address}")
67
+ except Exception as e:
68
+ logger.warning(f"[SkyWalking] Failed to initialize: {e}. Falling back to local tracing.")
69
+ self._initialized = False
70
+
71
+ def create_span(self, operation_name: str, span_type: str = "Local",
72
+ peer: str = "") -> 'Span':
73
+ """
74
+ 创建Span
75
+
76
+ 参数:
77
+ operation_name: 操作名称
78
+ span_type: Span类型(Local/Remote/DB/MQ等)
79
+ peer: 对端地址
80
+
81
+ 返回:
82
+ Span对象
83
+ """
84
+ if _skywalking_available and self._initialized:
85
+ # 使用SkyWalking创建Span
86
+ try:
87
+ span = context.create_entry_span(operation_name, Carrier())
88
+ span.tag(tag.Tag(key="span.type", val=span_type))
89
+ if peer:
90
+ span.tag(tag.Tag(key="peer", val=peer))
91
+ return span
92
+ except Exception as e:
93
+ logger.warning(f"[SkyWalking] Failed to create span: {e}")
94
+
95
+ # 回退到本地Span
96
+ return LocalSpan(operation_name, span_type, peer)
97
+
98
+ def create_exit_span(self, operation_name: str, peer: str) -> 'Span':
99
+ """
100
+ 创建Exit Span(调用外部服务)
101
+
102
+ 参数:
103
+ operation_name: 操作名称
104
+ peer: 对端地址
105
+
106
+ 返回:
107
+ Span对象
108
+ """
109
+ if _skywalking_available and self._initialized:
110
+ try:
111
+ span = context.create_exit_span(operation_name, peer, Carrier())
112
+ return span
113
+ except Exception as e:
114
+ logger.warning(f"[SkyWalking] Failed to create exit span: {e}")
115
+
116
+ # 回退到本地Span
117
+ return LocalSpan(operation_name, "Remote", peer)
118
+
119
+ def get_trace_id(self) -> str:
120
+ """获取当前Trace ID"""
121
+ if _skywalking_available and self._initialized:
122
+ try:
123
+ active_span = context.get_active_span()
124
+ if active_span:
125
+ return str(active_span.trace_id)
126
+ except Exception as e:
127
+ logger.warning(f"[SkyWalking] Failed to get trace ID: {e}")
128
+
129
+ # 回退到本地Trace ID
130
+ return getattr(self._local_context, 'trace_id', "")
131
+
132
+ def set_trace_id(self, trace_id: str):
133
+ """设置当前Trace ID"""
134
+ self._local_context.trace_id = trace_id
135
+
136
+ def inject_carrier(self, headers: Dict[str, str]) -> Dict[str, str]:
137
+ """
138
+ 将Trace信息注入到请求头中
139
+
140
+ 参数:
141
+ headers: 请求头字典
142
+
143
+ 返回:
144
+ 包含Trace信息的请求头
145
+ """
146
+ if _skywalking_available and self._initialized:
147
+ try:
148
+ carrier = Carrier()
149
+ context.inject(carrier)
150
+ for item in carrier:
151
+ headers[item.key] = item.val
152
+ except Exception as e:
153
+ logger.warning(f"[SkyWalking] Failed to inject carrier: {e}")
154
+
155
+ return headers
156
+
157
+ def extract_carrier(self, headers: Dict[str, str]):
158
+ """
159
+ 从请求头中提取Trace信息
160
+
161
+ 参数:
162
+ headers: 请求头字典
163
+ """
164
+ if _skywalking_available and self._initialized:
165
+ try:
166
+ carrier = Carrier()
167
+ for item in carrier:
168
+ if item.key in headers:
169
+ item.val = headers[item.key]
170
+ context.extract(carrier)
171
+ except Exception as e:
172
+ logger.warning(f"[SkyWalking] Failed to extract carrier: {e}")
173
+
174
+
175
+ class LocalSpan:
176
+ """本地Span(SkyWalking不可用时的回退实现)"""
177
+
178
+ def __init__(self, operation_name: str, span_type: str, peer: str = ""):
179
+ self.operation_name = operation_name
180
+ self.span_type = span_type
181
+ self.peer = peer
182
+ self.start_time = time.time()
183
+ self.end_time = None
184
+ self.tags = {}
185
+ self.trace_id = str(uuid.uuid4())[:16]
186
+
187
+ logger.info(f"[LocalTrace] Start span={operation_name}, trace_id={self.trace_id}, type={span_type}")
188
+
189
+ def tag(self, tag_obj):
190
+ """添加标签"""
191
+ if hasattr(tag_obj, 'key') and hasattr(tag_obj, 'val'):
192
+ self.tags[tag_obj.key] = tag_obj.val
193
+ elif isinstance(tag_obj, tuple):
194
+ self.tags[tag_obj[0]] = tag_obj[1]
195
+
196
+ def finish(self):
197
+ """结束Span"""
198
+ self.end_time = time.time()
199
+ duration = (self.end_time - self.start_time) * 1000
200
+ logger.info(
201
+ f"[LocalTrace] End span={self.operation_name}, trace_id={self.trace_id}, "
202
+ f"duration={duration:.2f}ms, type={self.span_type}"
203
+ )
204
+
205
+ def __enter__(self):
206
+ return self
207
+
208
+ def __exit__(self, exc_type, exc_val, exc_tb):
209
+ self.finish()
210
+ if exc_val:
211
+ logger.error(f"[LocalTrace] Error span={self.operation_name}, error={exc_val}")
212
+
213
+
214
+ # 创建全局SkyWalking追踪器实例
215
+ skywalking_tracer = SkyWalkingTracer()
216
+
217
+
218
+ def init_skywalking(config: dict) -> None:
219
+ """
220
+ 初始化SkyWalking配置
221
+
222
+ 参数:
223
+ config: 配置字典,包含service_name, collector_address等
224
+ """
225
+ global skywalking_tracer
226
+ skywalking_tracer = SkyWalkingTracer(
227
+ service_name=config.get('service_name', 'spring-python-app'),
228
+ collector_address=config.get('collector_address', '127.0.0.1:11800')
229
+ )
spring/tx/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """SpringBootAI 事务扩展模块(对齐 Spring ``@TransactionalEventListener``)。
2
+
3
+ 模块组成:
4
+ - ``synchronization``: ``TransactionSynchronizationManager`` + ``TransactionSynchronization`` +
5
+ ``TransactionPhase`` —— 事务同步回调管理(``ContextVar`` 兼容协程)。
6
+ - ``events``: ``@TransactionalEventListener`` 注解 + ``TransactionalEventPublisher`` ——
7
+ 事务阶段事件监听。
8
+
9
+ 典型用法::
10
+
11
+ from spring.tx import (
12
+ TransactionalEventListener, TransactionPhase,
13
+ TransactionalEventPublisher, TransactionSynchronizationManager,
14
+ )
15
+
16
+ class OrderCreatedEvent(ApplicationEvent):
17
+ pass
18
+
19
+ class OrderService:
20
+ @TransactionalEventListener(phase=TransactionPhase.AFTER_COMMIT)
21
+ def on_order_created(self, event: OrderCreatedEvent):
22
+ ... # 事务提交后才执行
23
+
24
+ 集成:
25
+ - ``@Transactional`` 切面(``bean_factory._wrap_transactional``)在事务边界调用
26
+ ``TransactionSynchronizationManager.init/clear`` 与各 ``trigger_*`` 触发回调。
27
+ - ``ApplicationContext`` 扫描 ``@TransactionalEventListener`` 注册到
28
+ ``TransactionalEventPublisher``,``publish_event`` 委托触发。
29
+
30
+ 与 Java 的差异:
31
+ - 用 ``ContextVar`` 替代 ``ThreadLocal``,兼容 ``asyncio`` 协程。
32
+ - 同步回调抛错统一记录不中断事务(Spring ``beforeCommit`` 抛错会回滚),已在文档标注。
33
+ """
34
+ from .synchronization import (
35
+ TransactionPhase,
36
+ TransactionSynchronization,
37
+ TransactionSynchronizationManager,
38
+ transaction_sync_scope,
39
+ )
40
+ from .events import TransactionalEventListener, TransactionalEventPublisher
41
+
42
+ __version__ = "1.0.0"
43
+
44
+ __all__ = [
45
+ "TransactionPhase",
46
+ "TransactionSynchronization",
47
+ "TransactionSynchronizationManager",
48
+ "transaction_sync_scope",
49
+ "TransactionalEventListener",
50
+ "TransactionalEventPublisher",
51
+ "__version__",
52
+ ]