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,58 @@
1
+ from typing import Dict, Type, Any, Optional
2
+
3
+
4
+ class BeanRegistry:
5
+ _instance: Optional['BeanRegistry'] = None
6
+ _initialized: bool = False
7
+
8
+ def __new__(cls, *args, **kwargs):
9
+ if cls._instance is None:
10
+ cls._instance = super().__new__(cls)
11
+ return cls._instance
12
+
13
+ def __init__(self):
14
+ if self._initialized:
15
+ return
16
+ self._beans: Dict[str, Any] = {}
17
+ self._types: Dict[Type, str] = {}
18
+ self._initialized = True
19
+
20
+ def register(self, name: str, bean: Any) -> None:
21
+ self._beans[name] = bean
22
+ self._types[type(bean)] = name
23
+
24
+ def get(self, name: str) -> Any:
25
+ return self._beans.get(name)
26
+
27
+ def get_by_type(self, bean_type: Type) -> Any:
28
+ if bean_type in self._types:
29
+ return self._beans[self._types[bean_type]]
30
+ for bean in self._beans.values():
31
+ if isinstance(bean, bean_type):
32
+ return bean
33
+ return None
34
+
35
+ def contains(self, name: str) -> bool:
36
+ return name in self._beans
37
+
38
+ def contains_type(self, bean_type: Type) -> bool:
39
+ return bean_type in self._types
40
+
41
+ def unregister(self, name: str) -> None:
42
+ if name in self._beans:
43
+ bean = self._beans[name]
44
+ del self._types[type(bean)]
45
+ del self._beans[name]
46
+
47
+ def clear(self) -> None:
48
+ self._beans.clear()
49
+ self._types.clear()
50
+
51
+ def get_all(self) -> Dict[str, Any]:
52
+ return dict(self._beans)
53
+
54
+ def get_names(self) -> list:
55
+ return list(self._beans.keys())
56
+
57
+ def get_count(self) -> int:
58
+ return len(self._beans)
@@ -0,0 +1,106 @@
1
+ from typing import List, Type, Any
2
+ import importlib
3
+ import os
4
+ import pkgutil
5
+ from spring.annotations.core import (
6
+ Component,
7
+ Service,
8
+ Repository,
9
+ Controller,
10
+ RestController,
11
+ Configuration,
12
+ ControllerAdvice,
13
+ ConfigurationProperties,
14
+ get_spring_annotations,
15
+ )
16
+
17
+
18
+ class ComponentScanner:
19
+ def __init__(self, application_context: Any):
20
+ self.application_context = application_context
21
+ self._scanned_classes: set = set()
22
+
23
+ def scan(self, base_packages: List[str]) -> List[Type]:
24
+ components: List[Type] = []
25
+ for package_name in base_packages:
26
+ components.extend(self._scan_package(package_name))
27
+ return components
28
+
29
+ def scan_classes(self, base_packages: List[str]) -> List[Type]:
30
+ """Import packages and return all declared classes once.
31
+
32
+ Cloud integrations such as Feign are interface-like declarations and
33
+ deliberately are not IoC components themselves, so they need a
34
+ separate class scan from the component scan.
35
+ """
36
+ classes: List[Type] = []
37
+ for package_name in base_packages:
38
+ classes.extend(self._scan_package_classes(package_name))
39
+ return classes
40
+
41
+ def _scan_package_classes(self, package_name: str) -> List[Type]:
42
+ classes: List[Type] = []
43
+ try:
44
+ package = importlib.import_module(package_name)
45
+ except ImportError:
46
+ return classes
47
+ if hasattr(package, '__path__'):
48
+ for _, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package_name + '.'):
49
+ if is_pkg:
50
+ classes.extend(self._scan_package_classes(module_name))
51
+ else:
52
+ try:
53
+ module = importlib.import_module(module_name)
54
+ except ImportError:
55
+ continue
56
+ classes.extend(
57
+ obj for obj in vars(module).values()
58
+ if isinstance(obj, type) and obj.__module__ == module.__name__
59
+ )
60
+ return classes
61
+
62
+ def _scan_package(self, package_name: str) -> List[Type]:
63
+ components: List[Type] = []
64
+ try:
65
+ package = importlib.import_module(package_name)
66
+ except ImportError:
67
+ return components
68
+
69
+ if hasattr(package, '__path__'):
70
+ for _, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package_name + '.'):
71
+ if is_pkg:
72
+ components.extend(self._scan_package(module_name))
73
+ else:
74
+ try:
75
+ module = importlib.import_module(module_name)
76
+ components.extend(self._find_components_in_module(module))
77
+ except ImportError:
78
+ continue
79
+ return components
80
+
81
+ def _find_components_in_module(self, module: Any) -> List[Type]:
82
+ components: List[Type] = []
83
+ for name in dir(module):
84
+ obj = getattr(module, name)
85
+ if isinstance(obj, type) and obj not in self._scanned_classes:
86
+ if self._is_component(obj):
87
+ self._scanned_classes.add(obj)
88
+ components.append(obj)
89
+ return components
90
+
91
+ def _is_component(self, cls: Type) -> bool:
92
+ annotations = get_spring_annotations(cls)
93
+ component_annotations = (
94
+ Component,
95
+ Service,
96
+ Repository,
97
+ Controller,
98
+ RestController,
99
+ Configuration,
100
+ ControllerAdvice,
101
+ ConfigurationProperties,
102
+ )
103
+ for annotation in annotations:
104
+ if isinstance(annotation, component_annotations):
105
+ return True
106
+ return False
@@ -0,0 +1,3 @@
1
+ from spring.core.graceful_shutdown import GracefulShutdown, shutdown_handler
2
+
3
+ __all__ = ['GracefulShutdown', 'shutdown_handler']
@@ -0,0 +1,196 @@
1
+ """
2
+ 优雅退出处理器 (Graceful Shutdown)
3
+
4
+ 功能:
5
+ - 捕获 SIGTERM/SIGINT 信号
6
+ - 停止接收新请求(健康检查返回 NOT_READY)
7
+ - 等待在途请求完成(可配置超时)
8
+ - 关闭连接池、消息消费者等资源
9
+ - 注销服务发现
10
+ """
11
+
12
+ import signal
13
+ import time
14
+ import logging
15
+ import threading
16
+ import asyncio
17
+ from typing import Optional, Callable, List, Dict, Any
18
+ from enum import Enum
19
+
20
+ logger = logging.getLogger("Spring.Core.Shutdown")
21
+
22
+
23
+ class ShutdownPhase(Enum):
24
+ RUNNING = "RUNNING"
25
+ DRAINING = "DRAINING" # 停止接收新请求
26
+ SHUTTING_DOWN = "SHUTTING_DOWN" # 关闭资源
27
+ STOPPED = "STOPPED"
28
+
29
+
30
+ class GracefulShutdown:
31
+ """
32
+ 优雅退出管理器
33
+
34
+ Usage:
35
+ shutdown = GracefulShutdown()
36
+ shutdown.register_hook("db_pool", pool.close)
37
+ shutdown.register_hook("redis", redis_client.close)
38
+ # 信号会自动注册
39
+ """
40
+
41
+ def __init__(self, drain_timeout: float = 30.0, shutdown_timeout: float = 30.0):
42
+ self.drain_timeout = drain_timeout
43
+ self.shutdown_timeout = shutdown_timeout
44
+ self._phase = ShutdownPhase.RUNNING
45
+ self._phase_lock = threading.RLock()
46
+ self._hooks: Dict[str, Callable] = {}
47
+ self._hooks_order: List[str] = []
48
+ self._inflight_count = 0
49
+ self._inflight_lock = threading.RLock()
50
+ self._shutdown_event = threading.Event()
51
+ self._original_sigterm = None
52
+ self._original_sigint = None
53
+ self._signal_received = False
54
+ self._shutdown_start_time: Optional[float] = None
55
+
56
+ def register_signals(self):
57
+ """注册系统信号处理器"""
58
+ try:
59
+ self._original_sigterm = signal.getsignal(signal.SIGTERM)
60
+ self._original_sigint = signal.getsignal(signal.SIGINT)
61
+ signal.signal(signal.SIGTERM, self._signal_handler)
62
+ signal.signal(signal.SIGINT, self._signal_handler)
63
+ logger.info("Graceful shutdown signal handlers registered (SIGTERM/SIGINT)")
64
+ except (ValueError, OSError) as e:
65
+ # 在非主线程中无法注册信号,忽略
66
+ logger.debug(f"Cannot register signal handlers: {e}")
67
+
68
+ def _signal_handler(self, signum, frame):
69
+ """信号处理回调"""
70
+ if self._signal_received:
71
+ logger.warning("Second signal received, forcing immediate shutdown")
72
+ self._force_shutdown()
73
+ return
74
+ self._signal_received = True
75
+ sig_name = signal.Signals(signum).name
76
+ logger.info(f"Received signal {sig_name}, starting graceful shutdown...")
77
+ threading.Thread(target=self.initiate_shutdown, daemon=True).start()
78
+
79
+ def register_hook(self, name: str, hook: Callable, order: int = 0):
80
+ """
81
+ 注册关闭钩子
82
+
83
+ Args:
84
+ name: 钩子名称(唯一标识)
85
+ hook: 无参可调用对象
86
+ order: 执行顺序(越小越先执行)
87
+ """
88
+ with self._phase_lock:
89
+ self._hooks[name] = (order, hook)
90
+ self._hooks_order = sorted(self._hooks.keys(), key=lambda n: self._hooks[n][0])
91
+ logger.debug(f"Registered shutdown hook: {name} (order={order})")
92
+
93
+ def request_started(self):
94
+ """请求开始时调用,跟踪在途请求数"""
95
+ with self._inflight_lock:
96
+ self._inflight_count += 1
97
+
98
+ def request_finished(self):
99
+ """请求结束时调用"""
100
+ with self._inflight_lock:
101
+ self._inflight_count -= 1
102
+
103
+ @property
104
+ def is_draining(self) -> bool:
105
+ """是否正在排空请求(不应接收新请求)"""
106
+ with self._phase_lock:
107
+ return self._phase in (ShutdownPhase.DRAINING, ShutdownPhase.SHUTTING_DOWN, ShutdownPhase.STOPPED)
108
+
109
+ @property
110
+ def is_shutting_down(self) -> bool:
111
+ return self._phase in (ShutdownPhase.SHUTTING_DOWN, ShutdownPhase.STOPPED)
112
+
113
+ @property
114
+ def phase(self) -> ShutdownPhase:
115
+ return self._phase
116
+
117
+ @property
118
+ def inflight_count(self) -> int:
119
+ return self._inflight_count
120
+
121
+ def initiate_shutdown(self):
122
+ """启动优雅关闭流程"""
123
+ with self._phase_lock:
124
+ if self._phase != ShutdownPhase.RUNNING:
125
+ return
126
+ self._phase = ShutdownPhase.DRAINING
127
+ self._shutdown_start_time = time.monotonic()
128
+
129
+ logger.info("Phase 1: Draining - stopping new requests, waiting for in-flight requests...")
130
+
131
+ # 等待在途请求完成
132
+ deadline = time.monotonic() + self.drain_timeout
133
+ while time.monotonic() < deadline:
134
+ with self._inflight_lock:
135
+ inflight = self._inflight_count
136
+ if inflight == 0:
137
+ break
138
+ logger.info(f"Waiting for {inflight} in-flight request(s)... ({deadline - time.monotonic():.1f}s remaining)")
139
+ time.sleep(0.5)
140
+
141
+ with self._inflight_lock:
142
+ remaining = self._inflight_count
143
+ if remaining > 0:
144
+ logger.warning(f"{remaining} request(s) still in-flight after drain timeout, proceeding to shutdown")
145
+
146
+ # 关闭资源
147
+ with self._phase_lock:
148
+ self._phase = ShutdownPhase.SHUTTING_DOWN
149
+
150
+ logger.info("Phase 2: Shutting down resources...")
151
+ self._execute_hooks()
152
+
153
+ with self._phase_lock:
154
+ self._phase = ShutdownPhase.STOPPED
155
+
156
+ elapsed = time.monotonic() - self._shutdown_start_time
157
+ logger.info(f"Graceful shutdown completed in {elapsed:.2f}s")
158
+ self._shutdown_event.set()
159
+
160
+ def _execute_hooks(self):
161
+ """执行所有关闭钩子"""
162
+ for name in self._hooks_order:
163
+ _, hook = self._hooks[name]
164
+ hook_start = time.monotonic()
165
+ try:
166
+ result = hook()
167
+ if asyncio.iscoroutine(result):
168
+ # 尝试在当前事件循环中运行协程
169
+ try:
170
+ loop = asyncio.get_event_loop()
171
+ if loop.is_running():
172
+ asyncio.ensure_future(result)
173
+ else:
174
+ loop.run_until_complete(result)
175
+ except RuntimeError:
176
+ pass
177
+ elapsed = time.monotonic() - hook_start
178
+ logger.info(f"Shutdown hook '{name}' completed in {elapsed:.2f}s")
179
+ except Exception as e:
180
+ elapsed = time.monotonic() - hook_start
181
+ logger.error(f"Shutdown hook '{name}' failed after {elapsed:.2f}s: {e}")
182
+
183
+ def _force_shutdown(self):
184
+ """强制关闭"""
185
+ logger.critical("Force shutdown initiated")
186
+ with self._phase_lock:
187
+ self._phase = ShutdownPhase.STOPPED
188
+ self._shutdown_event.set()
189
+
190
+ def wait_for_shutdown(self, timeout: Optional[float] = None):
191
+ """等待关闭完成"""
192
+ self._shutdown_event.wait(timeout=timeout)
193
+
194
+
195
+ # 全局单例
196
+ shutdown_handler = GracefulShutdown()
@@ -0,0 +1,50 @@
1
+ """共享类型工具 —— 规范化类型注解,消除 Python 版本差异。
2
+
3
+ 背景:``typing.get_type_hints`` 在 Python 3.10 上会把带 ``None`` 默认值的参数注解
4
+ 自动包装为 ``Optional[X]``(即 ``Union[X, None]``);自 Python 3.11 起该自动包装
5
+ 行为被移除,注解原样返回。这导致同一份代码在 3.10 与 3.11/3.12 上得到不同的
6
+ ``py_type``,进而影响 SQL 类型映射、CSV 转换器选择等下游推断。
7
+
8
+ 本模块统一把 ``Optional[X]`` / ``Union[X, None]`` 解包为 ``X``,使下游类型推断与
9
+ Python 版本无关。对齐 Spring ``org.springframework.core.ResolvableType`` 的职责
10
+ (提供统一的类型解析语义),仅做最小必要的可空解包。
11
+ """
12
+ import typing
13
+
14
+ # NoneType 的单例,用于在 Union 参数中识别并剔除 ``None``
15
+ _NONE_TYPE = type(None)
16
+
17
+
18
+ def unwrap_optional_type(tp):
19
+ """把 ``Optional[X]`` / ``Union[X, None]`` 解包为 ``X``;其他类型原样返回。
20
+
21
+ 用于从 ``get_type_hints`` 得到的类型中取出“承载类型”,供 SQL 类型映射、
22
+ 转换器选择等只关心实际承载类型、忽略可空性的场景。可空性由 ``nullable``
23
+ 等列元数据单独表达,不应混入类型映射。
24
+
25
+ - ``Optional[int]`` / ``Union[int, None]`` -> ``int``
26
+ - ``int`` -> ``int``(原样返回)
27
+ - ``Union[int, str, None]`` -> 原样返回(多元素 Union 无单一承载类型,不解包)
28
+ - ``None`` -> ``None``
29
+
30
+ Examples:
31
+ >>> import typing
32
+ >>> unwrap_optional_type(typing.Optional[int]) is int
33
+ True
34
+ >>> unwrap_optional_type(int) is int
35
+ True
36
+ >>> unwrap_optional_type(None) is None
37
+ True
38
+ """
39
+ if tp is None:
40
+ return tp
41
+ # Optional[X] 在运行期等价于 Union[X, None],其 origin 为 typing.Union
42
+ if typing.get_origin(tp) is typing.Union:
43
+ args = [a for a in typing.get_args(tp) if a is not _NONE_TYPE]
44
+ # 仅当剔除 None 后剩单一承载类型时才解包;多元素 Union 保持原样
45
+ if len(args) == 1:
46
+ return args[0]
47
+ return tp
48
+
49
+
50
+ __all__ = ["unwrap_optional_type"]
spring/csv/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """SpringBootAI CSV 模块 —— 注解驱动的 CSV 读写(对齐 alibaba EasyExcel / commons-csv)。
2
+
3
+ 模块组成(镜像 ``spring.excel`` 架构):
4
+ - annotations: ``@CsvProperty`` / ``@CsvIgnore`` / ``@csv_file`` 字段+类级注解
5
+ (复用 ORM ``Column``/``@entity`` 与 Excel ``ExcelProperty`` 元数据描述符范式)
6
+ - converters: 复用 ``spring.excel.converters`` 的 ``Converter`` 接口与内置转换器(DRY)
7
+ - reader: ``CsvReader`` 读取引擎(表头映射/类型转换/位置回退)
8
+ - writer: ``CsvWriter`` 写入引擎(表头/顺序/大数字防丢精度)
9
+ - easy_csv: ``EasyCsv`` 流式构建入口(对齐 ``EasyExcel`` API)
10
+ - exceptions: ``CsvError`` 异常族
11
+
12
+ 与 Excel 模块的核心区别:
13
+ - **无可选依赖**:CSV 使用 Python 标准库 ``csv``,``pip install springbootAI`` 即可用,
14
+ 无需 ``springbootAI[excel]`` 等额外 extras。
15
+ - 转换器复用 Excel 模块(``spring.excel.converters`` 不依赖 openpyxl,可安全导入)。
16
+ - 无单元格样式/数字格式(CSV 格式本身不支持)。
17
+
18
+ 设计原则:**复用项目既有范式,不重复造轮子**。注解描述符、反射解析、流式 API 全部对齐
19
+ 既有 Excel/ORM 实现,未引入任何第三方库。
20
+ """
21
+ from .exceptions import CsvError, CsvPropertyError, CsvReadError, CsvWriteError
22
+ from .annotations import (
23
+ CsvProperty, CsvIgnore, CsvFile, csv_file,
24
+ CsvColumnModel, parse_csv_columns,
25
+ )
26
+ from .converters import (
27
+ Converter, CsvConverter,
28
+ StringConverter, IntegerConverter, FloatConverter,
29
+ BooleanConverter, DateStringConverter, BigDecimalConverter,
30
+ resolve_converter, resolve_csv_converter,
31
+ )
32
+ from .reader import CsvReader
33
+ from .writer import CsvWriter
34
+ from .easy_csv import EasyCsv, read_csv, write_csv
35
+
36
+ __version__ = "1.0.0"
37
+
38
+ __all__ = [
39
+ # 异常
40
+ "CsvError", "CsvPropertyError", "CsvReadError", "CsvWriteError",
41
+ # 注解
42
+ "CsvProperty", "CsvIgnore", "CsvFile", "csv_file",
43
+ "CsvColumnModel", "parse_csv_columns",
44
+ # 转换器
45
+ "Converter", "CsvConverter",
46
+ "StringConverter", "IntegerConverter", "FloatConverter",
47
+ "BooleanConverter", "DateStringConverter", "BigDecimalConverter",
48
+ "resolve_converter", "resolve_csv_converter",
49
+ # 引擎
50
+ "CsvReader", "CsvWriter", "EasyCsv", "read_csv", "write_csv",
51
+ "__version__",
52
+ ]