toms-fast 0.2.1__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.
- toms_fast-0.2.1.dist-info/METADATA +467 -0
- toms_fast-0.2.1.dist-info/RECORD +60 -0
- toms_fast-0.2.1.dist-info/WHEEL +4 -0
- toms_fast-0.2.1.dist-info/entry_points.txt +2 -0
- tomskit/__init__.py +0 -0
- tomskit/celery/README.md +693 -0
- tomskit/celery/__init__.py +4 -0
- tomskit/celery/celery.py +306 -0
- tomskit/celery/config.py +377 -0
- tomskit/cli/__init__.py +207 -0
- tomskit/cli/__main__.py +8 -0
- tomskit/cli/scaffold.py +123 -0
- tomskit/cli/templates/__init__.py +42 -0
- tomskit/cli/templates/base.py +348 -0
- tomskit/cli/templates/celery.py +101 -0
- tomskit/cli/templates/extensions.py +213 -0
- tomskit/cli/templates/fastapi.py +400 -0
- tomskit/cli/templates/migrations.py +281 -0
- tomskit/cli/templates_config.py +122 -0
- tomskit/logger/README.md +466 -0
- tomskit/logger/__init__.py +4 -0
- tomskit/logger/config.py +106 -0
- tomskit/logger/logger.py +290 -0
- tomskit/py.typed +0 -0
- tomskit/redis/README.md +462 -0
- tomskit/redis/__init__.py +6 -0
- tomskit/redis/config.py +85 -0
- tomskit/redis/redis_pool.py +87 -0
- tomskit/redis/redis_sync.py +66 -0
- tomskit/server/__init__.py +47 -0
- tomskit/server/config.py +117 -0
- tomskit/server/exceptions.py +412 -0
- tomskit/server/middleware.py +371 -0
- tomskit/server/parser.py +312 -0
- tomskit/server/resource.py +464 -0
- tomskit/server/server.py +276 -0
- tomskit/server/type.py +263 -0
- tomskit/sqlalchemy/README.md +590 -0
- tomskit/sqlalchemy/__init__.py +20 -0
- tomskit/sqlalchemy/config.py +125 -0
- tomskit/sqlalchemy/database.py +125 -0
- tomskit/sqlalchemy/pagination.py +359 -0
- tomskit/sqlalchemy/property.py +19 -0
- tomskit/sqlalchemy/sqlalchemy.py +131 -0
- tomskit/sqlalchemy/types.py +32 -0
- tomskit/task/README.md +67 -0
- tomskit/task/__init__.py +4 -0
- tomskit/task/task_manager.py +124 -0
- tomskit/tools/README.md +63 -0
- tomskit/tools/__init__.py +18 -0
- tomskit/tools/config.py +70 -0
- tomskit/tools/warnings.py +37 -0
- tomskit/tools/woker.py +81 -0
- tomskit/utils/README.md +666 -0
- tomskit/utils/README_SERIALIZER.md +644 -0
- tomskit/utils/__init__.py +35 -0
- tomskit/utils/fields.py +434 -0
- tomskit/utils/marshal_utils.py +137 -0
- tomskit/utils/response_utils.py +13 -0
- tomskit/utils/serializers.py +447 -0
tomskit/redis/README.md
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# Redis Module Guide
|
|
2
|
+
|
|
3
|
+
该模块提供了异步和同步 Redis 客户端支持,支持单机、Sentinel 和 Cluster 模式,适用于 FastAPI 异步环境。
|
|
4
|
+
|
|
5
|
+
## 模块概述
|
|
6
|
+
|
|
7
|
+
Redis 模块基于 `redis.asyncio` 和 `redis` 库,提供了完整的异步和同步 Redis 客户端支持。主要特性包括:
|
|
8
|
+
|
|
9
|
+
- ⚡ **完全异步**:基于 `redis.asyncio` 实现异步 Redis 客户端
|
|
10
|
+
- 🔄 **多种模式**:支持单机、Sentinel 和 Cluster 模式
|
|
11
|
+
- 🔒 **SSL 支持**:支持 SSL/TLS 加密连接
|
|
12
|
+
- 🛠️ **配置管理**:基于 Pydantic Settings 的配置类
|
|
13
|
+
- 🔧 **连接池管理**:自动管理连接池,支持高并发场景
|
|
14
|
+
- 📦 **类型安全**:使用泛型提供类型安全的客户端访问
|
|
15
|
+
|
|
16
|
+
**Import Path:**
|
|
17
|
+
```python
|
|
18
|
+
from tomskit.redis import (
|
|
19
|
+
RedisClientWrapper,
|
|
20
|
+
redis_client,
|
|
21
|
+
RedisConfig,
|
|
22
|
+
redis_sync_client
|
|
23
|
+
)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 核心类和函数
|
|
27
|
+
|
|
28
|
+
### RedisConfig
|
|
29
|
+
|
|
30
|
+
Redis 配置类,继承自 `pydantic_settings.BaseSettings`,用于管理 Redis 连接配置。
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
class RedisConfig(BaseSettings):
|
|
34
|
+
REDIS_HOST: str = Field(default="localhost", ...)
|
|
35
|
+
REDIS_PORT: PositiveInt = Field(default=6379, ...)
|
|
36
|
+
REDIS_USERNAME: Optional[str] = Field(default=None, ...)
|
|
37
|
+
REDIS_PASSWORD: Optional[str] = Field(default=None, ...)
|
|
38
|
+
REDIS_DB: NonNegativeInt = Field(default=0, ...)
|
|
39
|
+
REDIS_USE_SSL: bool = Field(default=False, ...)
|
|
40
|
+
REDIS_USE_SENTINEL: Optional[bool] = Field(default=False, ...)
|
|
41
|
+
REDIS_SENTINELS: Optional[str] = Field(default=None, ...)
|
|
42
|
+
REDIS_SENTINEL_SERVICE_NAME: Optional[str] = Field(default=None, ...)
|
|
43
|
+
REDIS_SENTINEL_USERNAME: Optional[str] = Field(default=None, ...)
|
|
44
|
+
REDIS_SENTINEL_PASSWORD: Optional[str] = Field(default=None, ...)
|
|
45
|
+
REDIS_SENTINEL_SOCKET_TIMEOUT: Optional[PositiveFloat] = Field(default=0.1, ...)
|
|
46
|
+
REDIS_USE_CLUSTERS: bool = Field(default=False, ...)
|
|
47
|
+
REDIS_CLUSTERS: Optional[str] = Field(default=None, ...)
|
|
48
|
+
REDIS_CLUSTERS_PASSWORD: Optional[str] = Field(default=None, ...)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**配置属性说明:**
|
|
52
|
+
- `REDIS_HOST`: Redis 服务器主机地址,默认为 `localhost`
|
|
53
|
+
- `REDIS_PORT`: Redis 服务器端口,默认为 `6379`,必须为正整数
|
|
54
|
+
- `REDIS_USERNAME`: Redis 认证用户名(如果需要),默认为 `None`
|
|
55
|
+
- `REDIS_PASSWORD`: Redis 认证密码(如果需要),默认为 `None`
|
|
56
|
+
- `REDIS_DB`: Redis 数据库编号(0-15),默认为 `0`
|
|
57
|
+
- `REDIS_USE_SSL`: 是否启用 SSL/TLS 加密连接,默认为 `False`
|
|
58
|
+
- `REDIS_USE_SENTINEL`: 是否启用 Redis Sentinel 模式,默认为 `False`
|
|
59
|
+
- `REDIS_SENTINELS`: Sentinel 节点列表,格式为逗号分隔的 `host:port`,例如 `"127.0.0.1:26379,127.0.0.1:26380"`
|
|
60
|
+
- `REDIS_SENTINEL_SERVICE_NAME`: Sentinel 服务名称,默认为 `None`
|
|
61
|
+
- `REDIS_SENTINEL_USERNAME`: Sentinel 认证用户名,默认为 `None`
|
|
62
|
+
- `REDIS_SENTINEL_PASSWORD`: Sentinel 认证密码,默认为 `None`
|
|
63
|
+
- `REDIS_SENTINEL_SOCKET_TIMEOUT`: Sentinel 连接超时时间(秒),默认为 `0.1`
|
|
64
|
+
- `REDIS_USE_CLUSTERS`: 是否启用 Redis Cluster 模式,默认为 `False`
|
|
65
|
+
- `REDIS_CLUSTERS`: Cluster 节点列表,格式为逗号分隔的 `host:port`,例如 `"127.0.0.1:7000,127.0.0.1:7001"`
|
|
66
|
+
- `REDIS_CLUSTERS_PASSWORD`: Cluster 认证密码,默认为 `None`
|
|
67
|
+
|
|
68
|
+
**使用示例:**
|
|
69
|
+
```python
|
|
70
|
+
from tomskit.redis.config import RedisConfig
|
|
71
|
+
|
|
72
|
+
# 单机模式配置
|
|
73
|
+
config = RedisConfig(
|
|
74
|
+
REDIS_HOST='localhost',
|
|
75
|
+
REDIS_PORT=6379,
|
|
76
|
+
REDIS_PASSWORD='your_password',
|
|
77
|
+
REDIS_DB=0
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Sentinel 模式配置
|
|
81
|
+
sentinel_config = RedisConfig(
|
|
82
|
+
REDIS_USE_SENTINEL=True,
|
|
83
|
+
REDIS_SENTINELS='127.0.0.1:26379,127.0.0.1:26380',
|
|
84
|
+
REDIS_SENTINEL_SERVICE_NAME='mymaster',
|
|
85
|
+
REDIS_PASSWORD='your_password'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Cluster 模式配置
|
|
89
|
+
cluster_config = RedisConfig(
|
|
90
|
+
REDIS_USE_CLUSTERS=True,
|
|
91
|
+
REDIS_CLUSTERS='127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002',
|
|
92
|
+
REDIS_CLUSTERS_PASSWORD='your_password'
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### RedisClientWrapper
|
|
97
|
+
|
|
98
|
+
Redis 客户端包装器,提供类型安全的 Redis 客户端访问。支持异步操作和连接池管理。
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
class RedisClientWrapper(Generic[T]):
|
|
102
|
+
_client: Optional[T]
|
|
103
|
+
|
|
104
|
+
def __init__(self) -> None: ...
|
|
105
|
+
|
|
106
|
+
def __getattr__(self, item: str) -> Any: ...
|
|
107
|
+
|
|
108
|
+
def set_client(self, client: T) -> None: ...
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def initialize(config: dict[str, Any]) -> None: ...
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
async def shutdown() -> None: ...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**功能特性:**
|
|
118
|
+
- 提供类型安全的 Redis 客户端访问
|
|
119
|
+
- 自动代理所有 Redis 客户端方法
|
|
120
|
+
- 支持连接池管理(默认最大连接数为 128)
|
|
121
|
+
- 支持异步操作
|
|
122
|
+
- 提供优雅关闭方法
|
|
123
|
+
|
|
124
|
+
**方法说明:**
|
|
125
|
+
- `initialize(config)`: 静态方法,初始化 Redis 客户端。根据配置自动选择单机、Sentinel 或 Cluster 模式
|
|
126
|
+
- `shutdown()`: 静态异步方法,关闭 Redis 客户端连接
|
|
127
|
+
- `set_client(client)`: 设置 Redis 客户端实例
|
|
128
|
+
- `__getattr__(item)`: 代理所有 Redis 客户端方法,如 `get`, `set`, `hget`, `hset` 等
|
|
129
|
+
|
|
130
|
+
### redis_client
|
|
131
|
+
|
|
132
|
+
全局异步 Redis 客户端实例,类型为 `RedisClientWrapper[Redis]`。
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
redis_client: RedisClientWrapper[Redis] = RedisClientWrapper()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**使用场景:**
|
|
139
|
+
在需要异步 Redis 操作的地方使用 `redis_client` 实例。
|
|
140
|
+
|
|
141
|
+
### redis_sync_client
|
|
142
|
+
|
|
143
|
+
创建同步 Redis 客户端函数,返回同步的 Redis 客户端实例。
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
def redis_sync_client(config: dict[str, Any]) -> Redis | None: ...
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**参数说明:**
|
|
150
|
+
- `config`: 配置字典,包含 Redis 连接参数
|
|
151
|
+
|
|
152
|
+
**返回值:**
|
|
153
|
+
- 返回 `Redis` 客户端实例,如果配置错误则返回 `None`
|
|
154
|
+
|
|
155
|
+
**功能特性:**
|
|
156
|
+
- 支持单机、Sentinel 和 Cluster 模式
|
|
157
|
+
- 支持 SSL/TLS 加密连接
|
|
158
|
+
- 同步操作,适用于非异步场景
|
|
159
|
+
|
|
160
|
+
## 完整使用示例
|
|
161
|
+
|
|
162
|
+
### 初始化异步客户端
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from tomskit.redis import RedisClientWrapper, redis_client, RedisConfig
|
|
166
|
+
|
|
167
|
+
# 创建配置
|
|
168
|
+
config = RedisConfig(
|
|
169
|
+
REDIS_HOST='localhost',
|
|
170
|
+
REDIS_PORT=6379,
|
|
171
|
+
REDIS_PASSWORD='your_password',
|
|
172
|
+
REDIS_DB=0
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# 将配置转换为字典
|
|
176
|
+
config_dict = config.model_dump()
|
|
177
|
+
|
|
178
|
+
# 初始化客户端
|
|
179
|
+
RedisClientWrapper.initialize(config_dict)
|
|
180
|
+
|
|
181
|
+
# 现在可以使用 redis_client 进行操作
|
|
182
|
+
await redis_client.set('key', 'value')
|
|
183
|
+
value = await redis_client.get('key')
|
|
184
|
+
print(value) # 输出: value
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 基础操作
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from tomskit.redis import redis_client
|
|
191
|
+
|
|
192
|
+
# 字符串操作
|
|
193
|
+
await redis_client.set('name', 'John')
|
|
194
|
+
name = await redis_client.get('name')
|
|
195
|
+
print(name) # 输出: John
|
|
196
|
+
|
|
197
|
+
# 设置过期时间
|
|
198
|
+
await redis_client.setex('token', 3600, 'abc123')
|
|
199
|
+
|
|
200
|
+
# 检查键是否存在
|
|
201
|
+
exists = await redis_client.exists('name')
|
|
202
|
+
print(exists) # 输出: 1
|
|
203
|
+
|
|
204
|
+
# 删除键
|
|
205
|
+
await redis_client.delete('name')
|
|
206
|
+
|
|
207
|
+
# 设置多个键值对
|
|
208
|
+
await redis_client.mset({'key1': 'value1', 'key2': 'value2'})
|
|
209
|
+
|
|
210
|
+
# 获取多个键的值
|
|
211
|
+
values = await redis_client.mget(['key1', 'key2'])
|
|
212
|
+
print(values) # 输出: ['value1', 'value2']
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Hash 操作
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from tomskit.redis import redis_client
|
|
219
|
+
|
|
220
|
+
# 设置 Hash 字段
|
|
221
|
+
await redis_client.hset('user:1', mapping={
|
|
222
|
+
'name': 'John',
|
|
223
|
+
'age': '30',
|
|
224
|
+
'email': 'john@example.com'
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
# 获取 Hash 字段
|
|
228
|
+
name = await redis_client.hget('user:1', 'name')
|
|
229
|
+
print(name) # 输出: John
|
|
230
|
+
|
|
231
|
+
# 获取所有 Hash 字段
|
|
232
|
+
user_data = await redis_client.hgetall('user:1')
|
|
233
|
+
print(user_data) # 输出: {'name': 'John', 'age': '30', 'email': 'john@example.com'}
|
|
234
|
+
|
|
235
|
+
# 删除 Hash 字段
|
|
236
|
+
await redis_client.hdel('user:1', 'email')
|
|
237
|
+
|
|
238
|
+
# 检查 Hash 字段是否存在
|
|
239
|
+
exists = await redis_client.hexists('user:1', 'name')
|
|
240
|
+
print(exists) # 输出: True
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### List 操作
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from tomskit.redis import redis_client
|
|
247
|
+
|
|
248
|
+
# 从左侧推入
|
|
249
|
+
await redis_client.lpush('tasks', 'task1', 'task2', 'task3')
|
|
250
|
+
|
|
251
|
+
# 从右侧推入
|
|
252
|
+
await redis_client.rpush('tasks', 'task4')
|
|
253
|
+
|
|
254
|
+
# 获取列表长度
|
|
255
|
+
length = await redis_client.llen('tasks')
|
|
256
|
+
print(length) # 输出: 4
|
|
257
|
+
|
|
258
|
+
# 获取列表元素
|
|
259
|
+
tasks = await redis_client.lrange('tasks', 0, -1)
|
|
260
|
+
print(tasks) # 输出: ['task3', 'task2', 'task1', 'task4']
|
|
261
|
+
|
|
262
|
+
# 从左侧弹出
|
|
263
|
+
task = await redis_client.lpop('tasks')
|
|
264
|
+
print(task) # 输出: task3
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Set 操作
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from tomskit.redis import redis_client
|
|
271
|
+
|
|
272
|
+
# 添加元素
|
|
273
|
+
await redis_client.sadd('tags', 'python', 'redis', 'fastapi')
|
|
274
|
+
|
|
275
|
+
# 获取所有元素
|
|
276
|
+
tags = await redis_client.smembers('tags')
|
|
277
|
+
print(tags) # 输出: {'python', 'redis', 'fastapi'}
|
|
278
|
+
|
|
279
|
+
# 检查元素是否存在
|
|
280
|
+
is_member = await redis_client.sismember('tags', 'python')
|
|
281
|
+
print(is_member) # 输出: True
|
|
282
|
+
|
|
283
|
+
# 获取集合大小
|
|
284
|
+
size = await redis_client.scard('tags')
|
|
285
|
+
print(size) # 输出: 3
|
|
286
|
+
|
|
287
|
+
# 移除元素
|
|
288
|
+
await redis_client.srem('tags', 'redis')
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 有序集合(Sorted Set)操作
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from tomskit.redis import redis_client
|
|
295
|
+
|
|
296
|
+
# 添加元素(带分数)
|
|
297
|
+
await redis_client.zadd('leaderboard', {'player1': 100, 'player2': 200, 'player3': 150})
|
|
298
|
+
|
|
299
|
+
# 获取排名(按分数从高到低)
|
|
300
|
+
top_players = await redis_client.zrevrange('leaderboard', 0, 2, withscores=True)
|
|
301
|
+
print(top_players) # 输出: [('player2', 200.0), ('player3', 150.0), ('player1', 100.0)]
|
|
302
|
+
|
|
303
|
+
# 获取元素分数
|
|
304
|
+
score = await redis_client.zscore('leaderboard', 'player1')
|
|
305
|
+
print(score) # 输出: 100.0
|
|
306
|
+
|
|
307
|
+
# 增加元素分数
|
|
308
|
+
new_score = await redis_client.zincrby('leaderboard', 50, 'player1')
|
|
309
|
+
print(new_score) # 输出: 150.0
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 在 FastAPI 中使用
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from fastapi import FastAPI
|
|
316
|
+
from tomskit.redis import RedisClientWrapper, redis_client, RedisConfig
|
|
317
|
+
from contextlib import asynccontextmanager
|
|
318
|
+
|
|
319
|
+
app = FastAPI()
|
|
320
|
+
|
|
321
|
+
@asynccontextmanager
|
|
322
|
+
async def lifespan(app: FastAPI):
|
|
323
|
+
# 启动时初始化 Redis
|
|
324
|
+
config = RedisConfig()
|
|
325
|
+
RedisClientWrapper.initialize(config.model_dump())
|
|
326
|
+
yield
|
|
327
|
+
# 关闭时清理 Redis 连接
|
|
328
|
+
await RedisClientWrapper.shutdown()
|
|
329
|
+
|
|
330
|
+
app = FastAPI(lifespan=lifespan)
|
|
331
|
+
|
|
332
|
+
@app.get("/cache/{key}")
|
|
333
|
+
async def get_cache(key: str):
|
|
334
|
+
value = await redis_client.get(key)
|
|
335
|
+
if value is None:
|
|
336
|
+
return {"error": "Key not found"}
|
|
337
|
+
return {"key": key, "value": value}
|
|
338
|
+
|
|
339
|
+
@app.post("/cache/{key}")
|
|
340
|
+
async def set_cache(key: str, value: str):
|
|
341
|
+
await redis_client.set(key, value)
|
|
342
|
+
return {"key": key, "value": value, "status": "set"}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### 使用 Sentinel 模式
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
from tomskit.redis import RedisClientWrapper, redis_client, RedisConfig
|
|
349
|
+
|
|
350
|
+
# Sentinel 模式配置
|
|
351
|
+
config = RedisConfig(
|
|
352
|
+
REDIS_USE_SENTINEL=True,
|
|
353
|
+
REDIS_SENTINELS='127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381',
|
|
354
|
+
REDIS_SENTINEL_SERVICE_NAME='mymaster',
|
|
355
|
+
REDIS_PASSWORD='your_password',
|
|
356
|
+
REDIS_DB=0
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# 初始化客户端
|
|
360
|
+
RedisClientWrapper.initialize(config.model_dump())
|
|
361
|
+
|
|
362
|
+
# 使用方式与单机模式相同
|
|
363
|
+
await redis_client.set('key', 'value')
|
|
364
|
+
value = await redis_client.get('key')
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 使用 Cluster 模式
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
from tomskit.redis import RedisClientWrapper, redis_client, RedisConfig
|
|
371
|
+
|
|
372
|
+
# Cluster 模式配置
|
|
373
|
+
config = RedisConfig(
|
|
374
|
+
REDIS_USE_CLUSTERS=True,
|
|
375
|
+
REDIS_CLUSTERS='127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002',
|
|
376
|
+
REDIS_CLUSTERS_PASSWORD='your_password'
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# 初始化客户端
|
|
380
|
+
RedisClientWrapper.initialize(config.model_dump())
|
|
381
|
+
|
|
382
|
+
# 使用方式与单机模式相同
|
|
383
|
+
await redis_client.set('key', 'value')
|
|
384
|
+
value = await redis_client.get('key')
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### 使用同步客户端
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
from tomskit.redis import redis_sync_client, RedisConfig
|
|
391
|
+
|
|
392
|
+
# 创建配置
|
|
393
|
+
config = RedisConfig(
|
|
394
|
+
REDIS_HOST='localhost',
|
|
395
|
+
REDIS_PORT=6379,
|
|
396
|
+
REDIS_PASSWORD='your_password'
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# 转换为字典
|
|
400
|
+
config_dict = config.model_dump()
|
|
401
|
+
|
|
402
|
+
# 创建同步客户端
|
|
403
|
+
redis = redis_sync_client(config_dict)
|
|
404
|
+
|
|
405
|
+
if redis:
|
|
406
|
+
# 同步操作
|
|
407
|
+
redis.set('key', 'value')
|
|
408
|
+
value = redis.get('key')
|
|
409
|
+
print(value) # 输出: value
|
|
410
|
+
|
|
411
|
+
# 关闭连接
|
|
412
|
+
redis.close()
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### 优雅关闭
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
from tomskit.redis import RedisClientWrapper
|
|
419
|
+
|
|
420
|
+
# 在应用关闭时调用
|
|
421
|
+
async def cleanup():
|
|
422
|
+
await RedisClientWrapper.shutdown()
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## 环境变量配置
|
|
426
|
+
|
|
427
|
+
Redis 模块支持通过环境变量进行配置:
|
|
428
|
+
|
|
429
|
+
- `REDIS_HOST`: Redis 服务器主机地址
|
|
430
|
+
- `REDIS_PORT`: Redis 服务器端口
|
|
431
|
+
- `REDIS_USERNAME`: Redis 认证用户名
|
|
432
|
+
- `REDIS_PASSWORD`: Redis 认证密码
|
|
433
|
+
- `REDIS_DB`: Redis 数据库编号
|
|
434
|
+
- `REDIS_USE_SSL`: 是否启用 SSL/TLS
|
|
435
|
+
- `REDIS_USE_SENTINEL`: 是否启用 Sentinel 模式
|
|
436
|
+
- `REDIS_SENTINELS`: Sentinel 节点列表
|
|
437
|
+
- `REDIS_SENTINEL_SERVICE_NAME`: Sentinel 服务名称
|
|
438
|
+
- `REDIS_SENTINEL_USERNAME`: Sentinel 认证用户名
|
|
439
|
+
- `REDIS_SENTINEL_PASSWORD`: Sentinel 认证密码
|
|
440
|
+
- `REDIS_SENTINEL_SOCKET_TIMEOUT`: Sentinel 连接超时时间
|
|
441
|
+
- `REDIS_USE_CLUSTERS`: 是否启用 Cluster 模式
|
|
442
|
+
- `REDIS_CLUSTERS`: Cluster 节点列表
|
|
443
|
+
- `REDIS_CLUSTERS_PASSWORD`: Cluster 认证密码
|
|
444
|
+
|
|
445
|
+
**注意:** 在代码中使用 `REDIS_USE_CLUSTER`(单数)来检查配置,但配置类中定义的是 `REDIS_USE_CLUSTERS`(复数)。初始化时需要确保配置字典中的键名正确。
|
|
446
|
+
|
|
447
|
+
## 注意事项
|
|
448
|
+
|
|
449
|
+
1. **异步操作**:`redis_client` 的所有操作都是异步的,需要使用 `await` 关键字
|
|
450
|
+
2. **连接池管理**:异步客户端默认最大连接数为 128,可根据实际需求调整
|
|
451
|
+
3. **初始化顺序**:在使用 `redis_client` 之前必须先调用 `RedisClientWrapper.initialize()`
|
|
452
|
+
4. **优雅关闭**:应用关闭时应该调用 `RedisClientWrapper.shutdown()` 来关闭连接
|
|
453
|
+
5. **配置转换**:使用 `RedisConfig` 时,需要先转换为字典再传递给 `initialize()` 方法
|
|
454
|
+
6. **Sentinel 和 Cluster**:使用 Sentinel 或 Cluster 模式时,需要确保相应的配置项都已正确设置
|
|
455
|
+
7. **SSL 连接**:启用 SSL 时需要确保 Redis 服务器支持 SSL/TLS
|
|
456
|
+
8. **同步客户端**:同步客户端主要用于非异步场景,如 Celery 任务等
|
|
457
|
+
|
|
458
|
+
## 相关文档
|
|
459
|
+
|
|
460
|
+
- [Redis Guide](../docs/specs/redis_guide.md) - 详细的 Redis 使用指南
|
|
461
|
+
- [Redis 官方文档](https://redis.io/docs/) - Redis 官方文档
|
|
462
|
+
- [redis-py 文档](https://redis.readthedocs.io/) - redis-py 库文档
|
tomskit/redis/config.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, NonNegativeInt, PositiveFloat, PositiveInt
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RedisConfig(BaseSettings):
|
|
8
|
+
"""
|
|
9
|
+
Configuration settings for Redis connection
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
REDIS_HOST: str = Field(
|
|
13
|
+
description="Hostname or IP address of the Redis server",
|
|
14
|
+
default="localhost",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
REDIS_PORT: PositiveInt = Field(
|
|
18
|
+
description="Port number on which the Redis server is listening",
|
|
19
|
+
default=6379,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
REDIS_USERNAME: Optional[str] = Field(
|
|
23
|
+
description="Username for Redis authentication (if required)",
|
|
24
|
+
default=None,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
REDIS_PASSWORD: Optional[str] = Field(
|
|
28
|
+
description="Password for Redis authentication (if required)",
|
|
29
|
+
default=None,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
REDIS_DB: NonNegativeInt = Field(
|
|
33
|
+
description="Redis database number to use (0-15)",
|
|
34
|
+
default=0,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
REDIS_USE_SSL: bool = Field(
|
|
38
|
+
description="Enable SSL/TLS for the Redis connection",
|
|
39
|
+
default=False,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
REDIS_USE_SENTINEL: Optional[bool] = Field(
|
|
43
|
+
description="Enable Redis Sentinel mode for high availability",
|
|
44
|
+
default=False,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
REDIS_SENTINELS: Optional[str] = Field(
|
|
48
|
+
description="Comma-separated list of Redis Sentinel nodes (host:port)",
|
|
49
|
+
default=None,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
REDIS_SENTINEL_SERVICE_NAME: Optional[str] = Field(
|
|
53
|
+
description="Name of the Redis Sentinel service to monitor",
|
|
54
|
+
default=None,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
REDIS_SENTINEL_USERNAME: Optional[str] = Field(
|
|
58
|
+
description="Username for Redis Sentinel authentication (if required)",
|
|
59
|
+
default=None,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
REDIS_SENTINEL_PASSWORD: Optional[str] = Field(
|
|
63
|
+
description="Password for Redis Sentinel authentication (if required)",
|
|
64
|
+
default=None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
REDIS_SENTINEL_SOCKET_TIMEOUT: Optional[PositiveFloat] = Field(
|
|
68
|
+
description="Socket timeout in seconds for Redis Sentinel connections",
|
|
69
|
+
default=0.1,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
REDIS_USE_CLUSTERS: bool = Field(
|
|
73
|
+
description="Enable Redis Clusters mode for high availability",
|
|
74
|
+
default=False,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
REDIS_CLUSTERS: Optional[str] = Field(
|
|
78
|
+
description="Comma-separated list of Redis Clusters nodes (host:port)",
|
|
79
|
+
default=None,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
REDIS_CLUSTERS_PASSWORD: Optional[str] = Field(
|
|
83
|
+
description="Password for Redis Clusters authentication (if required)",
|
|
84
|
+
default=None,
|
|
85
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis 扩展模块
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any, Union, Optional, TypeVar, Generic
|
|
5
|
+
from redis.asyncio import Connection, ConnectionPool, Redis, Sentinel, SSLConnection, RedisCluster
|
|
6
|
+
from redis.asyncio.cluster import ClusterNode
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T", bound=Redis)
|
|
9
|
+
|
|
10
|
+
class RedisClientWrapper(Generic[T]):
|
|
11
|
+
_client: Optional[T] = None
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self._client: Redis | None = None
|
|
14
|
+
|
|
15
|
+
def __getattr__(self, item) -> Any:
|
|
16
|
+
if self._client is None:
|
|
17
|
+
raise RuntimeError("Redis client is not initialized. Call initialize first.")
|
|
18
|
+
return getattr(self._client, item)
|
|
19
|
+
|
|
20
|
+
def set_client(self, client: T) -> None:
|
|
21
|
+
if self._client is None:
|
|
22
|
+
self._client = client
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def initialize(config) -> None:
|
|
26
|
+
global redis_client
|
|
27
|
+
connection_class: type[Union[Connection, SSLConnection]] = Connection
|
|
28
|
+
if config.get("REDIS_USE_SSL"):
|
|
29
|
+
connection_class = SSLConnection
|
|
30
|
+
|
|
31
|
+
redis_params = {
|
|
32
|
+
"username": config.get("REDIS_USERNAME"),
|
|
33
|
+
"password": config.get("REDIS_PASSWORD"),
|
|
34
|
+
"db": config.get("REDIS_DB"),
|
|
35
|
+
"encoding": "utf-8",
|
|
36
|
+
"encoding_errors": "strict",
|
|
37
|
+
"decode_responses": True,
|
|
38
|
+
"max_connections": config.get("REDIS_MAX_CONNECTIONS", 128),
|
|
39
|
+
}
|
|
40
|
+
if config.get("REDIS_USE_SENTINEL"):
|
|
41
|
+
sentinel_hosts = [
|
|
42
|
+
(node.split(":")[0], int(node.split(":")[1])) for node in config.get("REDIS_SENTINELS").split(",")
|
|
43
|
+
]
|
|
44
|
+
sentinel = Sentinel(
|
|
45
|
+
sentinel_hosts,
|
|
46
|
+
sentinel_kwargs={
|
|
47
|
+
"socket_timeout": config.get("REDIS_SENTINEL_SOCKET_TIMEOUT", 0.1),
|
|
48
|
+
"username": config.get("REDIS_SENTINEL_USERNAME"),
|
|
49
|
+
"password": config.get("REDIS_SENTINEL_PASSWORD"),
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
master = sentinel.master_for(config.get("REDIS_SENTINEL_SERVICE_NAME"), **redis_params)
|
|
53
|
+
redis_client.set_client(master)
|
|
54
|
+
elif config.get("REDIS_USE_CLUSTER"):
|
|
55
|
+
nodes = [
|
|
56
|
+
ClusterNode(host=node.split(":")[0], port=int(node.split(":")[1])) for node in config.get("REDIS_CLUSTERS").split(",")
|
|
57
|
+
]
|
|
58
|
+
redis_params.update(
|
|
59
|
+
{
|
|
60
|
+
"password" : config.get("REDIS_CLUSTERS_PASSWORD"),
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
cluster = RedisCluster(
|
|
64
|
+
startup_nodes=nodes,
|
|
65
|
+
**redis_params
|
|
66
|
+
)
|
|
67
|
+
redis_client.set_client(cluster) # type: ignore
|
|
68
|
+
else:
|
|
69
|
+
redis_params.update(
|
|
70
|
+
{
|
|
71
|
+
"host": config.get("REDIS_HOST"),
|
|
72
|
+
"port": config.get("REDIS_PORT"),
|
|
73
|
+
"max_connections": config.get("REDIS_MAX_CONNECTIONS", 128),
|
|
74
|
+
"connection_class": connection_class,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
pool = ConnectionPool(**redis_params)
|
|
78
|
+
redis_client.set_client(Redis(connection_pool=pool))
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
async def shutdown() -> None:
|
|
82
|
+
global redis_client
|
|
83
|
+
if redis_client._client is not None:
|
|
84
|
+
await redis_client._client.aclose()
|
|
85
|
+
redis_client._client = None
|
|
86
|
+
|
|
87
|
+
redis_client: RedisClientWrapper[Redis] = RedisClientWrapper()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis 扩展模块
|
|
3
|
+
"""
|
|
4
|
+
from typing import Union
|
|
5
|
+
from redis import Redis, Connection, ConnectionPool, Sentinel, SSLConnection, RedisCluster
|
|
6
|
+
from redis.cluster import ClusterNode
|
|
7
|
+
|
|
8
|
+
def redis_sync_client(config) -> Redis | None:
|
|
9
|
+
"""
|
|
10
|
+
Redis 同步客户端
|
|
11
|
+
"""
|
|
12
|
+
redis: Redis | None = None
|
|
13
|
+
|
|
14
|
+
connection_class: type[Union[Connection, SSLConnection]] = Connection
|
|
15
|
+
if config.get("REDIS_USE_SSL"):
|
|
16
|
+
connection_class = SSLConnection
|
|
17
|
+
|
|
18
|
+
redis_params = {
|
|
19
|
+
"username": config.get("REDIS_USERNAME"),
|
|
20
|
+
"password": config.get("REDIS_PASSWORD"),
|
|
21
|
+
"db": config.get("REDIS_DB"),
|
|
22
|
+
"encoding": "utf-8",
|
|
23
|
+
"encoding_errors": "strict",
|
|
24
|
+
"decode_responses": True,
|
|
25
|
+
"max_connections": 1,
|
|
26
|
+
}
|
|
27
|
+
if config.get("REDIS_USE_SENTINEL"):
|
|
28
|
+
sentinel_hosts = [
|
|
29
|
+
(node.split(":")[0], int(node.split(":")[1])) for node in config.get("REDIS_SENTINELS").split(",")
|
|
30
|
+
]
|
|
31
|
+
sentinel = Sentinel(
|
|
32
|
+
sentinel_hosts,
|
|
33
|
+
sentinel_kwargs={
|
|
34
|
+
"socket_timeout": config.get("REDIS_SENTINEL_SOCKET_TIMEOUT", 0.1),
|
|
35
|
+
"username": config.get("REDIS_SENTINEL_USERNAME"),
|
|
36
|
+
"password": config.get("REDIS_SENTINEL_PASSWORD"),
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
redis = sentinel.master_for(config.get("REDIS_SENTINEL_SERVICE_NAME"), **redis_params)
|
|
40
|
+
elif config.get("REDIS_USE_CLUSTER"):
|
|
41
|
+
nodes = [
|
|
42
|
+
ClusterNode(host=node.split(":")[0], port=int(node.split(":")[1])) for node in config.get("REDIS_CLUSTERS").split(",")
|
|
43
|
+
]
|
|
44
|
+
redis_params.update(
|
|
45
|
+
{
|
|
46
|
+
"password" : config.get("REDIS_CLUSTERS_PASSWORD"),
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
redis = RedisCluster( # type: ignore
|
|
50
|
+
startup_nodes=nodes,
|
|
51
|
+
**redis_params
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
redis_params.update(
|
|
55
|
+
{
|
|
56
|
+
"host": config.get("REDIS_HOST"),
|
|
57
|
+
"port": config.get("REDIS_PORT"),
|
|
58
|
+
"max_connections": 1,
|
|
59
|
+
"connection_class": connection_class,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
pool = ConnectionPool(**redis_params)
|
|
63
|
+
redis = Redis(connection_pool=pool)
|
|
64
|
+
|
|
65
|
+
return redis
|
|
66
|
+
|