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
|
@@ -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
|
spring/core/__init__.py
ADDED
|
@@ -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
|
+
]
|