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/retry/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
重试注解定义
|
|
3
|
+
"""
|
|
4
|
+
from typing import Type, Tuple, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Backoff:
|
|
8
|
+
"""
|
|
9
|
+
退避策略配置
|
|
10
|
+
|
|
11
|
+
参数:
|
|
12
|
+
delay: 初始延迟时间(毫秒),默认1000
|
|
13
|
+
max_delay: 最大延迟时间(毫秒),默认10000
|
|
14
|
+
multiplier: 延迟倍增因子,默认2.0(指数退避)
|
|
15
|
+
random_factor: 随机因子(0.0-1.0),默认0.1
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
delay: int = 1000,
|
|
21
|
+
max_delay: int = 10000,
|
|
22
|
+
multiplier: float = 2.0,
|
|
23
|
+
random_factor: float = 0.1
|
|
24
|
+
):
|
|
25
|
+
self.delay = delay
|
|
26
|
+
self.max_delay = max_delay
|
|
27
|
+
self.multiplier = multiplier
|
|
28
|
+
self.random_factor = random_factor
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Retryable:
|
|
32
|
+
"""
|
|
33
|
+
重试注解
|
|
34
|
+
|
|
35
|
+
参数:
|
|
36
|
+
value: 要重试的异常类型(或异常类型元组)
|
|
37
|
+
max_retries: 最大重试次数,默认3(包含首次调用)
|
|
38
|
+
backoff: 退避策略配置
|
|
39
|
+
exclude: 不重试的异常类型(或异常类型元组)
|
|
40
|
+
recover: 恢复方法名(当重试失败时调用)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
value: Optional[Tuple[Type[Exception], ...]] = None,
|
|
46
|
+
max_retries: int = 3,
|
|
47
|
+
backoff: Optional[object] = None,
|
|
48
|
+
exclude: Optional[Tuple[Type[Exception], ...]] = None,
|
|
49
|
+
recover: str = "",
|
|
50
|
+
max_attempts: Optional[int] = None,
|
|
51
|
+
):
|
|
52
|
+
if max_attempts is not None:
|
|
53
|
+
if max_retries != 3 and max_retries != max_attempts:
|
|
54
|
+
raise ValueError("max_retries 与 max_attempts 不能设置为不同值")
|
|
55
|
+
max_retries = max_attempts
|
|
56
|
+
if max_retries <= 0:
|
|
57
|
+
raise ValueError("max_retries 必须大于0")
|
|
58
|
+
if isinstance(backoff, (int, float)):
|
|
59
|
+
if backoff < 0:
|
|
60
|
+
raise ValueError("backoff 延迟不能小于0")
|
|
61
|
+
backoff = Backoff(
|
|
62
|
+
delay=int(backoff),
|
|
63
|
+
max_delay=int(backoff),
|
|
64
|
+
multiplier=1.0,
|
|
65
|
+
random_factor=0.0,
|
|
66
|
+
)
|
|
67
|
+
self.value = value or (Exception,)
|
|
68
|
+
self.max_retries = max_retries
|
|
69
|
+
self.backoff = backoff or Backoff()
|
|
70
|
+
self.exclude = exclude or ()
|
|
71
|
+
self.recover = recover
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
重试切面实现
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
import random
|
|
6
|
+
import logging
|
|
7
|
+
import functools
|
|
8
|
+
from typing import Callable, Tuple, Type
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("Spring.Retry")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def retryable_decorator(annotation):
|
|
14
|
+
"""
|
|
15
|
+
@Retryable注解切面实现
|
|
16
|
+
|
|
17
|
+
支持:
|
|
18
|
+
- 指定重试的异常类型
|
|
19
|
+
- 指定不重试的异常类型
|
|
20
|
+
- 最大重试次数
|
|
21
|
+
- 退避策略(固定延迟/指数退避)
|
|
22
|
+
- 随机因子
|
|
23
|
+
- 恢复方法(recover)
|
|
24
|
+
"""
|
|
25
|
+
def decorator(func: Callable) -> Callable:
|
|
26
|
+
@functools.wraps(func)
|
|
27
|
+
def wrapper(*args, **kwargs):
|
|
28
|
+
max_retries = annotation.max_retries
|
|
29
|
+
exceptions_to_retry = annotation.value
|
|
30
|
+
exceptions_to_exclude = annotation.exclude
|
|
31
|
+
backoff = annotation.backoff
|
|
32
|
+
recover_method = annotation.recover
|
|
33
|
+
|
|
34
|
+
last_exception = None
|
|
35
|
+
retry_count = 0
|
|
36
|
+
|
|
37
|
+
while retry_count < max_retries:
|
|
38
|
+
try:
|
|
39
|
+
return func(*args, **kwargs)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
last_exception = e
|
|
42
|
+
retry_count += 1
|
|
43
|
+
|
|
44
|
+
# 检查是否是需要排除的异常
|
|
45
|
+
if isinstance(e, exceptions_to_exclude):
|
|
46
|
+
logger.info(f"[Retry] Exception {type(e).__name__} excluded from retry, re-raising")
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
# 检查是否是需要重试的异常
|
|
50
|
+
if not isinstance(e, exceptions_to_retry):
|
|
51
|
+
logger.info(f"[Retry] Exception {type(e).__name__} not in retry list, re-raising")
|
|
52
|
+
raise
|
|
53
|
+
|
|
54
|
+
# 判断是否需要继续重试
|
|
55
|
+
if retry_count >= max_retries:
|
|
56
|
+
logger.warning(f"[Retry] Max retries ({max_retries}) exceeded for {func.__name__}: {str(e)}")
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
# 计算退避时间
|
|
60
|
+
delay = _calculate_backoff(backoff, retry_count)
|
|
61
|
+
|
|
62
|
+
logger.info(
|
|
63
|
+
f"[Retry] Retrying {func.__name__} (attempt {retry_count}/{max_retries-1}), "
|
|
64
|
+
f"exception: {type(e).__name__}, delay: {delay:.2f}ms"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 等待
|
|
68
|
+
time.sleep(delay / 1000.0)
|
|
69
|
+
|
|
70
|
+
# 重试失败,尝试调用恢复方法
|
|
71
|
+
if last_exception and recover_method:
|
|
72
|
+
logger.info(f"[Retry] Calling recover method '{recover_method}' for {func.__name__}")
|
|
73
|
+
try:
|
|
74
|
+
# 尝试从实例中获取恢复方法
|
|
75
|
+
if args:
|
|
76
|
+
recover_func = getattr(args[0], recover_method, None)
|
|
77
|
+
else:
|
|
78
|
+
recover_func = None
|
|
79
|
+
|
|
80
|
+
if recover_func and callable(recover_func):
|
|
81
|
+
# 移除self参数(如果存在)
|
|
82
|
+
if args:
|
|
83
|
+
return recover_func(*args[1:], **kwargs)
|
|
84
|
+
else:
|
|
85
|
+
return recover_func(**kwargs)
|
|
86
|
+
except Exception as recover_e:
|
|
87
|
+
logger.error(f"[Retry] Recover method '{recover_method}' failed: {recover_e}")
|
|
88
|
+
|
|
89
|
+
# 所有重试都失败,抛出最后一个异常
|
|
90
|
+
if last_exception:
|
|
91
|
+
raise last_exception
|
|
92
|
+
|
|
93
|
+
return wrapper
|
|
94
|
+
return decorator
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _calculate_backoff(backoff, attempt: int) -> float:
|
|
98
|
+
"""
|
|
99
|
+
计算退避时间
|
|
100
|
+
|
|
101
|
+
参数:
|
|
102
|
+
backoff: 退避配置
|
|
103
|
+
attempt: 当前重试次数(从1开始)
|
|
104
|
+
|
|
105
|
+
返回:
|
|
106
|
+
退避时间(毫秒)
|
|
107
|
+
"""
|
|
108
|
+
# 指数退避:delay * (multiplier ^ (attempt - 1))
|
|
109
|
+
delay = backoff.delay * (backoff.multiplier ** (attempt - 1))
|
|
110
|
+
|
|
111
|
+
# 应用随机因子
|
|
112
|
+
if backoff.random_factor > 0:
|
|
113
|
+
random_delta = delay * backoff.random_factor
|
|
114
|
+
delay = delay + random.uniform(-random_delta, random_delta)
|
|
115
|
+
|
|
116
|
+
# 确保不超过最大延迟
|
|
117
|
+
delay = min(delay, backoff.max_delay)
|
|
118
|
+
|
|
119
|
+
# 确保不小于0
|
|
120
|
+
delay = max(delay, 0)
|
|
121
|
+
|
|
122
|
+
return delay
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def retry(
|
|
126
|
+
max_retries: int = 3,
|
|
127
|
+
delay: int = 1000,
|
|
128
|
+
max_delay: int = 10000,
|
|
129
|
+
multiplier: float = 2.0,
|
|
130
|
+
exceptions: Tuple[Type[Exception], ...] = (Exception,)
|
|
131
|
+
) -> Callable:
|
|
132
|
+
"""
|
|
133
|
+
便捷的重试装饰器
|
|
134
|
+
|
|
135
|
+
参数:
|
|
136
|
+
max_retries: 最大重试次数
|
|
137
|
+
delay: 初始延迟(毫秒)
|
|
138
|
+
max_delay: 最大延迟(毫秒)
|
|
139
|
+
multiplier: 延迟倍增因子
|
|
140
|
+
exceptions: 需要重试的异常类型
|
|
141
|
+
|
|
142
|
+
使用示例:
|
|
143
|
+
@retry(max_retries=5, delay=500)
|
|
144
|
+
def fetch_data():
|
|
145
|
+
pass
|
|
146
|
+
"""
|
|
147
|
+
from spring.retry.retry_annotations import Retryable, Backoff
|
|
148
|
+
|
|
149
|
+
annotation = Retryable(
|
|
150
|
+
value=exceptions,
|
|
151
|
+
max_retries=max_retries,
|
|
152
|
+
backoff=Backoff(delay=delay, max_delay=max_delay, multiplier=multiplier)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return retryable_decorator(annotation)
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
from typing import Dict, Any, Callable, Optional
|
|
2
|
+
import asyncio
|
|
3
|
+
import time
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Scheduler:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
# 存储任务信息: {task_id: {'task': asyncio.Task, 'loop': asyncio.AbstractEventLoop}}
|
|
11
|
+
self._tasks: Dict[str, dict] = {}
|
|
12
|
+
self._lock = threading.RLock()
|
|
13
|
+
self._logger = logging.getLogger("Spring.Scheduler")
|
|
14
|
+
|
|
15
|
+
def schedule(self, task_id: str, func: Callable, **kwargs) -> None:
|
|
16
|
+
fixed_rate = kwargs.get('fixed_rate')
|
|
17
|
+
fixed_delay = kwargs.get('fixed_delay')
|
|
18
|
+
cron = kwargs.get('cron')
|
|
19
|
+
initial_delay = kwargs.get('initial_delay', 0)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
loop = asyncio.get_event_loop()
|
|
23
|
+
if not loop.is_running():
|
|
24
|
+
# 如果有事件循环但未运行,创建新线程运行
|
|
25
|
+
def run_loop():
|
|
26
|
+
loop = asyncio.new_event_loop()
|
|
27
|
+
asyncio.set_event_loop(loop)
|
|
28
|
+
task = None
|
|
29
|
+
if fixed_rate:
|
|
30
|
+
task = loop.create_task(self._schedule_fixed_rate(task_id, func, fixed_rate, initial_delay))
|
|
31
|
+
elif fixed_delay:
|
|
32
|
+
task = loop.create_task(self._schedule_fixed_delay(task_id, func, fixed_delay, initial_delay))
|
|
33
|
+
elif cron:
|
|
34
|
+
task = loop.create_task(self._schedule_cron(task_id, func, cron, initial_delay))
|
|
35
|
+
|
|
36
|
+
if task:
|
|
37
|
+
with self._lock:
|
|
38
|
+
self._tasks[task_id] = {'task': task, 'loop': loop}
|
|
39
|
+
|
|
40
|
+
loop.run_forever()
|
|
41
|
+
|
|
42
|
+
thread = threading.Thread(target=run_loop, daemon=True)
|
|
43
|
+
thread.start()
|
|
44
|
+
return
|
|
45
|
+
except RuntimeError:
|
|
46
|
+
# 没有事件循环,创建新线程运行
|
|
47
|
+
def run_loop():
|
|
48
|
+
loop = asyncio.new_event_loop()
|
|
49
|
+
asyncio.set_event_loop(loop)
|
|
50
|
+
task = None
|
|
51
|
+
if fixed_rate:
|
|
52
|
+
task = loop.create_task(self._schedule_fixed_rate(task_id, func, fixed_rate, initial_delay))
|
|
53
|
+
elif fixed_delay:
|
|
54
|
+
task = loop.create_task(self._schedule_fixed_delay(task_id, func, fixed_delay, initial_delay))
|
|
55
|
+
elif cron:
|
|
56
|
+
task = loop.create_task(self._schedule_cron(task_id, func, cron, initial_delay))
|
|
57
|
+
|
|
58
|
+
if task:
|
|
59
|
+
with self._lock:
|
|
60
|
+
self._tasks[task_id] = {'task': task, 'loop': loop}
|
|
61
|
+
|
|
62
|
+
loop.run_forever()
|
|
63
|
+
|
|
64
|
+
thread = threading.Thread(target=run_loop, daemon=True)
|
|
65
|
+
thread.start()
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# 有运行中的事件循环
|
|
69
|
+
loop = asyncio.get_event_loop()
|
|
70
|
+
task = None
|
|
71
|
+
if fixed_rate:
|
|
72
|
+
task = asyncio.create_task(self._schedule_fixed_rate(task_id, func, fixed_rate, initial_delay))
|
|
73
|
+
elif fixed_delay:
|
|
74
|
+
task = asyncio.create_task(self._schedule_fixed_delay(task_id, func, fixed_delay, initial_delay))
|
|
75
|
+
elif cron:
|
|
76
|
+
task = asyncio.create_task(self._schedule_cron(task_id, func, cron, initial_delay))
|
|
77
|
+
else:
|
|
78
|
+
self._logger.warning(f"No scheduling type specified for task: {task_id}")
|
|
79
|
+
|
|
80
|
+
if task:
|
|
81
|
+
with self._lock:
|
|
82
|
+
self._tasks[task_id] = {'task': task, 'loop': loop}
|
|
83
|
+
|
|
84
|
+
async def _schedule_fixed_rate(self, task_id: str, func: Callable, rate_ms: int, initial_delay: int) -> None:
|
|
85
|
+
await asyncio.sleep(initial_delay / 1000)
|
|
86
|
+
|
|
87
|
+
while True:
|
|
88
|
+
# 检查任务是否已被取消
|
|
89
|
+
with self._lock:
|
|
90
|
+
task_info = self._tasks.get(task_id)
|
|
91
|
+
if task_info is None or task_info['task'].done():
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
start_time = time.time()
|
|
95
|
+
try:
|
|
96
|
+
if asyncio.iscoroutinefunction(func):
|
|
97
|
+
await func()
|
|
98
|
+
else:
|
|
99
|
+
func()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self._logger.error(f"Scheduled task {task_id} failed: {str(e)}")
|
|
102
|
+
|
|
103
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
104
|
+
sleep_time = max(0, rate_ms - elapsed_ms) / 1000
|
|
105
|
+
await asyncio.sleep(sleep_time)
|
|
106
|
+
|
|
107
|
+
async def _schedule_fixed_delay(self, task_id: str, func: Callable, delay_ms: int, initial_delay: int) -> None:
|
|
108
|
+
await asyncio.sleep(initial_delay / 1000)
|
|
109
|
+
|
|
110
|
+
while True:
|
|
111
|
+
# 检查任务是否已被取消
|
|
112
|
+
with self._lock:
|
|
113
|
+
task_info = self._tasks.get(task_id)
|
|
114
|
+
if task_info is None or task_info['task'].done():
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
if asyncio.iscoroutinefunction(func):
|
|
119
|
+
await func()
|
|
120
|
+
else:
|
|
121
|
+
func()
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self._logger.error(f"Scheduled task {task_id} failed: {str(e)}")
|
|
124
|
+
|
|
125
|
+
await asyncio.sleep(delay_ms / 1000)
|
|
126
|
+
|
|
127
|
+
async def _schedule_cron(self, task_id: str, func: Callable, cron_expr: str, initial_delay: int) -> None:
|
|
128
|
+
await asyncio.sleep(initial_delay / 1000)
|
|
129
|
+
|
|
130
|
+
while True:
|
|
131
|
+
# 检查任务是否已被取消
|
|
132
|
+
with self._lock:
|
|
133
|
+
task_info = self._tasks.get(task_id)
|
|
134
|
+
if task_info is None or task_info['task'].done():
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
if asyncio.iscoroutinefunction(func):
|
|
139
|
+
await func()
|
|
140
|
+
else:
|
|
141
|
+
func()
|
|
142
|
+
except Exception as e:
|
|
143
|
+
self._logger.error(f"Scheduled task {task_id} failed: {str(e)}")
|
|
144
|
+
|
|
145
|
+
await asyncio.sleep(self._parse_cron(cron_expr))
|
|
146
|
+
|
|
147
|
+
def _parse_cron(self, cron_expr: str) -> float:
|
|
148
|
+
import datetime
|
|
149
|
+
|
|
150
|
+
parts = cron_expr.split()
|
|
151
|
+
|
|
152
|
+
if len(parts) == 5:
|
|
153
|
+
second = '*'
|
|
154
|
+
minute, hour, day, month, weekday = parts
|
|
155
|
+
elif len(parts) == 6:
|
|
156
|
+
second, minute, hour, day, month, weekday = parts
|
|
157
|
+
else:
|
|
158
|
+
self._logger.warning(f"Invalid cron expression: {cron_expr}")
|
|
159
|
+
return 60.0
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
now = datetime.datetime.now()
|
|
163
|
+
next_run = now + datetime.timedelta(seconds=1)
|
|
164
|
+
|
|
165
|
+
# 解析所有字段的可能值
|
|
166
|
+
seconds = self._parse_field(second, 0, 59)
|
|
167
|
+
minutes = self._parse_field(minute, 0, 59)
|
|
168
|
+
hours = self._parse_field(hour, 0, 23)
|
|
169
|
+
days = self._parse_field(day, 1, 31)
|
|
170
|
+
months = self._parse_field(month, 1, 12)
|
|
171
|
+
|
|
172
|
+
# 获取当前月份的最大天数
|
|
173
|
+
def get_max_day(year, m):
|
|
174
|
+
if m == 2:
|
|
175
|
+
if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
|
|
176
|
+
return 29
|
|
177
|
+
return 28
|
|
178
|
+
if m in [4, 6, 9, 11]:
|
|
179
|
+
return 30
|
|
180
|
+
return 31
|
|
181
|
+
|
|
182
|
+
# 从当前时间开始查找下一个匹配时间
|
|
183
|
+
while True:
|
|
184
|
+
# 检查月份
|
|
185
|
+
if next_run.month not in months:
|
|
186
|
+
# 跳转到下一个匹配月份的第一天
|
|
187
|
+
next_month_found = False
|
|
188
|
+
for m in months:
|
|
189
|
+
if m > next_run.month:
|
|
190
|
+
next_run = next_run.replace(month=m, day=1, hour=0, minute=0, second=0)
|
|
191
|
+
next_month_found = True
|
|
192
|
+
break
|
|
193
|
+
if not next_month_found:
|
|
194
|
+
# 下一年的第一个匹配月份
|
|
195
|
+
if months:
|
|
196
|
+
next_run = next_run.replace(year=next_run.year + 1, month=months[0], day=1, hour=0, minute=0, second=0)
|
|
197
|
+
else:
|
|
198
|
+
return 60.0
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
# 检查日期
|
|
202
|
+
max_day = get_max_day(next_run.year, next_run.month)
|
|
203
|
+
valid_days = [d for d in days if d <= max_day]
|
|
204
|
+
|
|
205
|
+
if next_run.day not in valid_days:
|
|
206
|
+
# 跳转到下一个匹配日期
|
|
207
|
+
next_day_found = False
|
|
208
|
+
for d in valid_days:
|
|
209
|
+
if d > next_run.day:
|
|
210
|
+
next_run = next_run.replace(day=d, hour=0, minute=0, second=0)
|
|
211
|
+
next_day_found = True
|
|
212
|
+
break
|
|
213
|
+
if not next_day_found:
|
|
214
|
+
# 跳转到下一个月
|
|
215
|
+
next_month_found = False
|
|
216
|
+
for m in months:
|
|
217
|
+
if m > next_run.month:
|
|
218
|
+
next_run = next_run.replace(month=m, day=1, hour=0, minute=0, second=0)
|
|
219
|
+
next_month_found = True
|
|
220
|
+
break
|
|
221
|
+
if not next_month_found:
|
|
222
|
+
if months:
|
|
223
|
+
next_run = next_run.replace(year=next_run.year + 1, month=months[0], day=1, hour=0, minute=0, second=0)
|
|
224
|
+
else:
|
|
225
|
+
return 60.0
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
# 检查星期
|
|
229
|
+
if weekday != '*' and weekday != '?':
|
|
230
|
+
if not self._matches_weekday(next_run.weekday(), weekday):
|
|
231
|
+
# 跳到下一天
|
|
232
|
+
next_run = next_run + datetime.timedelta(days=1)
|
|
233
|
+
next_run = next_run.replace(hour=0, minute=0, second=0)
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
# 检查小时
|
|
237
|
+
if next_run.hour not in hours:
|
|
238
|
+
# 跳转到下一个匹配小时
|
|
239
|
+
next_hour_found = False
|
|
240
|
+
for h in hours:
|
|
241
|
+
if h > next_run.hour:
|
|
242
|
+
next_run = next_run.replace(hour=h, minute=0, second=0)
|
|
243
|
+
next_hour_found = True
|
|
244
|
+
break
|
|
245
|
+
if not next_hour_found:
|
|
246
|
+
# 跳转到下一天
|
|
247
|
+
next_run = next_run + datetime.timedelta(days=1)
|
|
248
|
+
next_run = next_run.replace(hour=0, minute=0, second=0)
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
# 检查分钟
|
|
252
|
+
if next_run.minute not in minutes:
|
|
253
|
+
# 跳转到下一个匹配分钟
|
|
254
|
+
next_minute_found = False
|
|
255
|
+
for m in minutes:
|
|
256
|
+
if m > next_run.minute:
|
|
257
|
+
next_run = next_run.replace(minute=m, second=0)
|
|
258
|
+
next_minute_found = True
|
|
259
|
+
break
|
|
260
|
+
if not next_minute_found:
|
|
261
|
+
# 跳转到下一小时
|
|
262
|
+
next_run = next_run + datetime.timedelta(hours=1)
|
|
263
|
+
next_run = next_run.replace(minute=0, second=0)
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
# 检查秒
|
|
267
|
+
if next_run.second in seconds:
|
|
268
|
+
# 找到匹配时间
|
|
269
|
+
delta = (next_run - now).total_seconds()
|
|
270
|
+
return max(0, delta)
|
|
271
|
+
|
|
272
|
+
# 跳转到下一个匹配秒
|
|
273
|
+
next_second_found = False
|
|
274
|
+
for s in seconds:
|
|
275
|
+
if s > next_run.second:
|
|
276
|
+
next_run = next_run.replace(second=s)
|
|
277
|
+
next_second_found = True
|
|
278
|
+
break
|
|
279
|
+
if not next_second_found:
|
|
280
|
+
# 跳转到下一分钟
|
|
281
|
+
next_run = next_run + datetime.timedelta(minutes=1)
|
|
282
|
+
next_run = next_run.replace(second=0)
|
|
283
|
+
|
|
284
|
+
# 防止无限循环
|
|
285
|
+
if (next_run - now).total_seconds() > 365 * 24 * 3600:
|
|
286
|
+
return 60.0
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
self._logger.error(f"Failed to parse cron expression '{cron_expr}': {str(e)}")
|
|
290
|
+
return 60.0
|
|
291
|
+
|
|
292
|
+
def _parse_field(self, expr: str, min_val: int, max_val: int) -> list:
|
|
293
|
+
"""解析 cron 字段表达式,返回所有可能的取值"""
|
|
294
|
+
if expr == '*' or expr == '?':
|
|
295
|
+
return list(range(min_val, max_val + 1))
|
|
296
|
+
|
|
297
|
+
result = []
|
|
298
|
+
for part in expr.split(','):
|
|
299
|
+
if '/' in part:
|
|
300
|
+
# 处理 step 表达式,如 */5 或 0/5
|
|
301
|
+
step_parts = part.split('/')
|
|
302
|
+
if len(step_parts) == 2:
|
|
303
|
+
start_str, step_str = step_parts
|
|
304
|
+
start = int(start_str) if start_str != '*' else min_val
|
|
305
|
+
step = int(step_str)
|
|
306
|
+
for val in range(start, max_val + 1, step):
|
|
307
|
+
if val >= min_val:
|
|
308
|
+
result.append(val)
|
|
309
|
+
elif '-' in part:
|
|
310
|
+
# 处理范围表达式,如 1-5
|
|
311
|
+
range_parts = part.split('-')
|
|
312
|
+
if len(range_parts) == 2:
|
|
313
|
+
start = int(range_parts[0])
|
|
314
|
+
end = int(range_parts[1])
|
|
315
|
+
for val in range(start, end + 1):
|
|
316
|
+
if min_val <= val <= max_val:
|
|
317
|
+
result.append(val)
|
|
318
|
+
elif part.isdigit():
|
|
319
|
+
# 处理单个值
|
|
320
|
+
val = int(part)
|
|
321
|
+
if min_val <= val <= max_val:
|
|
322
|
+
result.append(val)
|
|
323
|
+
|
|
324
|
+
return sorted(set(result))
|
|
325
|
+
|
|
326
|
+
def _matches_field(self, value: int, expr: str, min_val: int, max_val: int) -> bool:
|
|
327
|
+
if expr == '*' or expr == '?':
|
|
328
|
+
return True
|
|
329
|
+
|
|
330
|
+
# 处理 */5 或 0/5 格式(从起始值开始每隔step执行)
|
|
331
|
+
if '/' in expr:
|
|
332
|
+
parts = expr.split('/')
|
|
333
|
+
if len(parts) == 2:
|
|
334
|
+
start = int(parts[0]) if parts[0] != '*' else min_val
|
|
335
|
+
step = int(parts[1])
|
|
336
|
+
return (value - start) % step == 0 and value >= start
|
|
337
|
+
|
|
338
|
+
if expr.isdigit():
|
|
339
|
+
return value == int(expr)
|
|
340
|
+
|
|
341
|
+
if '-' in expr:
|
|
342
|
+
parts = expr.split('-')
|
|
343
|
+
if len(parts) == 2:
|
|
344
|
+
return min_val <= int(parts[0]) <= value <= int(parts[1]) <= max_val
|
|
345
|
+
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
def _matches_weekday(self, value: int, expr: str) -> bool:
|
|
349
|
+
if expr == '*' or expr == '?':
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
if expr.isdigit():
|
|
353
|
+
day = int(expr)
|
|
354
|
+
if day == 7:
|
|
355
|
+
day = 0
|
|
356
|
+
return value == day
|
|
357
|
+
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
def stop(self, task_id: str) -> None:
|
|
361
|
+
with self._lock:
|
|
362
|
+
if task_id not in self._tasks:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
task_info = self._tasks[task_id]
|
|
366
|
+
task = task_info['task']
|
|
367
|
+
loop = task_info['loop']
|
|
368
|
+
|
|
369
|
+
if not task.done():
|
|
370
|
+
try:
|
|
371
|
+
# 使用 call_soon_threadsafe 安全地跨线程取消任务
|
|
372
|
+
if loop.is_running():
|
|
373
|
+
loop.call_soon_threadsafe(task.cancel)
|
|
374
|
+
else:
|
|
375
|
+
task.cancel()
|
|
376
|
+
except Exception as e:
|
|
377
|
+
self._logger.error(f"Failed to cancel task {task_id}: {str(e)}")
|
|
378
|
+
|
|
379
|
+
del self._tasks[task_id]
|
|
380
|
+
self._logger.info(f"Scheduled task {task_id} stopped")
|
|
381
|
+
|
|
382
|
+
def stop_all(self) -> None:
|
|
383
|
+
with self._lock:
|
|
384
|
+
task_ids = list(self._tasks.keys())
|
|
385
|
+
|
|
386
|
+
for task_id in task_ids:
|
|
387
|
+
self.stop(task_id)
|
|
388
|
+
|
|
389
|
+
self._logger.info(f"All {len(task_ids)} scheduled tasks stopped")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spring Security 模块
|
|
3
|
+
提供认证授权、JWT支持、密钥管理、重放防护等企业级安全功能
|
|
4
|
+
"""
|
|
5
|
+
from .security_context import SecurityContext, SecurityContextHolder
|
|
6
|
+
from .security_aop import (
|
|
7
|
+
pre_authorize_decorator,
|
|
8
|
+
secured_decorator,
|
|
9
|
+
authenticate_decorator,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# 可选导入:JWT工具(需要pyjwt)
|
|
13
|
+
try:
|
|
14
|
+
from .jwt_utils import JwtUtils
|
|
15
|
+
except ImportError:
|
|
16
|
+
JwtUtils = None
|
|
17
|
+
|
|
18
|
+
# 密钥管理器
|
|
19
|
+
from .secret_manager import SecretManager, is_sensitive_key, mask_secret, resolve_secret_config
|
|
20
|
+
|
|
21
|
+
# 重放攻击防护
|
|
22
|
+
from .replay_protection import ReplayProtection, NonceCache, RedisNonceCache, create_replay_protection
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'JwtUtils',
|
|
26
|
+
'SecurityContext',
|
|
27
|
+
'SecurityContextHolder',
|
|
28
|
+
'pre_authorize_decorator',
|
|
29
|
+
'secured_decorator',
|
|
30
|
+
'authenticate_decorator',
|
|
31
|
+
'SecretManager',
|
|
32
|
+
'is_sensitive_key',
|
|
33
|
+
'mask_secret',
|
|
34
|
+
'resolve_secret_config',
|
|
35
|
+
'ReplayProtection',
|
|
36
|
+
'NonceCache',
|
|
37
|
+
'RedisNonceCache',
|
|
38
|
+
'create_replay_protection',
|
|
39
|
+
]
|