fiuai-sdk-python 0.7.5__tar.gz → 0.8.0__tar.gz
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.
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/PKG-INFO +1 -1
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/pyproject.toml +1 -1
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/__init__.py +1 -1
- fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/__init__.py +30 -0
- fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/cache_client.py +233 -0
- fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/circuit_breaker.py +78 -0
- fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/decorator.py +105 -0
- fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/redis_manager.py +152 -0
- fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/types.py +58 -0
- fiuai_sdk_python-0.7.5/src/fiuai_sdk_python/pkg/cache/__init__.py +0 -6
- fiuai_sdk_python-0.7.5/src/fiuai_sdk_python/pkg/cache/redis_manager.py +0 -188
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/.gitignore +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/CHANGELOG.md +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/LICENSE +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/README.md +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/__init__.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/context_mgr.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/header.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/helper.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/type.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/bank.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/client.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/company.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/config.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/const.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/context.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/datatype.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/doctype.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/error.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/examples/fastapi_integration.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/http/__init__.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/http/client.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/item.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/perm.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/profile.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/resp.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/setup.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/type.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/util.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/utils/__init__.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/utils/ids.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/utils/logger.py +0 -0
- {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/utils/text.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fiuai_sdk_python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: FiuAI Python SDK - 企业级AI服务集成开发工具包
|
|
5
5
|
Project-URL: Homepage, https://github.com/fiuai/fiuai-sdk-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/fiuai/fiuai-sdk-python#readme
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: fiuai-sdk-python
|
|
3
|
+
# Created Date: 2026-03-09
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Agent: Cursor
|
|
6
|
+
# Email: lmlala@aliyun.com
|
|
7
|
+
# Copyright (c) 2025 FiuAI
|
|
8
|
+
|
|
9
|
+
from .cache_client import CacheClient
|
|
10
|
+
from .circuit_breaker import CircuitBreaker
|
|
11
|
+
from .decorator import cached, get_default_client, init_cache
|
|
12
|
+
from .redis_manager import RedisDBConfig, redis_manager
|
|
13
|
+
from .types import CacheConfig, CircuitBreakerConfig, CircuitState
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
# 连接池 (已有)
|
|
17
|
+
"redis_manager",
|
|
18
|
+
"RedisDBConfig",
|
|
19
|
+
# 缓存客户端
|
|
20
|
+
"CacheClient",
|
|
21
|
+
"CacheConfig",
|
|
22
|
+
# 装饰器
|
|
23
|
+
"cached",
|
|
24
|
+
"init_cache",
|
|
25
|
+
"get_default_client",
|
|
26
|
+
# 熔断
|
|
27
|
+
"CircuitBreaker",
|
|
28
|
+
"CircuitBreakerConfig",
|
|
29
|
+
"CircuitState",
|
|
30
|
+
]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: fiuai-sdk-python
|
|
3
|
+
# Created Date: 2026-03-09
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Agent: Cursor
|
|
6
|
+
# Email: lmlala@aliyun.com
|
|
7
|
+
# Copyright (c) 2025 FiuAI
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Awaitable, Callable, Optional, TypeVar
|
|
11
|
+
|
|
12
|
+
from redis import Redis as SyncRedis
|
|
13
|
+
from redis.asyncio import Redis as AsyncRedis
|
|
14
|
+
|
|
15
|
+
from ...utils.logger import get_logger
|
|
16
|
+
from .circuit_breaker import CircuitBreaker
|
|
17
|
+
from .redis_manager import redis_manager
|
|
18
|
+
from .types import CacheConfig
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CacheClient:
|
|
25
|
+
"""通用缓存客户端
|
|
26
|
+
|
|
27
|
+
能力: KV / Hash / TTL / get_or_load (cache-aside) / 熔断降级
|
|
28
|
+
不含业务语义 (无 tenant/company 前缀), 由项目层包装。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: CacheConfig):
|
|
32
|
+
self._config = config
|
|
33
|
+
self._breaker = CircuitBreaker(config.circuit_breaker)
|
|
34
|
+
|
|
35
|
+
def _full_key(self, key: str) -> str:
|
|
36
|
+
if self._config.key_prefix:
|
|
37
|
+
return f"{self._config.key_prefix}:{key}"
|
|
38
|
+
return key
|
|
39
|
+
|
|
40
|
+
def _get_async_client(self) -> AsyncRedis:
|
|
41
|
+
return redis_manager.get_async_client(self._config.redis_db_name)
|
|
42
|
+
|
|
43
|
+
def _get_sync_client(self) -> SyncRedis:
|
|
44
|
+
return redis_manager.get_sync_client(self._config.redis_db_name)
|
|
45
|
+
|
|
46
|
+
def _effective_ttl(self, ttl: Optional[int]) -> Optional[int]:
|
|
47
|
+
return ttl if ttl is not None else self._config.default_ttl
|
|
48
|
+
|
|
49
|
+
# ── KV async ──────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
async def get(self, key: str) -> Optional[str]:
|
|
52
|
+
if not self._breaker.allow_request:
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
client = self._get_async_client()
|
|
56
|
+
val = await client.get(self._full_key(key))
|
|
57
|
+
self._breaker.record_success()
|
|
58
|
+
return val
|
|
59
|
+
except Exception as e:
|
|
60
|
+
self._breaker.record_failure()
|
|
61
|
+
logger.warning(f"cache get failed: key={key}, {e}")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
async def set(self, key: str, value: str, ttl: Optional[int] = None) -> bool:
|
|
65
|
+
if not self._breaker.allow_request:
|
|
66
|
+
return False
|
|
67
|
+
try:
|
|
68
|
+
client = self._get_async_client()
|
|
69
|
+
effective_ttl = self._effective_ttl(ttl)
|
|
70
|
+
if effective_ttl:
|
|
71
|
+
await client.setex(self._full_key(key), effective_ttl, value)
|
|
72
|
+
else:
|
|
73
|
+
await client.set(self._full_key(key), value)
|
|
74
|
+
self._breaker.record_success()
|
|
75
|
+
return True
|
|
76
|
+
except Exception as e:
|
|
77
|
+
self._breaker.record_failure()
|
|
78
|
+
logger.warning(f"cache set failed: key={key}, {e}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
async def delete(self, key: str) -> bool:
|
|
82
|
+
if not self._breaker.allow_request:
|
|
83
|
+
return False
|
|
84
|
+
try:
|
|
85
|
+
client = self._get_async_client()
|
|
86
|
+
await client.delete(self._full_key(key))
|
|
87
|
+
self._breaker.record_success()
|
|
88
|
+
return True
|
|
89
|
+
except Exception as e:
|
|
90
|
+
self._breaker.record_failure()
|
|
91
|
+
logger.warning(f"cache delete failed: key={key}, {e}")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
async def exists(self, key: str) -> bool:
|
|
95
|
+
if not self._breaker.allow_request:
|
|
96
|
+
return False
|
|
97
|
+
try:
|
|
98
|
+
client = self._get_async_client()
|
|
99
|
+
result = await client.exists(self._full_key(key))
|
|
100
|
+
self._breaker.record_success()
|
|
101
|
+
return bool(result)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
self._breaker.record_failure()
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
# ── Hash async ────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
async def hset(self, key: str, field: str, value: str) -> bool:
|
|
109
|
+
if not self._breaker.allow_request:
|
|
110
|
+
return False
|
|
111
|
+
try:
|
|
112
|
+
client = self._get_async_client()
|
|
113
|
+
await client.hset(self._full_key(key), field, value)
|
|
114
|
+
self._breaker.record_success()
|
|
115
|
+
return True
|
|
116
|
+
except Exception as e:
|
|
117
|
+
self._breaker.record_failure()
|
|
118
|
+
logger.warning(f"cache hset failed: key={key}, field={field}, {e}")
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
async def hget(self, key: str, field: str) -> Optional[str]:
|
|
122
|
+
if not self._breaker.allow_request:
|
|
123
|
+
return None
|
|
124
|
+
try:
|
|
125
|
+
client = self._get_async_client()
|
|
126
|
+
val = await client.hget(self._full_key(key), field)
|
|
127
|
+
self._breaker.record_success()
|
|
128
|
+
return val
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self._breaker.record_failure()
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
async def hgetall(self, key: str) -> dict:
|
|
134
|
+
if not self._breaker.allow_request:
|
|
135
|
+
return {}
|
|
136
|
+
try:
|
|
137
|
+
client = self._get_async_client()
|
|
138
|
+
val = await client.hgetall(self._full_key(key))
|
|
139
|
+
self._breaker.record_success()
|
|
140
|
+
return val or {}
|
|
141
|
+
except Exception as e:
|
|
142
|
+
self._breaker.record_failure()
|
|
143
|
+
return {}
|
|
144
|
+
|
|
145
|
+
# ── cache-aside async ─────────────────────────────────
|
|
146
|
+
|
|
147
|
+
async def get_or_load(
|
|
148
|
+
self,
|
|
149
|
+
key: str,
|
|
150
|
+
loader: Callable[[], Awaitable[T]],
|
|
151
|
+
ttl: Optional[int] = None,
|
|
152
|
+
serializer: Callable[[T], str] = json.dumps,
|
|
153
|
+
deserializer: Callable[[str], T] = json.loads,
|
|
154
|
+
) -> T:
|
|
155
|
+
"""Cache-aside: 命中返回缓存, miss 或熔断时调 loader。
|
|
156
|
+
|
|
157
|
+
loader 返回值写回 cache (best-effort)。
|
|
158
|
+
熔断时不报错, 直接走 loader — 保证业务不中断。
|
|
159
|
+
"""
|
|
160
|
+
if self._breaker.allow_request:
|
|
161
|
+
try:
|
|
162
|
+
client = self._get_async_client()
|
|
163
|
+
cached = await client.get(self._full_key(key))
|
|
164
|
+
if cached is not None:
|
|
165
|
+
self._breaker.record_success()
|
|
166
|
+
return deserializer(cached)
|
|
167
|
+
self._breaker.record_success()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
self._breaker.record_failure()
|
|
170
|
+
logger.warning(f"cache get_or_load read failed: key={key}, {e}")
|
|
171
|
+
|
|
172
|
+
value = await loader()
|
|
173
|
+
await self.set(key, serializer(value), ttl)
|
|
174
|
+
return value
|
|
175
|
+
|
|
176
|
+
# ── KV sync ───────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
def get_sync(self, key: str) -> Optional[str]:
|
|
179
|
+
if not self._breaker.allow_request:
|
|
180
|
+
return None
|
|
181
|
+
try:
|
|
182
|
+
client = self._get_sync_client()
|
|
183
|
+
val = client.get(self._full_key(key))
|
|
184
|
+
self._breaker.record_success()
|
|
185
|
+
return val
|
|
186
|
+
except Exception as e:
|
|
187
|
+
self._breaker.record_failure()
|
|
188
|
+
logger.warning(f"cache get_sync failed: key={key}, {e}")
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
def set_sync(self, key: str, value: str, ttl: Optional[int] = None) -> bool:
|
|
192
|
+
if not self._breaker.allow_request:
|
|
193
|
+
return False
|
|
194
|
+
try:
|
|
195
|
+
client = self._get_sync_client()
|
|
196
|
+
effective_ttl = self._effective_ttl(ttl)
|
|
197
|
+
if effective_ttl:
|
|
198
|
+
client.setex(self._full_key(key), effective_ttl, value)
|
|
199
|
+
else:
|
|
200
|
+
client.set(self._full_key(key), value)
|
|
201
|
+
self._breaker.record_success()
|
|
202
|
+
return True
|
|
203
|
+
except Exception as e:
|
|
204
|
+
self._breaker.record_failure()
|
|
205
|
+
logger.warning(f"cache set_sync failed: key={key}, {e}")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
# ── cache-aside sync ──────────────────────────────────
|
|
209
|
+
|
|
210
|
+
def get_or_load_sync(
|
|
211
|
+
self,
|
|
212
|
+
key: str,
|
|
213
|
+
loader: Callable[[], T],
|
|
214
|
+
ttl: Optional[int] = None,
|
|
215
|
+
serializer: Callable[[T], str] = json.dumps,
|
|
216
|
+
deserializer: Callable[[str], T] = json.loads,
|
|
217
|
+
) -> T:
|
|
218
|
+
"""同步版 cache-aside"""
|
|
219
|
+
if self._breaker.allow_request:
|
|
220
|
+
try:
|
|
221
|
+
client = self._get_sync_client()
|
|
222
|
+
cached = client.get(self._full_key(key))
|
|
223
|
+
if cached is not None:
|
|
224
|
+
self._breaker.record_success()
|
|
225
|
+
return deserializer(cached)
|
|
226
|
+
self._breaker.record_success()
|
|
227
|
+
except Exception as e:
|
|
228
|
+
self._breaker.record_failure()
|
|
229
|
+
logger.warning(f"cache get_or_load_sync read failed: key={key}, {e}")
|
|
230
|
+
|
|
231
|
+
value = loader()
|
|
232
|
+
self.set_sync(key, serializer(value), ttl)
|
|
233
|
+
return value
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: fiuai-sdk-python
|
|
3
|
+
# Created Date: 2026-03-09
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Agent: Cursor
|
|
6
|
+
# Email: lmlala@aliyun.com
|
|
7
|
+
# Copyright (c) 2025 FiuAI
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from ...utils.logger import get_logger
|
|
13
|
+
from .types import CircuitBreakerConfig, CircuitState
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CircuitBreaker:
|
|
19
|
+
"""三态熔断器 (CLOSED -> OPEN -> HALF_OPEN -> CLOSED)
|
|
20
|
+
|
|
21
|
+
CLOSED: 正常, 记录连续失败
|
|
22
|
+
OPEN: 熔断, 所有调用直接跳过 Redis, 走 fallback
|
|
23
|
+
HALF_OPEN: 冷却期过后, 允许少量探测请求
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: CircuitBreakerConfig):
|
|
27
|
+
self._config = config
|
|
28
|
+
self._state = CircuitState.CLOSED
|
|
29
|
+
self._failure_count = 0
|
|
30
|
+
self._last_failure_time: float = 0
|
|
31
|
+
self._half_open_calls = 0
|
|
32
|
+
self._lock = threading.Lock()
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def state(self) -> CircuitState:
|
|
36
|
+
with self._lock:
|
|
37
|
+
if self._state == CircuitState.OPEN:
|
|
38
|
+
elapsed = time.monotonic() - self._last_failure_time
|
|
39
|
+
if elapsed >= self._config.recovery_timeout:
|
|
40
|
+
self._state = CircuitState.HALF_OPEN
|
|
41
|
+
self._half_open_calls = 0
|
|
42
|
+
logger.info("circuit breaker: OPEN -> HALF_OPEN")
|
|
43
|
+
return self._state
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def allow_request(self) -> bool:
|
|
47
|
+
"""当前是否允许请求 Redis"""
|
|
48
|
+
current_state = self.state
|
|
49
|
+
if current_state == CircuitState.CLOSED:
|
|
50
|
+
return True
|
|
51
|
+
if current_state == CircuitState.HALF_OPEN:
|
|
52
|
+
with self._lock:
|
|
53
|
+
if self._half_open_calls < self._config.half_open_max_calls:
|
|
54
|
+
self._half_open_calls += 1
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def record_success(self):
|
|
60
|
+
with self._lock:
|
|
61
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
62
|
+
logger.info("circuit breaker: HALF_OPEN -> CLOSED")
|
|
63
|
+
self._state = CircuitState.CLOSED
|
|
64
|
+
self._failure_count = 0
|
|
65
|
+
|
|
66
|
+
def record_failure(self):
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._failure_count += 1
|
|
69
|
+
self._last_failure_time = time.monotonic()
|
|
70
|
+
if self._state == CircuitState.HALF_OPEN:
|
|
71
|
+
self._state = CircuitState.OPEN
|
|
72
|
+
logger.warning("circuit breaker: HALF_OPEN -> OPEN (probe failed)")
|
|
73
|
+
elif self._failure_count >= self._config.failure_threshold:
|
|
74
|
+
self._state = CircuitState.OPEN
|
|
75
|
+
logger.warning(
|
|
76
|
+
f"circuit breaker: CLOSED -> OPEN "
|
|
77
|
+
f"(failures={self._failure_count})"
|
|
78
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: fiuai-sdk-python
|
|
3
|
+
# Created Date: 2026-03-09
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Agent: Cursor
|
|
6
|
+
# Email: lmlala@aliyun.com
|
|
7
|
+
# Copyright (c) 2025 FiuAI
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import functools
|
|
11
|
+
from typing import Callable, Optional, Union
|
|
12
|
+
|
|
13
|
+
from ...utils.logger import get_logger
|
|
14
|
+
from .cache_client import CacheClient
|
|
15
|
+
from .types import CacheConfig
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
_default_client: Optional[CacheClient] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def init_cache(config: CacheConfig) -> CacheClient:
|
|
23
|
+
"""初始化模块级默认 CacheClient (供 @cached 装饰器使用)
|
|
24
|
+
|
|
25
|
+
项目启动时调用一次:
|
|
26
|
+
from fiuai_sdk_python.pkg.cache import init_cache, CacheConfig
|
|
27
|
+
init_cache(CacheConfig(redis_db_name="default", default_ttl=120, key_prefix="finnexus"))
|
|
28
|
+
"""
|
|
29
|
+
global _default_client
|
|
30
|
+
_default_client = CacheClient(config)
|
|
31
|
+
return _default_client
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_default_client() -> Optional[CacheClient]:
|
|
35
|
+
return _default_client
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cached(
|
|
39
|
+
key: Union[str, Callable[..., str]],
|
|
40
|
+
ttl: Optional[int] = None,
|
|
41
|
+
prefix: str = "",
|
|
42
|
+
client: Optional[CacheClient] = None,
|
|
43
|
+
):
|
|
44
|
+
"""函数级缓存装饰器 (cache-aside)
|
|
45
|
+
|
|
46
|
+
支持 sync 和 async 函数。
|
|
47
|
+
key 可以是固定字符串, 也可以是根据函数参数动态生成 key 的 callable。
|
|
48
|
+
|
|
49
|
+
用法:
|
|
50
|
+
@cached(key=lambda company_id, side: f"config:{company_id}:{side}", ttl=120)
|
|
51
|
+
def get_company_config(company_id: str, side: str) -> Config:
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@cached(key="global_settings", ttl=300)
|
|
55
|
+
async def get_settings() -> Settings:
|
|
56
|
+
...
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def decorator(fn: Callable) -> Callable:
|
|
60
|
+
@functools.wraps(fn)
|
|
61
|
+
async def async_wrapper(*args, **kwargs):
|
|
62
|
+
cache = client or _default_client
|
|
63
|
+
if cache is None:
|
|
64
|
+
return await fn(*args, **kwargs)
|
|
65
|
+
|
|
66
|
+
cache_key = _resolve_key(key, prefix, args, kwargs)
|
|
67
|
+
return await cache.get_or_load(
|
|
68
|
+
key=cache_key,
|
|
69
|
+
loader=lambda: fn(*args, **kwargs),
|
|
70
|
+
ttl=ttl,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@functools.wraps(fn)
|
|
74
|
+
def sync_wrapper(*args, **kwargs):
|
|
75
|
+
cache = client or _default_client
|
|
76
|
+
if cache is None:
|
|
77
|
+
return fn(*args, **kwargs)
|
|
78
|
+
|
|
79
|
+
cache_key = _resolve_key(key, prefix, args, kwargs)
|
|
80
|
+
return cache.get_or_load_sync(
|
|
81
|
+
key=cache_key,
|
|
82
|
+
loader=lambda: fn(*args, **kwargs),
|
|
83
|
+
ttl=ttl,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if asyncio.iscoroutinefunction(fn):
|
|
87
|
+
return async_wrapper
|
|
88
|
+
return sync_wrapper
|
|
89
|
+
|
|
90
|
+
return decorator
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _resolve_key(
|
|
94
|
+
key: Union[str, Callable],
|
|
95
|
+
prefix: str,
|
|
96
|
+
args: tuple,
|
|
97
|
+
kwargs: dict,
|
|
98
|
+
) -> str:
|
|
99
|
+
if callable(key) and not isinstance(key, str):
|
|
100
|
+
raw = key(*args, **kwargs)
|
|
101
|
+
else:
|
|
102
|
+
raw = str(key)
|
|
103
|
+
if prefix:
|
|
104
|
+
return f"{prefix}:{raw}"
|
|
105
|
+
return raw
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: fiuai-sdk-python
|
|
3
|
+
# Created Date: 2024-03-21
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Agent: Cursor
|
|
6
|
+
# Email: lmlala@aliyun.com
|
|
7
|
+
# Copyright (c) 2025 FiuAI
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Set
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
from redis import ConnectionPool as SyncConnectionPool
|
|
13
|
+
from redis import Redis as SyncRedis
|
|
14
|
+
from redis.asyncio import ConnectionPool as AsyncConnectionPool
|
|
15
|
+
from redis.asyncio import Redis as AsyncRedis
|
|
16
|
+
|
|
17
|
+
from ...utils.logger import get_logger
|
|
18
|
+
from ...utils.text import safe_str
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RedisDBConfig(BaseModel):
|
|
24
|
+
name: str = Field(description="连接名称")
|
|
25
|
+
host: str = Field(description="Redis主机")
|
|
26
|
+
port: int = Field(description="Redis端口")
|
|
27
|
+
password: str = Field(description="Redis密码")
|
|
28
|
+
db: int = Field(description="Redis数据库编号")
|
|
29
|
+
pool_size: int = Field(description="Redis连接池大小", default=20)
|
|
30
|
+
ttl: int = Field(description="Redis TTL", default=86400)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RedisManager:
|
|
34
|
+
"""Redis 连接池管理器 (单例)
|
|
35
|
+
|
|
36
|
+
管理多个 Redis 连接池, 每个连接池对应不同的数据库。
|
|
37
|
+
支持同步和异步两种模式, 支持增量注册新连接池。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_instance = None
|
|
41
|
+
_async_pools: Dict[str, AsyncConnectionPool] = {}
|
|
42
|
+
_sync_pools: Dict[str, SyncConnectionPool] = {}
|
|
43
|
+
_registered_names: Set[str] = set()
|
|
44
|
+
|
|
45
|
+
def __new__(cls):
|
|
46
|
+
if cls._instance is None:
|
|
47
|
+
cls._instance = super().__new__(cls)
|
|
48
|
+
return cls._instance
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
if not hasattr(self, "_inited"):
|
|
52
|
+
self._async_pools = {}
|
|
53
|
+
self._sync_pools = {}
|
|
54
|
+
self._registered_names = set()
|
|
55
|
+
self._inited = True
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_initialized(self) -> bool:
|
|
59
|
+
return len(self._registered_names) > 0
|
|
60
|
+
|
|
61
|
+
async def initialize(
|
|
62
|
+
self,
|
|
63
|
+
async_dbs: List[RedisDBConfig] | None = None,
|
|
64
|
+
sync_dbs: List[RedisDBConfig] | None = None,
|
|
65
|
+
):
|
|
66
|
+
"""初始化 Redis 连接池 (支持增量注册: 已存在的 pool 跳过, 新的追加)"""
|
|
67
|
+
for db in async_dbs or []:
|
|
68
|
+
if db.name not in self._registered_names:
|
|
69
|
+
await self.get_async_pool(db)
|
|
70
|
+
self._registered_names.add(db.name)
|
|
71
|
+
logger.info(f"registered async pool: {db.name}")
|
|
72
|
+
|
|
73
|
+
for db in sync_dbs or []:
|
|
74
|
+
if db.name not in self._registered_names:
|
|
75
|
+
self.get_sync_pool(db)
|
|
76
|
+
self._registered_names.add(db.name)
|
|
77
|
+
logger.info(f"registered sync pool: {db.name}")
|
|
78
|
+
|
|
79
|
+
async def get_async_pool(self, db: RedisDBConfig) -> AsyncConnectionPool:
|
|
80
|
+
"""获取指定数据库的异步连接池, 不存在则创建"""
|
|
81
|
+
if db.name not in self._async_pools:
|
|
82
|
+
self._async_pools[db.name] = AsyncConnectionPool.from_url(
|
|
83
|
+
f"redis://:{safe_str(db.password)}@{db.host}:{db.port}/{db.db}",
|
|
84
|
+
db=db.db,
|
|
85
|
+
decode_responses=True,
|
|
86
|
+
max_connections=db.pool_size,
|
|
87
|
+
)
|
|
88
|
+
return self._async_pools[db.name]
|
|
89
|
+
|
|
90
|
+
def get_sync_pool(self, db: RedisDBConfig) -> SyncConnectionPool:
|
|
91
|
+
"""获取指定数据库的同步连接池, 不存在则创建 (纯连接池, 不执行 LangGraph setup)"""
|
|
92
|
+
if db.name not in self._sync_pools:
|
|
93
|
+
self._sync_pools[db.name] = SyncConnectionPool.from_url(
|
|
94
|
+
f"redis://:{safe_str(db.password)}@{db.host}:{db.port}/{db.db}",
|
|
95
|
+
db=db.db,
|
|
96
|
+
decode_responses=True,
|
|
97
|
+
max_connections=db.pool_size,
|
|
98
|
+
)
|
|
99
|
+
return self._sync_pools[db.name]
|
|
100
|
+
|
|
101
|
+
def get_async_client(self, db_name: str) -> AsyncRedis:
|
|
102
|
+
"""获取指定数据库的异步 Redis 客户端"""
|
|
103
|
+
if db_name not in self._async_pools:
|
|
104
|
+
raise RuntimeError(f"async pool not registered: {db_name}")
|
|
105
|
+
return AsyncRedis(connection_pool=self._async_pools[db_name])
|
|
106
|
+
|
|
107
|
+
def get_sync_client(self, db_name: str) -> SyncRedis:
|
|
108
|
+
"""获取指定数据库的同步 Redis 客户端"""
|
|
109
|
+
if db_name not in self._sync_pools:
|
|
110
|
+
raise RuntimeError(f"sync pool not registered: {db_name}")
|
|
111
|
+
return SyncRedis(connection_pool=self._sync_pools[db_name])
|
|
112
|
+
|
|
113
|
+
def setup_langgraph_checkpoint(self, db_name: str):
|
|
114
|
+
"""按需初始化 LangGraph checkpoint (仅 Agent 场景需要, lazy import)"""
|
|
115
|
+
from langgraph.checkpoint.redis import RedisSaver
|
|
116
|
+
|
|
117
|
+
pool = self._sync_pools.get(db_name)
|
|
118
|
+
if not pool:
|
|
119
|
+
raise RuntimeError(f"sync pool not registered: {db_name}")
|
|
120
|
+
|
|
121
|
+
client = SyncRedis(connection_pool=pool)
|
|
122
|
+
with RedisSaver.from_conn_string(redis_client=client) as saver:
|
|
123
|
+
saver.setup()
|
|
124
|
+
logger.info(f"langgraph checkpoint setup done: {db_name}")
|
|
125
|
+
|
|
126
|
+
async def setup_langgraph_checkpoint_async(self, db_name: str):
|
|
127
|
+
"""异步版 LangGraph checkpoint 初始化"""
|
|
128
|
+
from langgraph.checkpoint.redis import AsyncRedisSaver
|
|
129
|
+
|
|
130
|
+
pool = self._async_pools.get(db_name)
|
|
131
|
+
if not pool:
|
|
132
|
+
raise RuntimeError(f"async pool not registered: {db_name}")
|
|
133
|
+
|
|
134
|
+
client = AsyncRedis(connection_pool=pool)
|
|
135
|
+
async with AsyncRedisSaver.from_conn_string(redis_client=client) as saver:
|
|
136
|
+
await saver.asetup()
|
|
137
|
+
logger.info(f"langgraph async checkpoint setup done: {db_name}")
|
|
138
|
+
|
|
139
|
+
async def close_all(self):
|
|
140
|
+
"""关闭所有连接池"""
|
|
141
|
+
for pool in self._async_pools.values():
|
|
142
|
+
await pool.disconnect()
|
|
143
|
+
self._async_pools.clear()
|
|
144
|
+
|
|
145
|
+
for pool in self._sync_pools.values():
|
|
146
|
+
pool.disconnect()
|
|
147
|
+
self._sync_pools.clear()
|
|
148
|
+
|
|
149
|
+
self._registered_names.clear()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
redis_manager = RedisManager()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: fiuai-sdk-python
|
|
3
|
+
# Created Date: 2026-03-09
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Agent: Cursor
|
|
6
|
+
# Email: lmlala@aliyun.com
|
|
7
|
+
# Copyright (c) 2025 FiuAI
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CircuitState(str, Enum):
|
|
16
|
+
"""熔断器状态"""
|
|
17
|
+
|
|
18
|
+
CLOSED = "closed"
|
|
19
|
+
OPEN = "open"
|
|
20
|
+
HALF_OPEN = "half_open"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CircuitBreakerConfig(BaseModel):
|
|
24
|
+
"""熔断器配置"""
|
|
25
|
+
|
|
26
|
+
failure_threshold: int = Field(
|
|
27
|
+
default=5,
|
|
28
|
+
description="连续失败次数达此值后熔断",
|
|
29
|
+
)
|
|
30
|
+
recovery_timeout: float = Field(
|
|
31
|
+
default=30.0,
|
|
32
|
+
description="熔断后冷却秒数, 之后进入半开",
|
|
33
|
+
)
|
|
34
|
+
half_open_max_calls: int = Field(
|
|
35
|
+
default=1,
|
|
36
|
+
description="半开状态允许探测的请求数",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CacheConfig(BaseModel):
|
|
41
|
+
"""CacheClient 配置"""
|
|
42
|
+
|
|
43
|
+
default_ttl: Optional[int] = Field(
|
|
44
|
+
default=None,
|
|
45
|
+
description="默认 TTL (秒), None 表示不过期",
|
|
46
|
+
)
|
|
47
|
+
key_prefix: str = Field(
|
|
48
|
+
default="",
|
|
49
|
+
description="key 前缀, 如 'finnexus' 或 'world'",
|
|
50
|
+
)
|
|
51
|
+
redis_db_name: str = Field(
|
|
52
|
+
default="default",
|
|
53
|
+
description="redis_manager 中注册的连接池名称",
|
|
54
|
+
)
|
|
55
|
+
circuit_breaker: CircuitBreakerConfig = Field(
|
|
56
|
+
default_factory=CircuitBreakerConfig,
|
|
57
|
+
description="熔断器配置, 用于 Redis 不可用时降级",
|
|
58
|
+
)
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Project: fiuai-agent
|
|
3
|
-
Created Date: 2024-03-21
|
|
4
|
-
Author: liming
|
|
5
|
-
Email: lmlala@aliyun.com
|
|
6
|
-
Copyright (c) 2025 FiuAI
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from typing import Dict, Optional, List, Union, Tuple
|
|
10
|
-
from redis.asyncio import Redis as AsyncRedis, ConnectionPool as AsyncConnectionPool
|
|
11
|
-
from redis import Redis as SyncRedis, ConnectionPool as SyncConnectionPool
|
|
12
|
-
from langgraph.checkpoint.redis import RedisSaver, AsyncRedisSaver
|
|
13
|
-
from pydantic import BaseModel, Field
|
|
14
|
-
from ...utils.text import safe_str
|
|
15
|
-
|
|
16
|
-
class RedisDBConfig(BaseModel):
|
|
17
|
-
name: str = Field(description="连接名称")
|
|
18
|
-
host: str = Field(description="Redis主机")
|
|
19
|
-
port: int = Field(description="Redis端口")
|
|
20
|
-
password: str = Field(description="Redis密码")
|
|
21
|
-
db: int = Field(description="Redis数据库编号")
|
|
22
|
-
pool_size: int = Field(description="Redis连接池大小", default=20)
|
|
23
|
-
ttl: int = Field(description="Redis TTL", default=86400)
|
|
24
|
-
|
|
25
|
-
class RedisManager:
|
|
26
|
-
"""Redis连接池管理器单例类
|
|
27
|
-
|
|
28
|
-
用于管理多个Redis连接池,每个连接池对应不同的数据库
|
|
29
|
-
支持同步和异步两种模式
|
|
30
|
-
"""
|
|
31
|
-
_instance = None
|
|
32
|
-
_async_pools: Dict[str, AsyncConnectionPool] = {}
|
|
33
|
-
_sync_pools: Dict[str, SyncConnectionPool] = {}
|
|
34
|
-
_initialized = False
|
|
35
|
-
|
|
36
|
-
def __new__(cls):
|
|
37
|
-
if cls._instance is None:
|
|
38
|
-
cls._instance = super().__new__(cls)
|
|
39
|
-
return cls._instance
|
|
40
|
-
|
|
41
|
-
def __init__(self):
|
|
42
|
-
if not hasattr(self, '_initialized'):
|
|
43
|
-
self._initialized = False
|
|
44
|
-
|
|
45
|
-
async def initialize(
|
|
46
|
-
self,
|
|
47
|
-
async_dbs: List[RedisDBConfig] = [],
|
|
48
|
-
sync_dbs: List[RedisDBConfig] = []
|
|
49
|
-
):
|
|
50
|
-
"""初始化Redis连接池
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
async_dbs: 需要初始化的异步数据库列表
|
|
54
|
-
sync_dbs: 需要初始化的同步数据库列表
|
|
55
|
-
"""
|
|
56
|
-
if self._initialized:
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
# 初始化异步连接池
|
|
60
|
-
for db in async_dbs:
|
|
61
|
-
await self.get_async_pool(db)
|
|
62
|
-
# await self._init_async_index(db)
|
|
63
|
-
|
|
64
|
-
# 初始化同步连接池
|
|
65
|
-
for db in sync_dbs:
|
|
66
|
-
self.get_sync_pool(db)
|
|
67
|
-
# self._init_sync_index(db)
|
|
68
|
-
self._initialized = True
|
|
69
|
-
|
|
70
|
-
def _init_sync_index(self, db: RedisDBConfig) -> SyncConnectionPool:
|
|
71
|
-
"""初始化索引"""
|
|
72
|
-
_client = SyncRedis(connection_pool=self._sync_pools[db.name])
|
|
73
|
-
with RedisSaver.from_conn_string(
|
|
74
|
-
redis_client=_client
|
|
75
|
-
) as c:
|
|
76
|
-
c.setup()
|
|
77
|
-
|
|
78
|
-
async def _init_async_index(self, db: RedisDBConfig) -> AsyncConnectionPool:
|
|
79
|
-
"""初始化索引"""
|
|
80
|
-
_client = AsyncRedis(connection_pool=self._async_pools[db.name])
|
|
81
|
-
async with AsyncRedisSaver.from_conn_string(
|
|
82
|
-
redis_client=_client
|
|
83
|
-
) as c:
|
|
84
|
-
await c.asetup()
|
|
85
|
-
|
|
86
|
-
async def get_async_pool(self, db: RedisDBConfig) -> AsyncConnectionPool:
|
|
87
|
-
"""获取指定数据库的异步连接池
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
db: RedisDBConfig
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
AsyncConnectionPool: Redis异步连接池实例
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
if db.name not in self._async_pools:
|
|
97
|
-
self._async_pools[db.name] = AsyncConnectionPool.from_url(
|
|
98
|
-
f"redis://:{safe_str(db.password)}@{db.host}:{db.port}/{db.db}",
|
|
99
|
-
db=db.db,
|
|
100
|
-
decode_responses=True,
|
|
101
|
-
max_connections=db.pool_size
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
return self._async_pools[db.name]
|
|
105
|
-
|
|
106
|
-
def get_sync_pool(self, db: RedisDBConfig) -> SyncConnectionPool:
|
|
107
|
-
"""获取指定数据库的同步连接池
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
db: RedisDBConfig
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
SyncConnectionPool: Redis同步连接池实例
|
|
114
|
-
"""
|
|
115
|
-
if db.name not in self._sync_pools:
|
|
116
|
-
self._sync_pools[db.name] = SyncConnectionPool.from_url(
|
|
117
|
-
f"redis://:{safe_str(db.password)}@{db.host}:{db.port}/{db.db}",
|
|
118
|
-
db=db.db,
|
|
119
|
-
decode_responses=True,
|
|
120
|
-
max_connections=db.pool_size
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
# 初始化
|
|
124
|
-
with RedisSaver.from_conn_string(
|
|
125
|
-
redis_client=self._sync_pools[db.name]
|
|
126
|
-
) as c:
|
|
127
|
-
c.setup()
|
|
128
|
-
|
|
129
|
-
return self._sync_pools[db.name]
|
|
130
|
-
|
|
131
|
-
def get_async_client(self, db_name: str) -> AsyncRedis:
|
|
132
|
-
"""获取指定数据库的异步Redis客户端
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
db_name: 数据库名称
|
|
136
|
-
|
|
137
|
-
Returns:
|
|
138
|
-
AsyncRedis: Redis异步客户端实例
|
|
139
|
-
|
|
140
|
-
Raises:
|
|
141
|
-
RuntimeError: 如果Redis连接池未初始化
|
|
142
|
-
"""
|
|
143
|
-
if not self._initialized:
|
|
144
|
-
raise RuntimeError("pool not initialized")
|
|
145
|
-
|
|
146
|
-
if db_name not in self._async_pools:
|
|
147
|
-
raise RuntimeError(f"pool not found: {db_name}")
|
|
148
|
-
|
|
149
|
-
pool = self._async_pools[db_name]
|
|
150
|
-
return AsyncRedis(connection_pool=pool)
|
|
151
|
-
|
|
152
|
-
def get_sync_client(self, db_name: str) -> SyncRedis:
|
|
153
|
-
"""获取指定数据库的同步Redis客户端
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
db_name: 数据库名称
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
SyncRedis: Redis同步客户端实例
|
|
160
|
-
|
|
161
|
-
Raises:
|
|
162
|
-
RuntimeError: 如果Redis连接池未初始化
|
|
163
|
-
"""
|
|
164
|
-
if not self._initialized:
|
|
165
|
-
raise RuntimeError("pool not initialized")
|
|
166
|
-
|
|
167
|
-
if db_name not in self._sync_pools:
|
|
168
|
-
raise RuntimeError(f"pool not found: {db_name}")
|
|
169
|
-
|
|
170
|
-
pool = self._sync_pools[db_name]
|
|
171
|
-
return SyncRedis(connection_pool=pool)
|
|
172
|
-
|
|
173
|
-
async def close_all(self):
|
|
174
|
-
"""关闭所有连接池"""
|
|
175
|
-
# 关闭异步连接池
|
|
176
|
-
for pool in self._async_pools.values():
|
|
177
|
-
await pool.disconnect()
|
|
178
|
-
self._async_pools.clear()
|
|
179
|
-
|
|
180
|
-
# 关闭同步连接池
|
|
181
|
-
for pool in self._sync_pools.values():
|
|
182
|
-
pool.disconnect()
|
|
183
|
-
self._sync_pools.clear()
|
|
184
|
-
|
|
185
|
-
self._initialized = False
|
|
186
|
-
|
|
187
|
-
# 创建全局单例实例
|
|
188
|
-
redis_manager = RedisManager()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|