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,274 @@
1
+ """Spring Data 仓库抽象(对齐 ``org.springframework.data.repository.PagingAndSortingRepository``)。
2
+
3
+ 提供基于实体元数据的 CRUD + 分页 + 排序 + ``Specification`` 动态查询统一抽象。
4
+ **复用现有范式不重复造轮子**:
5
+ - 实体解析复用 ``DdlAutoManager._parse_entity``(与 ``OptimisticLockExecutor`` 同一 ``__new__`` 技巧),
6
+ 无需 pool 即可解析表名/列/主键。
7
+ - SQL 执行复用 ``OptimisticLockExecutor`` 的轻量范式(pool + cursor + ``_quote``),
8
+ 不强依赖 PyMyBatis ``SqlSession``,降低使用门槛。
9
+ - 列名翻译 ``_col_resolver`` 把 Python 属性名映射为真实列名(对齐 ``Column(name=...)``),
10
+ 供 ``Sort`` / ``Specification`` 复用。
11
+
12
+ 与 Java 差异:
13
+ - Java 的 Spring Data Repository 是接口 + 方法名解析(运行时动态代理生成实现);
14
+ Python 无等价元编程惯例,故采用**基类继承** + 显式方法,更符合 Python 习惯。
15
+ - ``@DataRepository`` 为标记注解(声明管理的实体类型),实际能力由继承基类获得。
16
+ """
17
+ import inspect
18
+ from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar
19
+
20
+ from spring.data.page import Page, Pageable, Sort
21
+ from spring.data.specification import ColResolver, Specification
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ def _parse_entity_table(entity_class: Type):
27
+ """复用 ``DdlAutoManager._parse_entity`` 解析实体元数据(不依赖 pool)。
28
+
29
+ 与 ``OptimisticLockExecutor._parse_columns`` 同一技巧:``__new__`` 绕过 ``__init__``
30
+ (后者需要 pool),直接调用纯解析方法。
31
+ """
32
+ from spring.orm.ddl_auto import DdlAutoManager
33
+ tmp = DdlAutoManager.__new__(DdlAutoManager)
34
+ tmp.dialect = "sqlite"
35
+ tmp.mode = None # type: ignore
36
+ return tmp._parse_entity(entity_class)
37
+
38
+
39
+ class PagingAndSortingRepository(Generic[T]):
40
+ """分页排序仓库基类。
41
+
42
+ Args:
43
+ pool: 数据库连接池(需支持 ``connection()`` 返回带 ``cursor()``/``commit()``/``close()`` 的连接)。
44
+ entity_class: 管理的实体类(带 ``@entity``/``Column`` 元数据)。
45
+ dialect: SQL 方言(mysql/postgresql/sqlite),影响标识符引用。
46
+ """
47
+
48
+ def __init__(self, pool: Any, entity_class: Type[T], dialect: str = "sqlite"):
49
+ self.pool = pool
50
+ self.entity_class = entity_class
51
+ self.dialect = dialect.lower()
52
+ self._table = _parse_entity_table(entity_class)
53
+ self._columns: List[dict] = [c for c in self._table.columns if not c.get("transient")]
54
+ self._pk = next((c for c in self._columns if c.get("primary_key")), None)
55
+ if self._pk is None:
56
+ raise ValueError(
57
+ f"实体 {entity_class.__name__} 未声明主键(@Id),无法构建 Repository"
58
+ )
59
+ # py_name -> sql 列名 映射,供 Sort/Specification 翻译
60
+ self._col_map: Dict[str, str] = {
61
+ c.get("py_name") or c.get("name"): c.get("name") or c.get("py_name")
62
+ for c in self._columns
63
+ }
64
+
65
+ # ==================== 内部工具 ====================
66
+
67
+ def _quote(self, identifier: str) -> str:
68
+ if self.dialect == "mysql":
69
+ return f"`{identifier}`"
70
+ return f'"{identifier}"'
71
+
72
+ def _col_resolver(self, prop: str) -> str:
73
+ return self._col_map.get(prop, prop)
74
+
75
+ def _column_list(self) -> str:
76
+ return ", ".join(self._quote(c["name"]) for c in self._columns)
77
+
78
+ def _row_to_entity(self, row: tuple) -> T:
79
+ """按列顺序把行元组转为实体实例。"""
80
+ obj = self.entity_class.__new__(self.entity_class)
81
+ for col, val in zip(self._columns, row):
82
+ setattr(obj, col["py_name"], val)
83
+ return obj # type: ignore
84
+
85
+ def _get_field(self, entity: T, py_name: str) -> Any:
86
+ return getattr(entity, py_name, None)
87
+
88
+ def _execute(self, sql: str, params: list, fetch: bool = False):
89
+ """执行 SQL。fetch=True 返回行列表,否则返回影响行数。"""
90
+ conn = None
91
+ try:
92
+ if hasattr(self.pool, "connection"):
93
+ conn = self.pool.connection()
94
+ else:
95
+ conn = self.pool
96
+ cursor = conn.cursor()
97
+ cursor.execute(sql, params)
98
+ if fetch:
99
+ rows = cursor.fetchall()
100
+ conn.commit()
101
+ return rows
102
+ # DML:rowcount 为影响行数(sqlite3/MySQLdb/psycopg2 一致)
103
+ affected = cursor.rowcount
104
+ conn.commit()
105
+ return affected
106
+ finally:
107
+ if conn is not None and hasattr(self.pool, "connection"):
108
+ try:
109
+ conn.close()
110
+ except Exception:
111
+ pass
112
+
113
+ def _fetchone(self, sql: str, params: list):
114
+ conn = None
115
+ try:
116
+ if hasattr(self.pool, "connection"):
117
+ conn = self.pool.connection()
118
+ else:
119
+ conn = self.pool
120
+ cursor = conn.cursor()
121
+ cursor.execute(sql, params)
122
+ row = cursor.fetchone()
123
+ conn.commit()
124
+ return row
125
+ finally:
126
+ if conn is not None and hasattr(self.pool, "connection"):
127
+ try:
128
+ conn.close()
129
+ except Exception:
130
+ pass
131
+
132
+ # ==================== CRUD ====================
133
+
134
+ def save(self, entity: T) -> T:
135
+ """保存实体:主键已存在则 UPDATE,否则 INSERT。"""
136
+ pk_val = self._get_field(entity, self._pk["py_name"])
137
+ if pk_val is not None and self.exists_by_id(pk_val):
138
+ return self._update(entity)
139
+ return self._insert(entity)
140
+
141
+ def _insert(self, entity: T) -> T:
142
+ non_auto = [c for c in self._columns if not c.get("auto_increment")]
143
+ cols = ", ".join(self._quote(c["name"]) for c in non_auto)
144
+ placeholders = ", ".join("?" for _ in non_auto)
145
+ params = [self._get_field(entity, c["py_name"]) for c in non_auto]
146
+ sql = f"INSERT INTO {self._quote(self._table.table_name)} ({cols}) VALUES ({placeholders})"
147
+ self._execute(sql, params)
148
+ # 自增主键回填
149
+ if self._pk.get("auto_increment") and self._get_field(entity, self._pk["py_name"]) is None:
150
+ last = self._fetchone(
151
+ f"SELECT {self._quote(self._pk['name'])} FROM "
152
+ f"{self._quote(self._table.table_name)} ORDER BY "
153
+ f"{self._quote(self._pk['name'])} DESC LIMIT 1", []
154
+ )
155
+ if last:
156
+ setattr(entity, self._pk["py_name"], last[0])
157
+ return entity
158
+
159
+ def _update(self, entity: T) -> T:
160
+ non_pk = [c for c in self._columns if not c.get("primary_key")]
161
+ set_parts = ", ".join(f"{self._quote(c['name'])} = ?" for c in non_pk)
162
+ params = [self._get_field(entity, c["py_name"]) for c in non_pk]
163
+ pk_val = self._get_field(entity, self._pk["py_name"])
164
+ params.append(pk_val)
165
+ sql = (f"UPDATE {self._quote(self._table.table_name)} SET {set_parts} "
166
+ f"WHERE {self._quote(self._pk['name'])} = ?")
167
+ self._execute(sql, params)
168
+ return entity
169
+
170
+ def find_by_id(self, id_: Any) -> Optional[T]:
171
+ sql = (f"SELECT {self._column_list()} FROM {self._quote(self._table.table_name)} "
172
+ f"WHERE {self._quote(self._pk['name'])} = ?")
173
+ row = self._fetchone(sql, [id_])
174
+ return self._row_to_entity(row) if row else None
175
+
176
+ def exists_by_id(self, id_: Any) -> bool:
177
+ sql = (f"SELECT 1 FROM {self._quote(self._table.table_name)} "
178
+ f"WHERE {self._quote(self._pk['name'])} = ? LIMIT 1")
179
+ return self._fetchone(sql, [id_]) is not None
180
+
181
+ def count(self, specification: Optional[Specification] = None) -> int:
182
+ where, params = self._spec_where(specification)
183
+ sql = f"SELECT COUNT(*) FROM {self._quote(self._table.table_name)}{where}"
184
+ row = self._fetchone(sql, params)
185
+ return int(row[0]) if row else 0
186
+
187
+ def delete_by_id(self, id_: Any) -> int:
188
+ sql = (f"DELETE FROM {self._quote(self._table.table_name)} "
189
+ f"WHERE {self._quote(self._pk['name'])} = ?")
190
+ return self._execute(sql, [id_])
191
+
192
+ def delete(self, entity: T) -> int:
193
+ return self.delete_by_id(self._get_field(entity, self._pk["py_name"]))
194
+
195
+ def delete_all(self, specification: Optional[Specification] = None) -> int:
196
+ where, params = self._spec_where(specification)
197
+ sql = f"DELETE FROM {self._quote(self._table.table_name)}{where}"
198
+ return self._execute(sql, params)
199
+
200
+ # ==================== 查询(排序/分页/动态) ====================
201
+
202
+ def find_all(self,
203
+ sort: Optional[Sort] = None,
204
+ specification: Optional[Specification] = None,
205
+ pageable: Optional[Pageable] = None) -> Any:
206
+ """统一查询入口。
207
+
208
+ - 仅传 ``sort``:返回 ``List[T]``。
209
+ - 仅传 ``specification``:返回 ``List[T]``。
210
+ - 传 ``pageable``:返回 ``Page[T]``(可同时带 ``specification``)。
211
+ """
212
+ where, params = self._spec_where(specification)
213
+ if pageable is not None:
214
+ return self._find_page(pageable, where, params, specification)
215
+ order = self._sort_sql(sort or Sort.unsorted())
216
+ sql = (f"SELECT {self._column_list()} FROM {self._quote(self._table.table_name)}"
217
+ f"{where}{order}")
218
+ rows = self._execute(sql, params, fetch=True)
219
+ return [self._row_to_entity(r) for r in rows]
220
+
221
+ def find_one(self, specification: Specification) -> Optional[T]:
222
+ where, params = self._spec_where(specification)
223
+ sql = (f"SELECT {self._column_list()} FROM {self._quote(self._table.table_name)}"
224
+ f"{where} LIMIT 1")
225
+ row = self._fetchone(sql, params)
226
+ return self._row_to_entity(row) if row else None
227
+
228
+ def _find_page(self, pageable: Pageable, where: str, params: list,
229
+ specification: Optional[Specification] = None) -> Page:
230
+ order = self._sort_sql(pageable.sort)
231
+ # SQLite/MySQL/PostgreSQL 均支持 LIMIT/OFFSET
232
+ page_sql = (f"SELECT {self._column_list()} FROM {self._quote(self._table.table_name)}"
233
+ f"{where}{order} LIMIT ? OFFSET ?")
234
+ rows = self._execute(page_sql, params + [pageable.limit, pageable.offset], fetch=True)
235
+ content = [self._row_to_entity(r) for r in rows]
236
+ # total 必须带同一 specification,否则分页总数与筛选条件不一致
237
+ total = self.count(specification)
238
+ return Page(content, pageable, total)
239
+
240
+ def _spec_where(self, specification: Optional[Specification]) -> tuple:
241
+ if specification is None:
242
+ return "", []
243
+ sql, params = specification.to_predicate(self._col_resolver)
244
+ return (f" WHERE {sql}" if sql else ""), params
245
+
246
+ def _sort_sql(self, sort: Sort) -> str:
247
+ if not sort.is_sorted:
248
+ return ""
249
+ return " ORDER BY " + sort.to_sql(lambda p: self._quote(self._col_resolver(p)))
250
+
251
+
252
+ # ==================== 注解(标记) ====================
253
+
254
+ class DataRepository:
255
+ """``@DataRepository(EntityClass)`` 标记注解:声明仓库管理的实体类型。
256
+
257
+ 标记后可通过 ``get_data_repository_entity(cls)`` 读取实体类型,便于 IoC 集成
258
+ (如 ``@Bean`` 工厂方法根据实体类型构造 ``PagingAndSortingRepository``)。
259
+
260
+ 注意:本注解仅声明元数据,**不**自动生成仓库实现——仓库能力由继承
261
+ ``PagingAndSortingRepository`` 获得(对齐 Python 习惯,与 Java 接口代理方式不同)。
262
+ """
263
+
264
+ def __init__(self, entity_class: Type):
265
+ self.entity_class = entity_class
266
+
267
+ def __call__(self, target: Type) -> Type:
268
+ setattr(target, "__data_repository__", self)
269
+ return target
270
+
271
+
272
+ def get_data_repository_entity(cls: Type) -> Optional[Type]:
273
+ ann = getattr(cls, "__data_repository__", None)
274
+ return ann.entity_class if ann is not None else None
@@ -0,0 +1,228 @@
1
+ """Spring Data JPA 风格的动态查询 ``Specification``(对齐 ``org.springframework.data.jpa.domain.Specification``)。
2
+
3
+ 将查询条件抽象为可组合的谓词,``to_predicate()`` 返回 ``(where_sql, params)``,
4
+ 由 ``PagingAndSortingRepository`` 拼入 SQL。支持 ``and`` / ``or`` / ``not`` 复合。
5
+
6
+ 与 Java 差异:
7
+ - Java 的 ``Specification.to_predicate(Root, CriteriaQuery, CriteriaBuilder)`` 依赖 JPA Criteria API;
8
+ Python 无等价物,故简化为返回 ``(where_sql_fragment, params)`` 元组。
9
+ - 列名翻译通过 ``col_resolver``(``property_name -> sql_column_name``)回调完成,
10
+ 与 ``Pageable.Sort`` 一致,避免 ``Column(name=...)`` 自定义列名时生成错误 SQL。
11
+ """
12
+ from abc import ABC, abstractmethod
13
+ from typing import Any, Callable, List, Optional, Tuple, TypeVar
14
+
15
+ T = TypeVar("T")
16
+
17
+ # 谓词返回类型:(SQL 片段, 参数列表);SQL 片段为空串表示无条件
18
+ Predicate = Tuple[str, List[Any]]
19
+ ColResolver = Callable[[str], str]
20
+
21
+
22
+ class Specification(ABC):
23
+ """查询规范抽象基类。"""
24
+
25
+ @abstractmethod
26
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
27
+ """返回 ``(where_sql, params)``。``where_sql`` 为空串表示无条件。"""
28
+
29
+ def and_(self, other: "Specification") -> "Specification":
30
+ return And(self, other)
31
+
32
+ def or_(self, other: "Specification") -> "Specification":
33
+ return Or(self, other)
34
+
35
+ def not_(self) -> "Specification":
36
+ return Not(self)
37
+
38
+
39
+ def _resolve(prop: str, col_resolver: Optional[ColResolver]) -> str:
40
+ return col_resolver(prop) if col_resolver else prop
41
+
42
+
43
+ class _Empty(Specification):
44
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
45
+ return "", []
46
+
47
+
48
+ class And(Specification):
49
+ def __init__(self, *specs: Specification):
50
+ self.specs = specs
51
+
52
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
53
+ parts: List[str] = []
54
+ params: List[Any] = []
55
+ for s in self.specs:
56
+ sql, p = s.to_predicate(col_resolver)
57
+ if sql:
58
+ parts.append(f"({sql})")
59
+ params.extend(p)
60
+ if not parts:
61
+ return "", []
62
+ return " AND ".join(parts), params
63
+
64
+
65
+ class Or(Specification):
66
+ def __init__(self, *specs: Specification):
67
+ self.specs = specs
68
+
69
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
70
+ parts: List[str] = []
71
+ params: List[Any] = []
72
+ for s in self.specs:
73
+ sql, p = s.to_predicate(col_resolver)
74
+ if sql:
75
+ parts.append(f"({sql})")
76
+ params.extend(p)
77
+ if not parts:
78
+ return "", []
79
+ return " OR ".join(parts), params
80
+
81
+
82
+ class Not(Specification):
83
+ def __init__(self, spec: Specification):
84
+ self.spec = spec
85
+
86
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
87
+ sql, params = self.spec.to_predicate(col_resolver)
88
+ if not sql:
89
+ return "", []
90
+ return f"NOT ({sql})", params
91
+
92
+
93
+ class _Comparison(Specification):
94
+ """单字段比较谓词基类。"""
95
+
96
+ def __init__(self, property: str, op: str, value: Any, negated: bool = False):
97
+ self.property = property
98
+ self.op = op
99
+ self.value = value
100
+ self.negated = negated
101
+
102
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
103
+ col = _resolve(self.property, col_resolver)
104
+ op = self.op if not self.negated else _negate_op(self.op)
105
+ return f"{col} {op} ?", [self.value]
106
+
107
+
108
+ def _negate_op(op: str) -> str:
109
+ return {
110
+ "=": "<>", "<>": "=", "!=": "=",
111
+ ">": "<=", "<": ">=", ">=": "<", "<=": ">",
112
+ "LIKE": "NOT LIKE", "IN": "NOT IN", "IS": "IS NOT",
113
+ }.get(op, f"NOT {op}")
114
+
115
+
116
+ class _Like(Specification):
117
+ def __init__(self, property: str, pattern: str, case_insensitive: bool = False):
118
+ self.property = property
119
+ self.pattern = pattern
120
+ self.case_insensitive = case_insensitive
121
+
122
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
123
+ col = _resolve(self.property, col_resolver)
124
+ if self.case_insensitive:
125
+ return f"LOWER({col}) LIKE LOWER(?)", [self.pattern]
126
+ return f"{col} LIKE ?", [self.pattern]
127
+
128
+
129
+ class _In(Specification):
130
+ def __init__(self, property: str, values: list):
131
+ self.property = property
132
+ self.values = list(values)
133
+
134
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
135
+ col = _resolve(self.property, col_resolver)
136
+ if not self.values:
137
+ return "1=0", [] # 空集合:恒假
138
+ placeholders = ", ".join("?" for _ in self.values)
139
+ return f"{col} IN ({placeholders})", list(self.values)
140
+
141
+
142
+ class _IsNull(Specification):
143
+ def __init__(self, property: str, negate: bool = False):
144
+ self.property = property
145
+ self.negate = negate
146
+
147
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
148
+ col = _resolve(self.property, col_resolver)
149
+ return (f"{col} IS NOT NULL" if self.negate else f"{col} IS NULL"), []
150
+
151
+
152
+ class _Between(Specification):
153
+ def __init__(self, property: str, low: Any, high: Any):
154
+ self.property = property
155
+ self.low = low
156
+ self.high = high
157
+
158
+ def to_predicate(self, col_resolver: Optional[ColResolver] = None) -> Predicate:
159
+ col = _resolve(self.property, col_resolver)
160
+ return f"{col} BETWEEN ? AND ?", [self.low, self.high]
161
+
162
+
163
+ class Specifications:
164
+ """``Specification`` 静态工厂(对齐 Spring ``Specifications`` 工具类)。"""
165
+
166
+ @staticmethod
167
+ def empty() -> Specification:
168
+ return _Empty()
169
+
170
+ @staticmethod
171
+ def where(spec: Optional[Specification] = None) -> Specification:
172
+ return spec if spec is not None else _Empty()
173
+
174
+ @staticmethod
175
+ def equal(property: str, value: Any) -> Specification:
176
+ return _Comparison(property, "=", value)
177
+
178
+ @staticmethod
179
+ def not_equal(property: str, value: Any) -> Specification:
180
+ return _Comparison(property, "<>", value)
181
+
182
+ @staticmethod
183
+ def greater_than(property: str, value: Any) -> Specification:
184
+ return _Comparison(property, ">", value)
185
+
186
+ @staticmethod
187
+ def greater_equal(property: str, value: Any) -> Specification:
188
+ return _Comparison(property, ">=", value)
189
+
190
+ @staticmethod
191
+ def less_than(property: str, value: Any) -> Specification:
192
+ return _Comparison(property, "<", value)
193
+
194
+ @staticmethod
195
+ def less_equal(property: str, value: Any) -> Specification:
196
+ return _Comparison(property, "<=", value)
197
+
198
+ @staticmethod
199
+ def like(property: str, pattern: str, case_insensitive: bool = False) -> Specification:
200
+ return _Like(property, pattern, case_insensitive)
201
+
202
+ @staticmethod
203
+ def in_(property: str, values: list) -> Specification:
204
+ return _In(property, values)
205
+
206
+ @staticmethod
207
+ def is_null(property: str) -> Specification:
208
+ return _IsNull(property, negate=False)
209
+
210
+ @staticmethod
211
+ def is_not_null(property: str) -> Specification:
212
+ return _IsNull(property, negate=True)
213
+
214
+ @staticmethod
215
+ def between(property: str, low: Any, high: Any) -> Specification:
216
+ return _Between(property, low, high)
217
+
218
+ @staticmethod
219
+ def and_(spec: Specification, *others: Specification) -> Specification:
220
+ return And(spec, *others)
221
+
222
+ @staticmethod
223
+ def or_(spec: Specification, *others: Specification) -> Specification:
224
+ return Or(spec, *others)
225
+
226
+ @staticmethod
227
+ def not_(spec: Specification) -> Specification:
228
+ return Not(spec)
@@ -0,0 +1,66 @@
1
+ """SpringBootAI 多数据源读写分离模块(对齐 Spring ``AbstractRoutingDataSource`` +
2
+ ``dynamic-datasource-spring-boot-starter`` 的 ``@DS``/``@Master``/``@Slave``)。
3
+
4
+ 模块组成:
5
+ - ``context``: ``DataSourceContextHolder`` —— ``ContextVar`` 持有当前路由键。
6
+ - ``dynamic``: ``DynamicRoutingDataSource`` —— 多池路由 + 从库轮询 + 故障回退。
7
+ - ``annotations``: ``@DS``/``@Master``/``@Slave`` 注解 + 方法级 AOP 切面。
8
+
9
+ 典型用法::
10
+
11
+ from spring.datasource import (
12
+ DynamicRoutingDataSource, DS, Master, Slave,
13
+ DataSourceContextHolder, apply_ds_annotations,
14
+ )
15
+
16
+ # 1. 装配动态数据源(master + 两个 slave)
17
+ dynamic_ds = DynamicRoutingDataSource(
18
+ target_data_sources={"slave_1": pool1, "slave_2": pool2},
19
+ default_target_data_source=master_pool,
20
+ slave_keys=["slave_1", "slave_2"],
21
+ )
22
+
23
+ # 2. 注解驱动路由
24
+ class OrderService:
25
+ @Master
26
+ def create_order(self, order): ... # 走主库写
27
+ @Slave
28
+ def list_orders(self): ... # 走从库读(轮询)
29
+ @DS("report_db")
30
+ def report(self): ... # 走具名数据源
31
+
32
+ 设计原则:**复用项目既有范式**,注解继承 ``SpringAnnotation``,AOP 注册对齐
33
+ ``@Validate``/``@Cacheable``,连接池接口对齐 ``ConnectionPool``,未引入第三方库。
34
+
35
+ 与 Java 的差异:
36
+ - 用 ``ContextVar`` 替代 ``ThreadLocal``,兼容 ``asyncio`` 协程。
37
+ - ``@Slave`` 用占位路由键 + ``DynamicRoutingDataSource`` 轮询解析,无需运行时织入具体键。
38
+ """
39
+ from .context import DataSourceContextHolder, routing_scope
40
+ from .dynamic import DynamicRoutingDataSource
41
+ from .annotations import (
42
+ DS, Master, Slave,
43
+ ds_route_decorator, ds_decorator_factory, apply_ds_annotations, is_slave_placeholder,
44
+ )
45
+
46
+ __version__ = "1.0.0"
47
+
48
+ __all__ = [
49
+ "DataSourceContextHolder",
50
+ "routing_scope",
51
+ "DynamicRoutingDataSource",
52
+ "DS", "Master", "Slave",
53
+ "ds_route_decorator", "ds_decorator_factory",
54
+ "apply_ds_annotations", "is_slave_placeholder",
55
+ "__version__",
56
+ ]
57
+
58
+ # 接入 comprehensive_aop 受管 Bean 包装链路(对齐 @Validate/@Cacheable 注册模式)。
59
+ # 在受管 Bean 上标注 @DS/@Master/@Slave 时,IoC 容器会通过 apply_annotations 自动包装。
60
+ try:
61
+ from spring.aop.comprehensive_aop import ANNOTATION_DECORATORS
62
+ for _ann_cls in (DS, Master, Slave):
63
+ if _ann_cls not in ANNOTATION_DECORATORS:
64
+ ANNOTATION_DECORATORS[_ann_cls] = ds_decorator_factory
65
+ except ImportError: # pragma: no cover - comprehensive_aop 未安装时静默跳过
66
+ pass