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.
Files changed (43) hide show
  1. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/PKG-INFO +1 -1
  2. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/pyproject.toml +1 -1
  3. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/__init__.py +1 -1
  4. fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/__init__.py +30 -0
  5. fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/cache_client.py +233 -0
  6. fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/circuit_breaker.py +78 -0
  7. fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/decorator.py +105 -0
  8. fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/redis_manager.py +152 -0
  9. fiuai_sdk_python-0.8.0/src/fiuai_sdk_python/pkg/cache/types.py +58 -0
  10. fiuai_sdk_python-0.7.5/src/fiuai_sdk_python/pkg/cache/__init__.py +0 -6
  11. fiuai_sdk_python-0.7.5/src/fiuai_sdk_python/pkg/cache/redis_manager.py +0 -188
  12. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/.gitignore +0 -0
  13. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/CHANGELOG.md +0 -0
  14. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/LICENSE +0 -0
  15. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/README.md +0 -0
  16. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/__init__.py +0 -0
  17. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/context_mgr.py +0 -0
  18. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/header.py +0 -0
  19. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/helper.py +0 -0
  20. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/auth/type.py +0 -0
  21. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/bank.py +0 -0
  22. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/client.py +0 -0
  23. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/company.py +0 -0
  24. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/config.py +0 -0
  25. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/const.py +0 -0
  26. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/context.py +0 -0
  27. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/datatype.py +0 -0
  28. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/doctype.py +0 -0
  29. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/error.py +0 -0
  30. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/examples/fastapi_integration.py +0 -0
  31. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/http/__init__.py +0 -0
  32. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/http/client.py +0 -0
  33. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/item.py +0 -0
  34. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/perm.py +0 -0
  35. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/profile.py +0 -0
  36. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/resp.py +0 -0
  37. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/setup.py +0 -0
  38. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/type.py +0 -0
  39. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/util.py +0 -0
  40. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/utils/__init__.py +0 -0
  41. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/utils/ids.py +0 -0
  42. {fiuai_sdk_python-0.7.5 → fiuai_sdk_python-0.8.0}/src/fiuai_sdk_python/utils/logger.py +0 -0
  43. {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.7.5
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fiuai_sdk_python"
3
- version = "0.7.5"
3
+ version = "0.8.0"
4
4
  description = "FiuAI Python SDK - 企业级AI服务集成开发工具包"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -11,7 +11,7 @@ from .context import (
11
11
  get_trace_id,
12
12
  )
13
13
 
14
- __version__ = "0.6.5"
14
+ __version__ = "0.8.0"
15
15
 
16
16
  __all__ = [
17
17
  'FiuaiSDK',
@@ -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,6 +0,0 @@
1
- from .redis_manager import redis_manager, RedisDBConfig
2
-
3
- __all__ = [
4
- "redis_manager",
5
- "RedisDBConfig",
6
- ]
@@ -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()