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.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- 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"]
|
spring/data/__init__.py
ADDED
|
@@ -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)
|