pythonboot-redis 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ """Public native Redis clients, configuration properties, and static utilities."""
2
+
3
+ from pythonboot_redis.async_util import AsyncRedisUtil
4
+ from pythonboot_redis.client import AsyncRedisClient, RedisClient
5
+ from pythonboot_redis.properties import RedisProperties
6
+ from pythonboot_redis.util import RedisUtil
7
+
8
+ __all__ = ["RedisClient", "AsyncRedisClient", "RedisProperties", "RedisUtil", "AsyncRedisUtil"]
@@ -0,0 +1,151 @@
1
+ """Asynchronous static Redis operations resolved against the current application context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from datetime import timedelta
7
+ from math import isfinite
8
+
9
+ from redis.asyncio.lock import Lock
10
+
11
+ from pythonboot import ContextUtil
12
+ from pythonboot_redis.client import AsyncRedisClient, RedisKey, RedisResult, RedisValue
13
+ from pythonboot_redis.util import validate_lock_options
14
+
15
+
16
+ class AsyncRedisUtil:
17
+ """Awaitable Redis helpers without blocking calls or globally cached clients."""
18
+
19
+ @staticmethod
20
+ async def get_client(*, name: str | None = None, qualifier: str | None = None) -> AsyncRedisClient:
21
+ return await ContextUtil.aget_bean(AsyncRedisClient, name=name, qualifier=qualifier)
22
+
23
+ @staticmethod
24
+ async def get(key: RedisKey, *, name: str | None = None) -> RedisResult | None:
25
+ return await (await AsyncRedisUtil.get_client(name=name)).get(key)
26
+
27
+ @staticmethod
28
+ async def set(
29
+ key: RedisKey, value: RedisValue, *, ex: int | timedelta | None = None,
30
+ px: int | timedelta | None = None, nx: bool = False, xx: bool = False,
31
+ keepttl: bool = False, name: str | None = None,
32
+ ) -> bool | None:
33
+ client = await AsyncRedisUtil.get_client(name=name)
34
+ return await client.set(key, value, ex=ex, px=px, nx=nx, xx=xx, keepttl=keepttl)
35
+
36
+ @staticmethod
37
+ async def delete(*keys: RedisKey, name: str | None = None) -> int:
38
+ return await (await AsyncRedisUtil.get_client(name=name)).delete(*keys)
39
+
40
+ @staticmethod
41
+ async def exists(*keys: RedisKey, name: str | None = None) -> int:
42
+ return await (await AsyncRedisUtil.get_client(name=name)).exists(*keys)
43
+
44
+ @staticmethod
45
+ async def expire(key: RedisKey, seconds: int | timedelta, *, name: str | None = None) -> bool:
46
+ return await (await AsyncRedisUtil.get_client(name=name)).expire(key, seconds)
47
+
48
+ @staticmethod
49
+ async def ttl(key: RedisKey, *, name: str | None = None) -> int:
50
+ return await (await AsyncRedisUtil.get_client(name=name)).ttl(key)
51
+
52
+ @staticmethod
53
+ async def incr(key: RedisKey, amount: int = 1, *, name: str | None = None) -> int:
54
+ return await (await AsyncRedisUtil.get_client(name=name)).incr(key, amount)
55
+
56
+ @staticmethod
57
+ async def decr(key: RedisKey, amount: int = 1, *, name: str | None = None) -> int:
58
+ return await (await AsyncRedisUtil.get_client(name=name)).decr(key, amount)
59
+
60
+ @staticmethod
61
+ async def hset(key: RedisKey, mapping: Mapping[RedisKey, RedisValue], *, name: str | None = None) -> int:
62
+ return await (await AsyncRedisUtil.get_client(name=name)).hset(key, mapping=mapping)
63
+
64
+ @staticmethod
65
+ async def hget(key: RedisKey, field: RedisKey, *, name: str | None = None) -> RedisResult | None:
66
+ return await (await AsyncRedisUtil.get_client(name=name)).hget(key, field)
67
+
68
+ @staticmethod
69
+ async def hgetall(key: RedisKey, *, name: str | None = None) -> dict[RedisResult, RedisResult]:
70
+ return await (await AsyncRedisUtil.get_client(name=name)).hgetall(key)
71
+
72
+ @staticmethod
73
+ async def hdel(key: RedisKey, *fields: RedisKey, name: str | None = None) -> int:
74
+ return await (await AsyncRedisUtil.get_client(name=name)).hdel(key, *fields)
75
+
76
+ @staticmethod
77
+ async def lpush(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
78
+ return await (await AsyncRedisUtil.get_client(name=name)).lpush(key, *values)
79
+
80
+ @staticmethod
81
+ async def rpush(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
82
+ return await (await AsyncRedisUtil.get_client(name=name)).rpush(key, *values)
83
+
84
+ @staticmethod
85
+ async def lpop(key: RedisKey, *, name: str | None = None) -> RedisResult | None:
86
+ return await (await AsyncRedisUtil.get_client(name=name)).lpop(key)
87
+
88
+ @staticmethod
89
+ async def rpop(key: RedisKey, *, name: str | None = None) -> RedisResult | None:
90
+ return await (await AsyncRedisUtil.get_client(name=name)).rpop(key)
91
+
92
+ @staticmethod
93
+ async def lrange(key: RedisKey, start: int = 0, end: int = -1, *, name: str | None = None) -> list[RedisResult]:
94
+ return await (await AsyncRedisUtil.get_client(name=name)).lrange(key, start, end)
95
+
96
+ @staticmethod
97
+ async def llen(key: RedisKey, *, name: str | None = None) -> int:
98
+ return await (await AsyncRedisUtil.get_client(name=name)).llen(key)
99
+
100
+ @staticmethod
101
+ async def sadd(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
102
+ return await (await AsyncRedisUtil.get_client(name=name)).sadd(key, *values)
103
+
104
+ @staticmethod
105
+ async def smembers(key: RedisKey, *, name: str | None = None) -> set[RedisResult]:
106
+ return await (await AsyncRedisUtil.get_client(name=name)).smembers(key)
107
+
108
+ @staticmethod
109
+ async def srem(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
110
+ return await (await AsyncRedisUtil.get_client(name=name)).srem(key, *values)
111
+
112
+ @staticmethod
113
+ async def zadd(key: RedisKey, mapping: Mapping[RedisKey, float], *, name: str | None = None) -> int:
114
+ return await (await AsyncRedisUtil.get_client(name=name)).zadd(key, mapping)
115
+
116
+ @staticmethod
117
+ async def zrange(key: RedisKey, start: int = 0, end: int = -1, *, name: str | None = None) -> list[RedisResult]:
118
+ return await (await AsyncRedisUtil.get_client(name=name)).zrange(key, start, end)
119
+
120
+ @staticmethod
121
+ async def zrem(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
122
+ return await (await AsyncRedisUtil.get_client(name=name)).zrem(key, *values)
123
+
124
+ @staticmethod
125
+ async def get_lock(
126
+ key: RedisKey, *, timeout: float = 30, blocking_timeout: float = 5,
127
+ sleep: float = 0.1, thread_local: bool = True, name: str | None = None,
128
+ ) -> Lock:
129
+ validate_lock_options(timeout, blocking_timeout, sleep)
130
+ client = await AsyncRedisUtil.get_client(name=name)
131
+ return client.lock(
132
+ key, timeout=timeout, blocking_timeout=blocking_timeout, sleep=sleep, thread_local=thread_local,
133
+ )
134
+
135
+ @staticmethod
136
+ async def acquire_lock(lock: Lock, *, blocking: bool = True) -> bool:
137
+ return await lock.acquire(blocking=blocking)
138
+
139
+ @staticmethod
140
+ async def release_lock(lock: Lock) -> None:
141
+ await lock.release()
142
+
143
+ @staticmethod
144
+ async def extend_lock(lock: Lock, seconds: float, *, replace_ttl: bool = False) -> bool:
145
+ if not isfinite(seconds) or seconds <= 0:
146
+ raise ValueError("Lock extension must be positive")
147
+ return await lock.extend(seconds, replace_ttl=replace_ttl)
148
+
149
+ @staticmethod
150
+ async def lock_owned(lock: Lock) -> bool:
151
+ return await lock.owned()
@@ -0,0 +1,11 @@
1
+ """Concrete aliases of native redis-py client classes for Bean injection."""
2
+
3
+ from redis import Redis
4
+ from redis.asyncio import Redis as AsyncRedis
5
+
6
+ RedisClient = Redis
7
+ AsyncRedisClient = AsyncRedis
8
+
9
+ RedisKey = str | bytes
10
+ RedisValue = str | bytes | int | float
11
+ RedisResult = str | bytes
@@ -0,0 +1,72 @@
1
+ """Conditional native Redis clients and application-owned pool lifecycle."""
2
+
3
+ import logging
4
+
5
+ from redis import ConnectionPool
6
+ from redis.asyncio import ConnectionPool as AsyncConnectionPool
7
+
8
+ from pythonboot import auto_configuration, bean, conditional_on_missing_bean, conditional_on_property
9
+ from pythonboot_redis.client import AsyncRedisClient, RedisClient
10
+ from pythonboot_redis.properties import RedisProperties
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @auto_configuration(order=2900, imports=(RedisProperties,))
16
+ @conditional_on_property("pythonboot.redis.enabled", having_value=True, match_if_missing=True)
17
+ class RedisPropertiesAutoConfiguration:
18
+ """Register typed Redis connection configuration before resource factories."""
19
+
20
+
21
+ @auto_configuration(order=3000)
22
+ @conditional_on_property("pythonboot.redis.enabled", having_value=True, match_if_missing=True)
23
+ @conditional_on_property("pythonboot.redis.mode", having_value="sync", match_if_missing=True)
24
+ class RedisSyncPoolAutoConfiguration:
25
+ """Create a pool only when neither an application client nor a pool is available."""
26
+
27
+ @bean(name="redisConnectionPool", destroy_method="disconnect")
28
+ @conditional_on_missing_bean(ConnectionPool, RedisClient, "redisConnectionPool", "redisClient")
29
+ def connection_pool(self, properties: RedisProperties) -> ConnectionPool:
30
+ logger.info("Creating synchronous Redis connection pool with max_connections=%d.", properties.max_connections)
31
+ return ConnectionPool.from_url(properties.connection_url("sync"), **properties.connection_options())
32
+
33
+
34
+ @auto_configuration(order=3000)
35
+ @conditional_on_property("pythonboot.redis.enabled", having_value=True, match_if_missing=True)
36
+ @conditional_on_property("pythonboot.redis.mode", having_value="async")
37
+ class RedisAsyncPoolAutoConfiguration:
38
+ """Create an asynchronous pool with awaited shutdown."""
39
+
40
+ @bean(name="redisAsyncConnectionPool", destroy_method="disconnect")
41
+ @conditional_on_missing_bean(
42
+ AsyncConnectionPool, AsyncRedisClient, "redisAsyncConnectionPool", "redisAsyncClient",
43
+ )
44
+ def connection_pool(self, properties: RedisProperties) -> AsyncConnectionPool:
45
+ logger.info("Creating asynchronous Redis connection pool with max_connections=%d.", properties.max_connections)
46
+ return AsyncConnectionPool.from_url(properties.connection_url("async"), **properties.connection_options())
47
+
48
+
49
+ @auto_configuration(order=3100)
50
+ @conditional_on_property("pythonboot.redis.enabled", having_value=True, match_if_missing=True)
51
+ @conditional_on_property("pythonboot.redis.mode", having_value="sync", match_if_missing=True)
52
+ class RedisSyncClientAutoConfiguration:
53
+ """Register a native synchronous Redis client using the selected pool."""
54
+
55
+ @bean(name="redisClient", destroy_method="close")
56
+ @conditional_on_missing_bean(RedisClient, "redisClient")
57
+ def client(self, pool: ConnectionPool) -> RedisClient:
58
+ logger.info("Creating synchronous Redis client from the managed connection pool.")
59
+ return RedisClient(connection_pool=pool)
60
+
61
+
62
+ @auto_configuration(order=3100)
63
+ @conditional_on_property("pythonboot.redis.enabled", having_value=True, match_if_missing=True)
64
+ @conditional_on_property("pythonboot.redis.mode", having_value="async")
65
+ class RedisAsyncClientAutoConfiguration:
66
+ """Register a native asynchronous Redis client without opening connections during import."""
67
+
68
+ @bean(name="redisAsyncClient", destroy_method="aclose")
69
+ @conditional_on_missing_bean(AsyncRedisClient, "redisAsyncClient")
70
+ def client(self, pool: AsyncConnectionPool) -> AsyncRedisClient:
71
+ logger.info("Creating asynchronous Redis client from the managed connection pool.")
72
+ return AsyncRedisClient(connection_pool=pool)
@@ -0,0 +1,52 @@
1
+ """Connection settings bound through PythonBoot Configuration Properties."""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from pydantic import Field, SecretStr
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+ from pythonboot import configuration_properties
9
+
10
+
11
+ @configuration_properties("pythonboot.redis")
12
+ class RedisProperties(BaseSettings):
13
+ """Native standalone Redis connection options; connections open on first command."""
14
+
15
+ model_config = SettingsConfigDict(env_prefix="PYTHONBOOT_REDIS_", extra="forbid")
16
+
17
+ enabled: bool = True
18
+ mode: Literal["sync", "async"] = "sync"
19
+ url: SecretStr = SecretStr("redis://localhost:6379/0")
20
+ username: str | None = None
21
+ password: SecretStr | None = None
22
+ decode_responses: bool = True
23
+ encoding: str = Field(default="utf-8", min_length=1)
24
+ max_connections: int = Field(default=50, ge=1)
25
+ socket_timeout: float = Field(default=5, gt=0, allow_inf_nan=False)
26
+ socket_connect_timeout: float = Field(default=5, gt=0, allow_inf_nan=False)
27
+ health_check_interval: int = Field(default=30, ge=0)
28
+
29
+ def connection_options(self) -> dict[str, Any]:
30
+ """Return keyword options without exposing secret representations."""
31
+ options: dict[str, Any] = {
32
+ "decode_responses": self.decode_responses,
33
+ "encoding": self.encoding,
34
+ "max_connections": self.max_connections,
35
+ "socket_timeout": self.socket_timeout,
36
+ "socket_connect_timeout": self.socket_connect_timeout,
37
+ "health_check_interval": self.health_check_interval,
38
+ }
39
+ if self.username is not None:
40
+ options["username"] = self.username
41
+ if self.password is not None:
42
+ options["password"] = self.password.get_secret_value()
43
+ return options
44
+
45
+ def connection_url(self, mode: Literal["sync", "async"]) -> str:
46
+ """Validate selection before constructing a default connection pool."""
47
+ if not self.enabled or self.mode != mode:
48
+ raise ValueError("Redis enabled and mode must be selected in application YAML")
49
+ url = self.url.get_secret_value()
50
+ if not url.strip():
51
+ raise ValueError("pythonboot.redis.url must not be empty")
52
+ return url
@@ -0,0 +1,158 @@
1
+ """Synchronous static Redis operations resolved against the current application context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from datetime import timedelta
7
+ from math import isfinite
8
+
9
+ from redis.lock import Lock
10
+
11
+ from pythonboot import ContextUtil
12
+ from pythonboot_redis.client import RedisClient, RedisKey, RedisResult, RedisValue
13
+
14
+
15
+ def validate_lock_options(timeout: float, blocking_timeout: float, sleep: float) -> None:
16
+ """Require bounded acquisition and a positive finite lease."""
17
+ if not isfinite(timeout) or timeout <= 0:
18
+ raise ValueError("Lock timeout must be finite and positive")
19
+ if not isfinite(blocking_timeout) or blocking_timeout < 0:
20
+ raise ValueError("Lock blocking_timeout must be finite and non-negative")
21
+ if not isfinite(sleep) or sleep <= 0:
22
+ raise ValueError("Lock sleep must be finite and positive")
23
+
24
+
25
+ class RedisUtil:
26
+ """Convenience methods backed by native RedisClient Beans, never a global client."""
27
+
28
+ @staticmethod
29
+ def get_client(*, name: str | None = None, qualifier: str | None = None) -> RedisClient:
30
+ return ContextUtil.get_bean(RedisClient, name=name, qualifier=qualifier)
31
+
32
+ @staticmethod
33
+ def get(key: RedisKey, *, name: str | None = None) -> RedisResult | None:
34
+ return RedisUtil.get_client(name=name).get(key)
35
+
36
+ @staticmethod
37
+ def set(
38
+ key: RedisKey, value: RedisValue, *, ex: int | timedelta | None = None,
39
+ px: int | timedelta | None = None, nx: bool = False, xx: bool = False,
40
+ keepttl: bool = False, name: str | None = None,
41
+ ) -> bool | None:
42
+ return RedisUtil.get_client(name=name).set(key, value, ex=ex, px=px, nx=nx, xx=xx, keepttl=keepttl)
43
+
44
+ @staticmethod
45
+ def delete(*keys: RedisKey, name: str | None = None) -> int:
46
+ return RedisUtil.get_client(name=name).delete(*keys)
47
+
48
+ @staticmethod
49
+ def exists(*keys: RedisKey, name: str | None = None) -> int:
50
+ return RedisUtil.get_client(name=name).exists(*keys)
51
+
52
+ @staticmethod
53
+ def expire(key: RedisKey, seconds: int | timedelta, *, name: str | None = None) -> bool:
54
+ return RedisUtil.get_client(name=name).expire(key, seconds)
55
+
56
+ @staticmethod
57
+ def ttl(key: RedisKey, *, name: str | None = None) -> int:
58
+ return RedisUtil.get_client(name=name).ttl(key)
59
+
60
+ @staticmethod
61
+ def incr(key: RedisKey, amount: int = 1, *, name: str | None = None) -> int:
62
+ return RedisUtil.get_client(name=name).incr(key, amount)
63
+
64
+ @staticmethod
65
+ def decr(key: RedisKey, amount: int = 1, *, name: str | None = None) -> int:
66
+ return RedisUtil.get_client(name=name).decr(key, amount)
67
+
68
+ @staticmethod
69
+ def hset(key: RedisKey, mapping: Mapping[RedisKey, RedisValue], *, name: str | None = None) -> int:
70
+ return RedisUtil.get_client(name=name).hset(key, mapping=mapping)
71
+
72
+ @staticmethod
73
+ def hget(key: RedisKey, field: RedisKey, *, name: str | None = None) -> RedisResult | None:
74
+ return RedisUtil.get_client(name=name).hget(key, field)
75
+
76
+ @staticmethod
77
+ def hgetall(key: RedisKey, *, name: str | None = None) -> dict[RedisResult, RedisResult]:
78
+ return RedisUtil.get_client(name=name).hgetall(key)
79
+
80
+ @staticmethod
81
+ def hdel(key: RedisKey, *fields: RedisKey, name: str | None = None) -> int:
82
+ return RedisUtil.get_client(name=name).hdel(key, *fields)
83
+
84
+ @staticmethod
85
+ def lpush(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
86
+ return RedisUtil.get_client(name=name).lpush(key, *values)
87
+
88
+ @staticmethod
89
+ def rpush(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
90
+ return RedisUtil.get_client(name=name).rpush(key, *values)
91
+
92
+ @staticmethod
93
+ def lpop(key: RedisKey, *, name: str | None = None) -> RedisResult | None:
94
+ return RedisUtil.get_client(name=name).lpop(key)
95
+
96
+ @staticmethod
97
+ def rpop(key: RedisKey, *, name: str | None = None) -> RedisResult | None:
98
+ return RedisUtil.get_client(name=name).rpop(key)
99
+
100
+ @staticmethod
101
+ def lrange(key: RedisKey, start: int = 0, end: int = -1, *, name: str | None = None) -> list[RedisResult]:
102
+ return RedisUtil.get_client(name=name).lrange(key, start, end)
103
+
104
+ @staticmethod
105
+ def llen(key: RedisKey, *, name: str | None = None) -> int:
106
+ return RedisUtil.get_client(name=name).llen(key)
107
+
108
+ @staticmethod
109
+ def sadd(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
110
+ return RedisUtil.get_client(name=name).sadd(key, *values)
111
+
112
+ @staticmethod
113
+ def smembers(key: RedisKey, *, name: str | None = None) -> set[RedisResult]:
114
+ return RedisUtil.get_client(name=name).smembers(key)
115
+
116
+ @staticmethod
117
+ def srem(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
118
+ return RedisUtil.get_client(name=name).srem(key, *values)
119
+
120
+ @staticmethod
121
+ def zadd(key: RedisKey, mapping: Mapping[RedisKey, float], *, name: str | None = None) -> int:
122
+ return RedisUtil.get_client(name=name).zadd(key, mapping)
123
+
124
+ @staticmethod
125
+ def zrange(key: RedisKey, start: int = 0, end: int = -1, *, name: str | None = None) -> list[RedisResult]:
126
+ return RedisUtil.get_client(name=name).zrange(key, start, end)
127
+
128
+ @staticmethod
129
+ def zrem(key: RedisKey, *values: RedisValue, name: str | None = None) -> int:
130
+ return RedisUtil.get_client(name=name).zrem(key, *values)
131
+
132
+ @staticmethod
133
+ def get_lock(
134
+ key: RedisKey, *, timeout: float = 30, blocking_timeout: float = 5,
135
+ sleep: float = 0.1, thread_local: bool = True, name: str | None = None,
136
+ ) -> Lock:
137
+ validate_lock_options(timeout, blocking_timeout, sleep)
138
+ return RedisUtil.get_client(name=name).lock(
139
+ key, timeout=timeout, blocking_timeout=blocking_timeout, sleep=sleep, thread_local=thread_local,
140
+ )
141
+
142
+ @staticmethod
143
+ def acquire_lock(lock: Lock, *, blocking: bool = True) -> bool:
144
+ return lock.acquire(blocking=blocking)
145
+
146
+ @staticmethod
147
+ def release_lock(lock: Lock) -> None:
148
+ lock.release()
149
+
150
+ @staticmethod
151
+ def extend_lock(lock: Lock, seconds: float, *, replace_ttl: bool = False) -> bool:
152
+ if not isfinite(seconds) or seconds <= 0:
153
+ raise ValueError("Lock extension must be positive")
154
+ return lock.extend(seconds, replace_ttl=replace_ttl)
155
+
156
+ @staticmethod
157
+ def lock_owned(lock: Lock) -> bool:
158
+ return lock.owned()
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.5
2
+ Name: pythonboot-redis
3
+ Version: 0.1.0
4
+ Summary: Native redis-py clients, utilities and distributed locks for PythonBoot
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: pydantic-settings<3,>=2.7
7
+ Requires-Dist: pydantic<3,>=2.10
8
+ Requires-Dist: pythonboot<0.2,>=0.1.0
9
+ Requires-Dist: redis<8,>=5.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # pythonboot-redis
13
+
14
+ PythonBoot 的 redis-py 集成实现包。分发名为 `pythonboot-redis`,导入名为
15
+ `pythonboot_redis`;应用推荐安装依赖聚合包 `pythonboot-starter-redis`。
16
+
17
+ - Configuration Properties 绑定 `pythonboot.redis` 并注册 `RedisProperties` Bean。
18
+ - 默认同步模式,可选择异步;自动注入原生 `RedisClient` / `AsyncRedisClient`。
19
+ - 客户端类别名直接指向 redis-py 的 Redis 类,没有模板或代理包装。
20
+ - `RedisUtil` / `AsyncRedisUtil` 提供字符串、TTL、计数器、Hash、List、Set、有序集合操作。
21
+ - 原生分布式锁支持获取、释放、续期及归属判断;不通过直接删除键解锁。
22
+ - 工具类从当前 ContextVar 绑定的容器解析客户端,不持有全局客户端;支持命名 Bean。
23
+ - 默认池和客户端接入容器生命周期,异步关闭由容器等待。应用 Bean 优先。
24
+ - 创建对象不连接 Redis;首次命令才连接。异常按原生语义抛出,不静默返回默认值。
25
+
26
+ ```yaml
27
+ pythonboot:
28
+ redis:
29
+ url: redis://localhost:6379/0
30
+ mode: sync
31
+ decode_responses: true
32
+ ```
33
+
34
+ ```python
35
+ from pythonboot import autowired, service
36
+ from pythonboot_redis import RedisClient, RedisUtil
37
+
38
+ @service
39
+ class CacheService:
40
+ redis_client: RedisClient = autowired()
41
+
42
+ def save(self, value: str) -> None:
43
+ self.redis_client.set("example:value", value, ex=60)
44
+
45
+ RedisUtil.set("example:value", "hello", ex=60)
46
+ value = RedisUtil.get("example:value")
47
+ RedisUtil.rpush("example:list", "a", "b")
48
+ values = RedisUtil.lrange("example:list")
49
+ client = RedisUtil.get_client()
50
+ ```
51
+
52
+ 静态调用需要在已绑定容器的请求/生命周期中,独立任务使用 `ContextUtil.use(context)`。
53
+ 异步模式注入 `AsyncRedisClient`,使用 `await AsyncRedisUtil.get(...)`;
54
+ 不要在异步方法中直接执行同步客户端命令。
55
+ 工具锁默认租期 30 秒、获取等待 5 秒;每次操作创建独立锁对象,成功获取后在 finally 中释放。
56
+ 没有自动续期,超过租期需要业务显式续期;释放过期或非本方持有的锁会抛出原生异常。
57
+
58
+ 完整配置、公共 API 和锁示例见 [中文文档](../../viteDocs/docs/zh/redis.md)、
59
+ [English documentation](../../viteDocs/docs/en/redis.md) 和 [示例](../../examples/redis/README.md)。
60
+
61
+ ## English
62
+
63
+ The implementation distribution is `pythonboot-redis`; import `pythonboot_redis`.
64
+ Install `pythonboot-starter-redis` in applications. Configuration Properties bind `pythonboot.redis`.
65
+ Native client aliases support concrete-class Bean injection; sync is the default, async is optional.
66
+ Static utilities resolve the current application context and provide common commands and native locks.
67
+ Default clients and pools are lifecycle-managed without connecting during import or construction.
68
+ Application Beans override defaults; providers declare cleanup for custom resources.
69
+ Locks use native ownership tokens, bounded acquisition, explicit renewal, and atomic release.
70
+ There is no automatic renewal. Use a distinct lock object per task or thread and release in finally.
71
+ Standalone tasks bind their container through `ContextUtil.use(context)`.
72
+
73
+
74
+ 完整分层应用见 [全量 Starter 示例](../../example/README.md),包含全部 Starter、PostgreSQL/Redis 和同步/异步 HTTP 用法。
75
+ See the [complete Starter application](../../example/README.md) for all Starters and sync/async HTTP examples.
@@ -0,0 +1,10 @@
1
+ pythonboot_redis/__init__.py,sha256=NeQEIVASy-MVg4K42CPR9SZIg4K3Q48gwvAQDXq6AY8,402
2
+ pythonboot_redis/async_util.py,sha256=PUu8_Nm_dE1DJb4kcS_PQlBM5m2Tio_0EAwjk8AzFF0,6724
3
+ pythonboot_redis/client.py,sha256=zxxdPj2M3rE9MOphM9Lis4Nf0Ka_UiGmB1iOnJo3UUc,288
4
+ pythonboot_redis/configuration.py,sha256=3Z1BUqruGzHlamn81WgFsKZag-6Oy8BZJi7ZUrli8Is,3727
5
+ pythonboot_redis/properties.py,sha256=lbJirAjMg5y6-0K2HPtb5Zi_-r4NUdhAkkivOUxS-0Y,2245
6
+ pythonboot_redis/util.py,sha256=fdaBaixLttAslMMUOUnITSeIyZdDx4-gAHBI7cw_tcY,6447
7
+ pythonboot_redis-0.1.0.dist-info/METADATA,sha256=kFb6eTxpYFCS_exFQ35KkkTQ5p3W2fJZAI-RwXzZQaU,3780
8
+ pythonboot_redis-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ pythonboot_redis-0.1.0.dist-info/entry_points.txt,sha256=XUBWJQjKO-h0PhcPwYGD0mptFsjWZWxrgyrJJ1YPIQE,66
10
+ pythonboot_redis-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pythonboot.autoconfigure]
2
+ redis = pythonboot_redis.configuration