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.
Files changed (33) hide show
  1. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/CHANGELOG.md +14 -0
  2. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/PKG-INFO +6 -1
  3. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/README.md +5 -0
  4. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/__init__.py +1 -1
  5. redis_client_kit-0.2.0/redis_client_kit/__version__.py +1 -0
  6. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/factory.py +2 -2
  7. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/lifecycle.py +17 -3
  8. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/lifecycle.py +17 -3
  9. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/utils.py +32 -6
  10. redis_client_kit-0.1.4/redis_client_kit/__version__.py +0 -1
  11. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/.gitignore +0 -0
  12. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/LICENSE +0 -0
  13. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/pyproject.toml +0 -0
  14. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/__init__.py +0 -0
  15. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/instrumented.py +0 -0
  16. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/aio/types.py +0 -0
  17. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/config.py +0 -0
  18. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/metrics/__init__.py +0 -0
  19. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/metrics/_deps.py +0 -0
  20. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/metrics/redis.py +0 -0
  21. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/protocols.py +0 -0
  22. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/providers/__init__.py +0 -0
  23. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/providers/_deps.py +0 -0
  24. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/providers/redis.py +0 -0
  25. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/providers/utils.py +0 -0
  26. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/py.typed +0 -0
  27. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/settings/__init__.py +0 -0
  28. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/settings/_deps.py +0 -0
  29. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/settings/redis.py +0 -0
  30. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/__init__.py +0 -0
  31. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/factory.py +0 -0
  32. {redis_client_kit-0.1.4 → redis_client_kit-0.2.0}/redis_client_kit/sync/instrumented.py +0 -0
  33. {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.1.4
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.1.4"
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
- return all(bool(v) for v in result.values())
62
+ healthy = all(bool(v) for v in result.values())
63
+ else:
64
+ healthy = bool(result)
54
65
 
55
- return bool(result)
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
- return all(bool(v) for v in result.values())
54
+ healthy = all(bool(v) for v in result.values())
55
+ else:
56
+ healthy = bool(result)
46
57
 
47
- return bool(result)
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
- def build_redis_retry(settings: RedisSettingsProtocol) -> Retry:
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 Retry(
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 Retry(backoff=NoBackoff(), retries=0)
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"