redis-client-kit 0.1.4__tar.gz → 0.2.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.
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/CHANGELOG.md +14 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/PKG-INFO +6 -1
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/README.md +5 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/__init__.py +1 -1
- redis_client_kit-0.2.0/redis_client_kit/__version__.py +1 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/factory.py +2 -2
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/lifecycle.py +17 -3
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/lifecycle.py +17 -3
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/utils.py +32 -6
- redis_client_kit-0.1.4/redis_client_kit/__version__.py +0 -1
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/.gitignore +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/LICENSE +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/pyproject.toml +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/__init__.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/instrumented.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/types.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/config.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/metrics/__init__.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/metrics/_deps.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/metrics/redis.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/protocols.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/providers/__init__.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/providers/_deps.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/providers/redis.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/providers/utils.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/py.typed +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/settings/__init__.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/settings/_deps.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/settings/redis.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/__init__.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/factory.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/instrumented.py +0 -0
- {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/types.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/bedrock-python/redis-client-kit/compare/redis-client-kit-v0.1.5...redis-client-kit-v0.2.0) (2026-09-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add an opt-in write probe to the health checks ([#32](https://github.com/bedrock-python/redis-client-kit/issues/32)) ([aabafd1](https://github.com/bedrock-python/redis-client-kit/commit/aabafd1922e3e0cf1b525570f98cdf0f71a2856e))
|
|
9
|
+
|
|
10
|
+
## [0.1.5](https://github.com/bedrock-python/redis-client-kit/compare/redis-client-kit-v0.1.4...redis-client-kit-v0.1.5) (2026-09-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* hand redis.asyncio.Redis the async Retry, not the sync one ([#29](https://github.com/bedrock-python/redis-client-kit/issues/29)) ([e8ee302](https://github.com/bedrock-python/redis-client-kit/commit/e8ee3024b8e402cdb80c531b9284d513111116ea))
|
|
16
|
+
|
|
3
17
|
## [0.1.4](https://github.com/bedrock-python/redis-client-kit/compare/redis-client-kit-v0.1.3...redis-client-kit-v0.1.4) (2026-09-07)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: redis-client-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Redis client with optional Pydantic, Prometheus, and Dishka support
|
|
5
5
|
Project-URL: Repository, https://github.com/bedrock-python/redis-client-kit
|
|
6
6
|
Project-URL: Documentation, https://bedrock-python.github.io/redis-client-kit/
|
|
@@ -491,6 +491,11 @@ from redis_client_kit import check_async_redis_health
|
|
|
491
491
|
is_healthy = await check_async_redis_health(client)
|
|
492
492
|
if is_healthy:
|
|
493
493
|
print("Redis is ready!")
|
|
494
|
+
|
|
495
|
+
# PING alone says healthy for a read-only replica and for a primary that is out of memory
|
|
496
|
+
# under noeviction. write_key adds a SET <key> 1 EX 60 after the ping, for a service that
|
|
497
|
+
# needs Redis for more than reads.
|
|
498
|
+
is_healthy = await check_async_redis_health(client, write_key="myapp:health")
|
|
494
499
|
```
|
|
495
500
|
|
|
496
501
|
### Graceful Shutdown
|
|
@@ -251,6 +251,11 @@ from redis_client_kit import check_async_redis_health
|
|
|
251
251
|
is_healthy = await check_async_redis_health(client)
|
|
252
252
|
if is_healthy:
|
|
253
253
|
print("Redis is ready!")
|
|
254
|
+
|
|
255
|
+
# PING alone says healthy for a read-only replica and for a primary that is out of memory
|
|
256
|
+
# under noeviction. write_key adds a SET <key> 1 EX 60 after the ping, for a service that
|
|
257
|
+
# needs Redis for more than reads.
|
|
258
|
+
is_healthy = await check_async_redis_health(client, write_key="myapp:health")
|
|
254
259
|
```
|
|
255
260
|
|
|
256
261
|
### Graceful Shutdown
|
|
@@ -24,7 +24,7 @@ from .utils import build_base_redis_kwargs, build_redis_retry, parse_redis_url_n
|
|
|
24
24
|
try:
|
|
25
25
|
__version__ = version("redis-client-kit")
|
|
26
26
|
except PackageNotFoundError: # pragma: no cover
|
|
27
|
-
__version__ = "0.
|
|
27
|
+
__version__ = "0.2.0"
|
|
28
28
|
|
|
29
29
|
# ruff: noqa: RUF022
|
|
30
30
|
__all__ = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -50,7 +50,7 @@ def _create_async_single_client(
|
|
|
50
50
|
extra={"host": settings.connection.host, "port": settings.connection.port},
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
-
client_kwargs: dict[str, Any] = build_base_redis_kwargs(settings)
|
|
53
|
+
client_kwargs: dict[str, Any] = build_base_redis_kwargs(settings, asyncio=True)
|
|
54
54
|
client_kwargs.update(
|
|
55
55
|
{
|
|
56
56
|
"host": settings.connection.host,
|
|
@@ -87,7 +87,7 @@ def _create_async_cluster_client(
|
|
|
87
87
|
},
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
cluster_kwargs: dict[str, Any] = build_base_redis_kwargs(settings)
|
|
90
|
+
cluster_kwargs: dict[str, Any] = build_base_redis_kwargs(settings, asyncio=True)
|
|
91
91
|
cluster_kwargs.update(
|
|
92
92
|
{
|
|
93
93
|
"startup_nodes": startup_nodes,
|
|
@@ -8,6 +8,7 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
from redis.exceptions import RedisClusterException, RedisError
|
|
10
10
|
|
|
11
|
+
from ..utils import WRITE_PROBE_TTL_S
|
|
11
12
|
from .types import AsyncRedisClient
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
@@ -36,11 +37,19 @@ async def close_async_redis_client(client: AsyncRedisClient) -> None:
|
|
|
36
37
|
logger.info("Closed Redis client")
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
async def check_async_redis_health(client: AsyncRedisClient) -> bool:
|
|
40
|
+
async def check_async_redis_health(client: AsyncRedisClient, *, write_key: str | None = None) -> bool:
|
|
40
41
|
"""Check async Redis connection health.
|
|
41
42
|
|
|
43
|
+
Pings the server, and with ``write_key`` writes that key as well. A ping alone is a
|
|
44
|
+
liveness answer: a read-only replica and a server at ``maxmemory`` under ``noeviction``
|
|
45
|
+
both reply PONG and refuse every write. The write probe turns that into a readiness
|
|
46
|
+
answer for a service that needs Redis for more than reads.
|
|
47
|
+
|
|
42
48
|
Args:
|
|
43
49
|
client: Redis client (single or cluster) to check
|
|
50
|
+
write_key: Key to ``SET`` after the ping, expiring after ``WRITE_PROBE_TTL_S``
|
|
51
|
+
seconds. None, the default, pings only. Prefix it yourself; this package
|
|
52
|
+
applies no key prefix.
|
|
44
53
|
|
|
45
54
|
Returns:
|
|
46
55
|
True if Redis is healthy, False otherwise
|
|
@@ -50,9 +59,12 @@ async def check_async_redis_health(client: AsyncRedisClient) -> bool:
|
|
|
50
59
|
|
|
51
60
|
# Redis Cluster ping returns dict[str, bool] (node_id -> success); single node returns bool.
|
|
52
61
|
if isinstance(result, dict) and result and all(isinstance(k, str) for k in result):
|
|
53
|
-
|
|
62
|
+
healthy = all(bool(v) for v in result.values())
|
|
63
|
+
else:
|
|
64
|
+
healthy = bool(result)
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
if healthy and write_key is not None:
|
|
67
|
+
healthy = bool(await client.set(write_key, "1", ex=WRITE_PROBE_TTL_S))
|
|
56
68
|
except (RedisError, RedisClusterException, OSError, ConnectionError, TimeoutError) as e:
|
|
57
69
|
logger.warning(
|
|
58
70
|
"Async Redis health check failed",
|
|
@@ -66,6 +78,8 @@ async def check_async_redis_health(client: AsyncRedisClient) -> bool:
|
|
|
66
78
|
# We return False to indicate unhealthy state but log the full exception for debugging.
|
|
67
79
|
logger.exception("Unexpected error during Redis health check")
|
|
68
80
|
return False
|
|
81
|
+
else:
|
|
82
|
+
return healthy
|
|
69
83
|
|
|
70
84
|
|
|
71
85
|
__all__ = ["check_async_redis_health", "close_async_redis_client"]
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from redis.exceptions import RedisClusterException, RedisError
|
|
9
9
|
|
|
10
|
+
from ..utils import WRITE_PROBE_TTL_S
|
|
10
11
|
from .types import SyncRedisClient
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
@@ -28,11 +29,19 @@ def close_redis_client(client: SyncRedisClient) -> None:
|
|
|
28
29
|
logger.info("Closed Redis client")
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
def check_redis_health(client: SyncRedisClient) -> bool:
|
|
32
|
+
def check_redis_health(client: SyncRedisClient, *, write_key: str | None = None) -> bool:
|
|
32
33
|
"""Check sync Redis connection health.
|
|
33
34
|
|
|
35
|
+
Pings the server, and with ``write_key`` writes that key as well. A ping alone is a
|
|
36
|
+
liveness answer: a read-only replica and a server at ``maxmemory`` under ``noeviction``
|
|
37
|
+
both reply PONG and refuse every write. The write probe turns that into a readiness
|
|
38
|
+
answer for a service that needs Redis for more than reads.
|
|
39
|
+
|
|
34
40
|
Args:
|
|
35
41
|
client: Redis client (single or cluster) to check
|
|
42
|
+
write_key: Key to ``SET`` after the ping, expiring after ``WRITE_PROBE_TTL_S``
|
|
43
|
+
seconds. None, the default, pings only. Prefix it yourself; this package
|
|
44
|
+
applies no key prefix.
|
|
36
45
|
|
|
37
46
|
Returns:
|
|
38
47
|
True if Redis is healthy, False otherwise
|
|
@@ -42,9 +51,12 @@ def check_redis_health(client: SyncRedisClient) -> bool:
|
|
|
42
51
|
|
|
43
52
|
# Redis Cluster ping returns dict[str, bool] (node_id -> success); single node returns bool.
|
|
44
53
|
if isinstance(result, dict) and result and all(isinstance(k, str) for k in result):
|
|
45
|
-
|
|
54
|
+
healthy = all(bool(v) for v in result.values())
|
|
55
|
+
else:
|
|
56
|
+
healthy = bool(result)
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
if healthy and write_key is not None:
|
|
59
|
+
healthy = bool(client.set(write_key, "1", ex=WRITE_PROBE_TTL_S))
|
|
48
60
|
except (RedisError, RedisClusterException, OSError, ConnectionError, TimeoutError) as e:
|
|
49
61
|
logger.warning(
|
|
50
62
|
"Redis health check failed",
|
|
@@ -58,6 +70,8 @@ def check_redis_health(client: SyncRedisClient) -> bool:
|
|
|
58
70
|
# We return False to indicate unhealthy state but log the full exception for debugging.
|
|
59
71
|
logger.exception("Unexpected error during Redis health check")
|
|
60
72
|
return False
|
|
73
|
+
else:
|
|
74
|
+
return healthy
|
|
61
75
|
|
|
62
76
|
|
|
63
77
|
__all__ = ["check_redis_health", "close_redis_client"]
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
import base64
|
|
4
4
|
import binascii
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from typing import Literal, overload
|
|
6
7
|
from urllib.parse import urlparse
|
|
7
8
|
|
|
9
|
+
from redis.asyncio.retry import Retry as AsyncRetry
|
|
8
10
|
from redis.backoff import ExponentialBackoff, NoBackoff
|
|
9
11
|
from redis.retry import Retry
|
|
10
12
|
|
|
11
13
|
from .config import RedisSettingsProtocol
|
|
12
14
|
|
|
15
|
+
WRITE_PROBE_TTL_S = 60
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
def parse_redis_url_node(node: str) -> tuple[str, int]:
|
|
15
19
|
"""Parse Redis node from host:port, [ipv6]:port or redis:// URL string."""
|
|
@@ -34,8 +38,12 @@ def parse_redis_url_node(node: str) -> tuple[str, int]:
|
|
|
34
38
|
return host, port
|
|
35
39
|
|
|
36
40
|
|
|
37
|
-
def build_base_redis_kwargs(settings: RedisSettingsProtocol) -> dict[str, object]:
|
|
38
|
-
"""Build base Redis client keyword arguments.
|
|
41
|
+
def build_base_redis_kwargs(settings: RedisSettingsProtocol, *, asyncio: bool = False) -> dict[str, object]:
|
|
42
|
+
"""Build base Redis client keyword arguments.
|
|
43
|
+
|
|
44
|
+
``asyncio=True`` builds them for a ``redis.asyncio`` client, whose ``retry`` has to be
|
|
45
|
+
``redis.asyncio.retry.Retry``.
|
|
46
|
+
"""
|
|
39
47
|
if settings.ssl.enabled:
|
|
40
48
|
if settings.ssl.ca_certs:
|
|
41
49
|
_validate_pem_format(settings.ssl.ca_certs, "CERTIFICATE")
|
|
@@ -52,7 +60,7 @@ def build_base_redis_kwargs(settings: RedisSettingsProtocol) -> dict[str, object
|
|
|
52
60
|
"socket_keepalive": settings.pool.socket_keepalive,
|
|
53
61
|
"socket_keepalive_options": settings.pool.socket_keepalive_options,
|
|
54
62
|
"health_check_interval": settings.health_check_interval,
|
|
55
|
-
"retry": build_redis_retry(settings),
|
|
63
|
+
"retry": build_redis_retry(settings, asyncio=asyncio),
|
|
56
64
|
"decode_responses": settings.response.decode_responses,
|
|
57
65
|
"encoding": settings.response.encoding,
|
|
58
66
|
"client_name": settings.connection.client_name,
|
|
@@ -72,22 +80,39 @@ def build_base_redis_kwargs(settings: RedisSettingsProtocol) -> dict[str, object
|
|
|
72
80
|
return kwargs
|
|
73
81
|
|
|
74
82
|
|
|
75
|
-
|
|
83
|
+
@overload
|
|
84
|
+
def build_redis_retry(settings: RedisSettingsProtocol, *, asyncio: Literal[False] = ...) -> Retry: ...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@overload
|
|
88
|
+
def build_redis_retry(settings: RedisSettingsProtocol, *, asyncio: Literal[True]) -> AsyncRetry: ...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@overload
|
|
92
|
+
def build_redis_retry(settings: RedisSettingsProtocol, *, asyncio: bool) -> Retry | AsyncRetry: ...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_redis_retry(settings: RedisSettingsProtocol, *, asyncio: bool = False) -> Retry | AsyncRetry:
|
|
76
96
|
"""Build Redis Retry object from settings.
|
|
77
97
|
|
|
78
98
|
Retries are off unless both ``retry.enabled`` and ``retry.max_attempts`` are set. Off is
|
|
79
99
|
still a ``Retry``, with zero retries: handed nothing, redis-py retries on its own (three
|
|
80
100
|
times since 6.0, ten since 8.0, with jittered backoff).
|
|
101
|
+
|
|
102
|
+
``asyncio=True`` builds ``redis.asyncio.retry.Retry``, the one a ``redis.asyncio`` client
|
|
103
|
+
needs: the sync class's ``call_with_retry`` does not await, so on an async client it
|
|
104
|
+
never retries.
|
|
81
105
|
"""
|
|
106
|
+
retry_class = AsyncRetry if asyncio else Retry
|
|
82
107
|
if settings.retry.enabled and settings.retry.max_attempts:
|
|
83
|
-
return
|
|
108
|
+
return retry_class(
|
|
84
109
|
backoff=ExponentialBackoff(
|
|
85
110
|
cap=settings.retry.backoff_cap,
|
|
86
111
|
base=settings.retry.backoff_base,
|
|
87
112
|
),
|
|
88
113
|
retries=settings.retry.max_attempts,
|
|
89
114
|
)
|
|
90
|
-
return
|
|
115
|
+
return retry_class(backoff=NoBackoff(), retries=0)
|
|
91
116
|
|
|
92
117
|
|
|
93
118
|
def _validate_pem_format(path: str | Path, file_type: str) -> None:
|
|
@@ -120,6 +145,7 @@ def mask_redis_kwargs(kwargs: dict[str, object]) -> dict[str, object]:
|
|
|
120
145
|
|
|
121
146
|
|
|
122
147
|
__all__ = [
|
|
148
|
+
"WRITE_PROBE_TTL_S",
|
|
123
149
|
"build_base_redis_kwargs",
|
|
124
150
|
"build_redis_retry",
|
|
125
151
|
"mask_redis_kwargs",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.4"
|
|
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
|