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/csv/reader.py ADDED
@@ -0,0 +1,195 @@
1
+ """SpringBootAI CSV 读取引擎。
2
+
3
+ 按 ``@CsvProperty`` / ``@CsvIgnore`` / ``@csv_file`` 注解把 CSV 文件读为实体对象列表。
4
+ 底层使用 Python 标准库 ``csv``(**无可选依赖**,开箱即用)。
5
+
6
+ 读取流程对齐 Excel 模块(``spring.excel.reader``)与 alibaba EasyExcel:
7
+ 1. 读取表头行(``has_header``,默认 True)。
8
+ 2. 表头文案 -> ``CsvColumnModel`` 映射(按 ``value`` 匹配;无注解时按列位置匹配)。
9
+ 3. 数据行逐行按 ``converter.from_excel`` 转换,构造实体实例。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import csv as _csv
14
+ import inspect
15
+ from typing import Any, List, Optional, Type
16
+
17
+ from .annotations import (
18
+ CsvColumnModel, CsvFile, _get_class_file_meta, has_explicit_properties,
19
+ parse_csv_columns,
20
+ )
21
+ from .converters import resolve_csv_converter
22
+ from .exceptions import CsvReadError
23
+
24
+
25
+ def _build_instance(cls: Type, kwargs: dict) -> Any:
26
+ """构造实体实例:优先 ``cls(**kwargs)``,失败则逐字段 setattr(兼容纯 ``__init__`` 模型)。
27
+
28
+ 镜像 Excel ``_build_instance``。
29
+ """
30
+ try:
31
+ sig = inspect.signature(cls.__init__)
32
+ params = list(sig.parameters.values())[1:] # 跳过 self
33
+ accept_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params)
34
+ if accept_kwargs:
35
+ return cls(**kwargs)
36
+ allowed = {p.name for p in params if p.kind in (
37
+ inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY,
38
+ )}
39
+ filtered = {k: v for k, v in kwargs.items() if k in allowed}
40
+ return cls(**filtered)
41
+ except TypeError:
42
+ pass
43
+ # 回退:无参构造 + setattr
44
+ try:
45
+ obj = cls()
46
+ except Exception:
47
+ obj = object.__new__(cls)
48
+ for k, v in kwargs.items():
49
+ try:
50
+ setattr(obj, k, v)
51
+ except Exception:
52
+ pass
53
+ return obj
54
+
55
+
56
+ def _open_for_read(source: Any, encoding: str):
57
+ """支持文件路径(str/Path)或类文件对象。"""
58
+ try:
59
+ if isinstance(source, (str, bytes)) or hasattr(source, "__fspath__"):
60
+ return open(source, "r", encoding=encoding, newline="")
61
+ # 类文件对象,直接使用
62
+ return source
63
+ except Exception as e:
64
+ raise CsvReadError(f"打开 CSV 文件失败: {e}") from e
65
+
66
+
67
+ class CsvReader:
68
+ """CSV 读取器。由 ``EasyCsv.read(...)`` 构建,调用 ``doRead`` 执行。"""
69
+
70
+ def __init__(
71
+ self,
72
+ source: Any,
73
+ head: Optional[Type] = None,
74
+ has_header: Optional[bool] = None,
75
+ delimiter: Optional[str] = None,
76
+ encoding: Optional[str] = None,
77
+ ):
78
+ self.source = source
79
+ self.head = head
80
+ self._has_header = has_header
81
+ self._delimiter = delimiter
82
+ self._encoding = encoding
83
+
84
+ # ---- 流式配置 ----
85
+
86
+ def has_header(self, flag: bool) -> "CsvReader":
87
+ self._has_header = flag
88
+ return self
89
+
90
+ def delimiter(self, d: str) -> "CsvReader":
91
+ self._delimiter = d
92
+ return self
93
+
94
+ def encoding(self, enc: str) -> "CsvReader":
95
+ self._encoding = enc
96
+ return self
97
+
98
+ # ---- 执行 ----
99
+
100
+ def _resolve_meta(self) -> CsvFile:
101
+ meta = _get_class_file_meta(self.head) if self.head is not None else CsvFile()
102
+ if self._has_header is not None:
103
+ meta.has_header = self._has_header
104
+ if self._delimiter is not None:
105
+ meta.delimiter = self._delimiter
106
+ if self._encoding is not None:
107
+ meta.encoding = self._encoding
108
+ return meta
109
+
110
+ def doRead(self) -> List[Any]:
111
+ """读取 CSV,返回实体列表。"""
112
+ if self.head is None:
113
+ raise CsvReadError("read 需指定 head 实体类")
114
+ meta = self._resolve_meta()
115
+ columns: List[CsvColumnModel] = parse_csv_columns(self.head)
116
+ explicit = has_explicit_properties(self.head)
117
+
118
+ f = _open_for_read(self.source, meta.encoding)
119
+ owns_fh = f is not self.source
120
+ try:
121
+ reader = _csv.reader(f, delimiter=meta.delimiter, quotechar=meta.quote_char)
122
+ rows = list(reader)
123
+ finally:
124
+ if owns_fh:
125
+ try:
126
+ f.close()
127
+ except Exception:
128
+ pass
129
+
130
+ if not rows:
131
+ return []
132
+
133
+ # 表头
134
+ start_idx = 0
135
+ header_cells: List[str] = []
136
+ if meta.has_header:
137
+ header_cells = [str(c).strip() if c is not None else "" for c in rows[0]]
138
+ start_idx = 1
139
+
140
+ # 列映射:列序号(0-based) -> CsvColumnModel
141
+ col_mapping = self._map_columns(columns, header_cells, explicit, meta.has_header)
142
+
143
+ results: List[Any] = []
144
+ for row in rows[start_idx:]:
145
+ if not row or all((v is None or str(v).strip() == "") for v in row):
146
+ continue # 跳过全空行
147
+ kwargs = {}
148
+ for col_idx, model in col_mapping.items():
149
+ cell_value = row[col_idx] if col_idx < len(row) else None
150
+ value = self._convert_from_cell(model, cell_value)
151
+ kwargs[model.attr_name] = value
152
+ results.append(_build_instance(self.head, kwargs))
153
+ return results
154
+
155
+ def _map_columns(
156
+ self,
157
+ columns: List[CsvColumnModel],
158
+ header_cells: List[str],
159
+ explicit: bool,
160
+ has_header: bool,
161
+ ) -> dict:
162
+ """表头 -> 列模型映射。返回 {列序号(0-based): CsvColumnModel}。"""
163
+ col_mapping: dict = {}
164
+ if explicit and has_header and any(c.header for c in columns):
165
+ # 按表头文案匹配
166
+ header_to_col = {}
167
+ for idx, h in enumerate(header_cells):
168
+ if h == "":
169
+ continue
170
+ header_to_col.setdefault(h, idx)
171
+ for model in columns:
172
+ col_idx = header_to_col.get(model.header)
173
+ if col_idx is None:
174
+ continue
175
+ col_mapping[col_idx] = model
176
+ else:
177
+ # 按列位置匹配(无注解或无表头)
178
+ for offset, model in enumerate(columns):
179
+ col_idx = (model.index if model.index is not None else offset)
180
+ col_mapping[col_idx] = model
181
+ return col_mapping
182
+
183
+ def _convert_from_cell(self, model: CsvColumnModel, cell_value: Any) -> Any:
184
+ converter = resolve_csv_converter(model.py_type, model.converter, model.date_format)
185
+ if converter is None:
186
+ return cell_value
187
+ try:
188
+ return converter.from_excel(cell_value)
189
+ except Exception as e:
190
+ raise CsvReadError(
191
+ f"字段 '{model.attr_name}' 转换失败 (单元格值={cell_value!r}): {e}"
192
+ ) from e
193
+
194
+
195
+ __all__ = ["CsvReader"]
spring/csv/writer.py ADDED
@@ -0,0 +1,155 @@
1
+ """SpringBootAI CSV 写入引擎。
2
+
3
+ 按 ``@CsvProperty`` / ``@CsvIgnore`` / ``@csv_file`` 注解把实体对象列表写入 CSV。
4
+ 底层使用 Python 标准库 ``csv``(**无可选依赖**,开箱即用)。
5
+
6
+ 写入流程对齐 Excel 模块(``spring.excel.writer``)与 alibaba EasyExcel:
7
+ 1. 按列模型排序写表头行(``has_header=True`` 时)。
8
+ 2. 逐行按 ``converter.to_excel`` 转换并写单元格;处理大数字防丢精度。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import csv as _csv
13
+ from typing import Any, Iterable, List, Optional, Type
14
+
15
+ from .annotations import (
16
+ CsvColumnModel, CsvFile, _get_class_file_meta, parse_csv_columns,
17
+ )
18
+ from .converters import resolve_csv_converter
19
+ from .exceptions import CsvWriteError
20
+
21
+
22
+ def _get_value(item: Any, attr_name: str) -> Any:
23
+ """从实体对象或 dict 取字段值。镜像 Excel ``_get_value``。"""
24
+ if isinstance(item, dict):
25
+ return item.get(attr_name)
26
+ return getattr(item, attr_name, None)
27
+
28
+
29
+ def _is_large_int(value: Any) -> bool:
30
+ """是否为会丢精度的长整数(>15 位有效数字)。镜像 Excel ``_is_large_int``。"""
31
+ if isinstance(value, bool):
32
+ return False
33
+ if isinstance(value, int):
34
+ return len(str(abs(value))) > 15
35
+ return False
36
+
37
+
38
+ def _open_for_write(target: Any, encoding: str):
39
+ """支持文件路径(str/Path)或类文件对象。"""
40
+ try:
41
+ if isinstance(target, (str, bytes)) or hasattr(target, "__fspath__"):
42
+ return open(target, "w", encoding=encoding, newline="")
43
+ return target
44
+ except Exception as e:
45
+ raise CsvWriteError(f"打开 CSV 文件失败: {e}") from e
46
+
47
+
48
+ class CsvWriter:
49
+ """CSV 写入器。由 ``EasyCsv.write(...)`` 构建,调用 ``doWrite`` 执行。"""
50
+
51
+ def __init__(
52
+ self,
53
+ target: Any,
54
+ head: Optional[Type] = None,
55
+ delimiter: Optional[str] = None,
56
+ encoding: Optional[str] = None,
57
+ ):
58
+ self.target = target
59
+ self.head = head
60
+ self._delimiter = delimiter
61
+ self._encoding = encoding
62
+ self._has_header: Optional[bool] = None
63
+
64
+ # ---- 流式配置 ----
65
+
66
+ def delimiter(self, d: str) -> "CsvWriter":
67
+ self._delimiter = d
68
+ return self
69
+
70
+ def encoding(self, enc: str) -> "CsvWriter":
71
+ self._encoding = enc
72
+ return self
73
+
74
+ def has_header(self, flag: bool) -> "CsvWriter":
75
+ self._has_header = flag
76
+ return self
77
+
78
+ # ---- 执行 ----
79
+
80
+ def _resolve_meta(self) -> CsvFile:
81
+ meta = _get_class_file_meta(self.head) if self.head is not None else CsvFile()
82
+ if self._delimiter is not None:
83
+ meta.delimiter = self._delimiter
84
+ if self._encoding is not None:
85
+ meta.encoding = self._encoding
86
+ if self._has_header is not None:
87
+ meta.has_header = self._has_header
88
+ return meta
89
+
90
+ def doWrite(self, data: Iterable[Any]) -> Any:
91
+ """写入 CSV 并保存到 ``target``。返回目标路径。"""
92
+ if self.head is None:
93
+ raise CsvWriteError("write 需指定 head 实体类")
94
+ meta = self._resolve_meta()
95
+ columns: List[CsvColumnModel] = parse_csv_columns(self.head)
96
+
97
+ f = _open_for_write(self.target, meta.encoding)
98
+ owns_fh = f is not self.target
99
+ try:
100
+ writer = _csv.writer(
101
+ f,
102
+ delimiter=meta.delimiter,
103
+ quotechar=meta.quote_char,
104
+ lineterminator=meta.line_terminator,
105
+ )
106
+ # 1. 表头
107
+ if meta.has_header:
108
+ writer.writerow([m.header for m in columns])
109
+ # 2. 数据行
110
+ for item in data:
111
+ row = []
112
+ for model in columns:
113
+ raw = _get_value(item, model.attr_name)
114
+ cell_value = self._convert_to_cell(model, raw)
115
+ row.append(cell_value)
116
+ writer.writerow(row)
117
+ except Exception as e:
118
+ raise CsvWriteError(f"写入 CSV 文件失败: {e}") from e
119
+ finally:
120
+ if owns_fh:
121
+ try:
122
+ f.close()
123
+ except Exception:
124
+ pass
125
+ return self.target
126
+
127
+ def _convert_to_cell(self, model: CsvColumnModel, raw: Any) -> str:
128
+ """Python 值 -> CSV 单元格字符串。"""
129
+ converter = resolve_csv_converter(model.py_type, model.converter, model.date_format)
130
+ if converter is not None:
131
+ try:
132
+ converted = converter.to_excel(raw)
133
+ except Exception as e:
134
+ raise CsvWriteError(
135
+ f"字段 '{model.attr_name}' 转换失败 (值={raw!r}): {e}"
136
+ ) from e
137
+ return self._to_cell_str(converted)
138
+ # 无转换器:按规则处理
139
+ if raw is None:
140
+ return ""
141
+ if model.big_number or _is_large_int(raw):
142
+ return str(raw)
143
+ return self._to_cell_str(raw)
144
+
145
+ @staticmethod
146
+ def _to_cell_str(value: Any) -> str:
147
+ """统一转字符串:None -> "",bool -> "True"/"False",其余 str(value)。"""
148
+ if value is None:
149
+ return ""
150
+ if isinstance(value, bool):
151
+ return "True" if value else "False"
152
+ return str(value)
153
+
154
+
155
+ __all__ = ["CsvWriter"]
@@ -0,0 +1,54 @@
1
+ """SpringBootAI Spring Data 模块(对齐 Spring Data Commons / Spring Data JPA)。
2
+
3
+ 提供基于实体元数据的统一数据访问抽象:分页(``Pageable``/``Page``)、排序(``Sort``)、
4
+ 动态查询(``Specification``)、CRUD 仓库基类(``PagingAndSortingRepository``)。
5
+
6
+ 复用现有范式:
7
+ - 实体解析复用 ``spring.orm.ddl_auto.DdlAutoManager._parse_entity``
8
+ - SQL 执行复用 ``OptimisticLockExecutor`` 的轻量范式(pool + cursor)
9
+ - 无需 PyMyBatis ``SqlSession`` 即可使用
10
+
11
+ 用法::
12
+
13
+ from spring.data import PagingAndSortingRepository, Pageable, Sort, Specifications
14
+ from spring.orm import entity, Id, Column
15
+
16
+ @entity("user")
17
+ class User:
18
+ id = Id()
19
+ name = Column("user_name")
20
+ age = Column()
21
+ def __init__(self, id=None, name=None, age=None):
22
+ self.id = id; self.name = name; self.age = age
23
+
24
+ repo = PagingAndSortingRepository(pool, User, dialect="sqlite")
25
+ repo.save(User(name="Tom", age=18))
26
+ page = repo.find_all(pageable=Pageable.of(0, 10, Sort.by("age")))
27
+ adults = repo.find_all(specification=Specifications.greater_equal("age", 18))
28
+ """
29
+ from spring.data.page import Direction, Order, Sort, Pageable, Page
30
+ from spring.data.specification import (
31
+ Specification,
32
+ And, Or, Not,
33
+ Predicate, ColResolver,
34
+ Specifications,
35
+ )
36
+ from spring.data.repository import (
37
+ PagingAndSortingRepository,
38
+ DataRepository,
39
+ get_data_repository_entity,
40
+ )
41
+
42
+ __version__ = "1.0.0"
43
+
44
+ __all__ = [
45
+ # 分页/排序
46
+ "Direction", "Order", "Sort", "Pageable", "Page",
47
+ # 动态查询
48
+ "Specification", "And", "Or", "Not", "Predicate", "ColResolver",
49
+ "Specifications",
50
+ # 仓库
51
+ "PagingAndSortingRepository",
52
+ "DataRepository", "get_data_repository_entity",
53
+ "__version__",
54
+ ]
spring/data/page.py ADDED
@@ -0,0 +1,181 @@
1
+ """Spring Data 风格的分页/排序值对象(对齐 ``org.springframework.data.domain``)。
2
+
3
+ 提供 ``Pageable`` / ``Page`` / ``Sort`` / ``Order``,供 ``PagingAndSortingRepository``
4
+ 与上层服务使用。纯值对象,无 IO 依赖,便于单测。
5
+
6
+ 与 Java 差异:
7
+ - ``Pageable`` 为可实例化的值对象(Python 无接口),``.offset`` 直接计算为 ``(page_number) * page_size``。
8
+ - ``Sort.Order`` 的 ``direction`` 用枚举 ``Direction.ASC/DESC``。
9
+ - 页码从 0 起(与 Spring Data 一致),``page_number=0`` 为第一页。
10
+ """
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from typing import Any, Generic, List, Optional, TypeVar
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ class Direction(Enum):
19
+ ASC = "ASC"
20
+ DESC = "DESC"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Order:
25
+ """单字段排序指令。"""
26
+ property: str
27
+ direction: Direction = Direction.ASC
28
+
29
+ @staticmethod
30
+ def asc(property: str) -> "Order":
31
+ return Order(property=property, direction=Direction.ASC)
32
+
33
+ @staticmethod
34
+ def desc(property: str) -> "Order":
35
+ return Order(property=property, direction=Direction.DESC)
36
+
37
+ def to_sql(self, col_resolver=None) -> str:
38
+ """转 SQL 片段 ``"col" ASC``。
39
+
40
+ Args:
41
+ col_resolver: 可选 ``property_name -> sql_column_name`` 映射函数,
42
+ 用于把 Python 属性名翻译为真实列名(对齐 ``Column(name=...)``)。
43
+ """
44
+ col = col_resolver(self.property) if col_resolver else self.property
45
+ return f"{col} {self.direction.value}"
46
+
47
+
48
+ class Sort:
49
+ """多字段排序(对齐 ``Sort``)。空 ``orders`` 表示不排序。
50
+
51
+ 不可变值对象:构造后 ``orders`` 为 tuple。
52
+ """
53
+
54
+ __slots__ = ("orders",)
55
+
56
+ def __init__(self, *orders):
57
+ object.__setattr__(self, "orders", tuple(orders))
58
+
59
+ def __setattr__(self, name, value):
60
+ raise AttributeError("Sort 是不可变值对象")
61
+
62
+ def __delattr__(self, name):
63
+ raise AttributeError("Sort 是不可变值对象")
64
+
65
+ def __eq__(self, other):
66
+ return isinstance(other, Sort) and self.orders == other.orders
67
+
68
+ def __repr__(self):
69
+ return f"Sort({', '.join(repr(o) for o in self.orders)})"
70
+
71
+ @staticmethod
72
+ def by(*properties: str) -> "Sort":
73
+ return Sort(*[Order(p) for p in properties])
74
+
75
+ @staticmethod
76
+ def unsorted() -> "Sort":
77
+ return Sort()
78
+
79
+ @property
80
+ def is_sorted(self) -> bool:
81
+ return bool(self.orders)
82
+
83
+ def to_sql(self, col_resolver=None) -> str:
84
+ if not self.orders:
85
+ return ""
86
+ return ", ".join(o.to_sql(col_resolver) for o in self.orders)
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class Pageable:
91
+ """分页请求(页码从 0 起)。"""
92
+ page_number: int = 0
93
+ page_size: int = 20
94
+ sort: Sort = field(default_factory=Sort.unsorted)
95
+
96
+ def __post_init__(self):
97
+ if self.page_number < 0:
98
+ raise ValueError(f"page_number 不能为负: {self.page_number}")
99
+ if self.page_size <= 0:
100
+ raise ValueError(f"page_size 必须为正: {self.page_size}")
101
+
102
+ @property
103
+ def offset(self) -> int:
104
+ return self.page_number * self.page_size
105
+
106
+ @property
107
+ def limit(self) -> int:
108
+ return self.page_size
109
+
110
+ def next(self) -> "Pageable":
111
+ return Pageable(self.page_number + 1, self.page_size, self.sort)
112
+
113
+ def previous_or_first(self) -> "Pageable":
114
+ return Pageable(max(0, self.page_number - 1), self.page_size, self.sort)
115
+
116
+ def first(self) -> "Pageable":
117
+ return Pageable(0, self.page_size, self.sort)
118
+
119
+ @staticmethod
120
+ def of(page_number: int = 0, page_size: int = 20, sort: Optional[Sort] = None) -> "Pageable":
121
+ return Pageable(page_number, page_size, sort or Sort.unsorted())
122
+
123
+
124
+ @dataclass
125
+ class Page(Generic[T]):
126
+ """分页结果(对齐 ``Page<T>``)。"""
127
+ content: List[T]
128
+ pageable: Pageable
129
+ total: int
130
+
131
+ @property
132
+ def number(self) -> int:
133
+ return self.pageable.page_number
134
+
135
+ @property
136
+ def size(self) -> int:
137
+ return self.pageable.page_size
138
+
139
+ @property
140
+ def total_pages(self) -> int:
141
+ if self.size == 0:
142
+ return 0
143
+ return (self.total + self.size - 1) // self.size
144
+
145
+ @property
146
+ def number_of_elements(self) -> int:
147
+ return len(self.content)
148
+
149
+ @property
150
+ def has_content(self) -> bool:
151
+ return bool(self.content)
152
+
153
+ @property
154
+ def has_next(self) -> bool:
155
+ return self.number + 1 < self.total_pages
156
+
157
+ @property
158
+ def has_previous(self) -> bool:
159
+ return self.number > 0
160
+
161
+ @property
162
+ def is_first(self) -> bool:
163
+ return self.number == 0
164
+
165
+ @property
166
+ def is_last(self) -> bool:
167
+ return not self.has_next
168
+
169
+ @property
170
+ def is_empty(self) -> bool:
171
+ return not self.has_content
172
+
173
+ def next_pageable(self) -> Optional[Pageable]:
174
+ return self.pageable.next() if self.has_next else None
175
+
176
+ def previous_pageable(self) -> Optional[Pageable]:
177
+ return self.pageable.previous_or_first() if self.has_previous else None
178
+
179
+ @staticmethod
180
+ def empty(pageable: Pageable) -> "Page":
181
+ return Page([], pageable, 0)