redis 6.2.0__tar.gz → 6.4.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 (156) hide show
  1. {redis-6.2.0 → redis-6.4.0}/PKG-INFO +4 -4
  2. {redis-6.2.0 → redis-6.4.0}/README.md +3 -3
  3. {redis-6.2.0 → redis-6.4.0}/redis/__init__.py +3 -2
  4. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/helpers.py +2 -1
  5. {redis-6.2.0 → redis-6.4.0}/redis/asyncio/cluster.py +12 -14
  6. {redis-6.2.0 → redis-6.4.0}/redis/asyncio/connection.py +13 -5
  7. {redis-6.2.0 → redis-6.4.0}/redis/asyncio/retry.py +14 -35
  8. {redis-6.2.0 → redis-6.4.0}/redis/asyncio/sentinel.py +33 -18
  9. {redis-6.2.0 → redis-6.4.0}/redis/backoff.py +1 -1
  10. {redis-6.2.0 → redis-6.4.0}/redis/client.py +3 -7
  11. {redis-6.2.0 → redis-6.4.0}/redis/cluster.py +12 -5
  12. {redis-6.2.0 → redis-6.4.0}/redis/commands/core.py +74 -12
  13. redis-6.4.0/redis/commands/json/_util.py +5 -0
  14. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/aggregation.py +3 -3
  15. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/commands.py +3 -3
  16. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/field.py +4 -4
  17. {redis-6.2.0 → redis-6.4.0}/redis/commands/sentinel.py +42 -12
  18. {redis-6.2.0 → redis-6.4.0}/redis/commands/vectorset/commands.py +7 -0
  19. {redis-6.2.0 → redis-6.4.0}/redis/connection.py +19 -16
  20. {redis-6.2.0 → redis-6.4.0}/redis/event.py +4 -4
  21. {redis-6.2.0 → redis-6.4.0}/redis/exceptions.py +7 -1
  22. {redis-6.2.0 → redis-6.4.0}/redis/retry.py +36 -18
  23. {redis-6.2.0 → redis-6.4.0}/redis/sentinel.py +30 -15
  24. {redis-6.2.0 → redis-6.4.0}/redis/utils.py +7 -3
  25. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/compat.py +0 -6
  26. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/conftest.py +1 -2
  27. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_cluster.py +2 -1
  28. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_commands.py +247 -0
  29. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_connection.py +1 -1
  30. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_connection_pool.py +7 -5
  31. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_pipeline.py +3 -1
  32. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_pubsub.py +3 -1
  33. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_search.py +178 -0
  34. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_sentinel.py +85 -6
  35. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_sentinel_managed_connection.py +2 -2
  36. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_vsets.py +15 -0
  37. {redis-6.2.0 → redis-6.4.0}/tests/test_cluster.py +6 -2
  38. {redis-6.2.0 → redis-6.4.0}/tests/test_cluster_transaction.py +2 -2
  39. {redis-6.2.0 → redis-6.4.0}/tests/test_commands.py +246 -0
  40. {redis-6.2.0 → redis-6.4.0}/tests/test_connection.py +3 -3
  41. {redis-6.2.0 → redis-6.4.0}/tests/test_connection_pool.py +10 -7
  42. {redis-6.2.0 → redis-6.4.0}/tests/test_credentials.py +5 -5
  43. redis-6.4.0/tests/test_max_connections_error.py +112 -0
  44. {redis-6.2.0 → redis-6.4.0}/tests/test_retry.py +17 -5
  45. {redis-6.2.0 → redis-6.4.0}/tests/test_search.py +887 -2
  46. {redis-6.2.0 → redis-6.4.0}/tests/test_sentinel.py +82 -6
  47. redis-6.4.0/tests/test_sentinel_managed_connection.py +34 -0
  48. {redis-6.2.0 → redis-6.4.0}/tests/test_vsets.py +15 -0
  49. redis-6.2.0/redis/commands/json/_util.py +0 -3
  50. {redis-6.2.0 → redis-6.4.0}/.gitignore +0 -0
  51. {redis-6.2.0 → redis-6.4.0}/LICENSE +0 -0
  52. {redis-6.2.0 → redis-6.4.0}/dev_requirements.txt +0 -0
  53. {redis-6.2.0 → redis-6.4.0}/pyproject.toml +0 -0
  54. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/__init__.py +0 -0
  55. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/base.py +0 -0
  56. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/commands.py +0 -0
  57. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/encoders.py +0 -0
  58. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/hiredis.py +0 -0
  59. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/resp2.py +0 -0
  60. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/resp3.py +0 -0
  61. {redis-6.2.0 → redis-6.4.0}/redis/_parsers/socket.py +0 -0
  62. {redis-6.2.0 → redis-6.4.0}/redis/asyncio/__init__.py +0 -0
  63. {redis-6.2.0 → redis-6.4.0}/redis/asyncio/client.py +0 -0
  64. {redis-6.2.0 → redis-6.4.0}/redis/asyncio/lock.py +0 -0
  65. {redis-6.2.0 → redis-6.4.0}/redis/asyncio/utils.py +0 -0
  66. {redis-6.2.0 → redis-6.4.0}/redis/auth/__init__.py +0 -0
  67. {redis-6.2.0 → redis-6.4.0}/redis/auth/err.py +0 -0
  68. {redis-6.2.0 → redis-6.4.0}/redis/auth/idp.py +0 -0
  69. {redis-6.2.0 → redis-6.4.0}/redis/auth/token.py +0 -0
  70. {redis-6.2.0 → redis-6.4.0}/redis/auth/token_manager.py +0 -0
  71. {redis-6.2.0 → redis-6.4.0}/redis/cache.py +0 -0
  72. {redis-6.2.0 → redis-6.4.0}/redis/commands/__init__.py +0 -0
  73. {redis-6.2.0 → redis-6.4.0}/redis/commands/bf/__init__.py +0 -0
  74. {redis-6.2.0 → redis-6.4.0}/redis/commands/bf/commands.py +0 -0
  75. {redis-6.2.0 → redis-6.4.0}/redis/commands/bf/info.py +0 -0
  76. {redis-6.2.0 → redis-6.4.0}/redis/commands/cluster.py +0 -0
  77. {redis-6.2.0 → redis-6.4.0}/redis/commands/helpers.py +0 -0
  78. {redis-6.2.0 → redis-6.4.0}/redis/commands/json/__init__.py +0 -0
  79. {redis-6.2.0 → redis-6.4.0}/redis/commands/json/commands.py +0 -0
  80. {redis-6.2.0 → redis-6.4.0}/redis/commands/json/decoders.py +0 -0
  81. {redis-6.2.0 → redis-6.4.0}/redis/commands/json/path.py +0 -0
  82. {redis-6.2.0 → redis-6.4.0}/redis/commands/redismodules.py +0 -0
  83. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/__init__.py +0 -0
  84. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/_util.py +0 -0
  85. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/dialect.py +0 -0
  86. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/document.py +0 -0
  87. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/index_definition.py +0 -0
  88. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/profile_information.py +0 -0
  89. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/query.py +0 -0
  90. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/querystring.py +0 -0
  91. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/reducers.py +0 -0
  92. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/result.py +0 -0
  93. {redis-6.2.0 → redis-6.4.0}/redis/commands/search/suggestion.py +0 -0
  94. {redis-6.2.0 → redis-6.4.0}/redis/commands/timeseries/__init__.py +0 -0
  95. {redis-6.2.0 → redis-6.4.0}/redis/commands/timeseries/commands.py +0 -0
  96. {redis-6.2.0 → redis-6.4.0}/redis/commands/timeseries/info.py +0 -0
  97. {redis-6.2.0 → redis-6.4.0}/redis/commands/timeseries/utils.py +0 -0
  98. {redis-6.2.0 → redis-6.4.0}/redis/commands/vectorset/__init__.py +0 -0
  99. {redis-6.2.0 → redis-6.4.0}/redis/commands/vectorset/utils.py +0 -0
  100. {redis-6.2.0 → redis-6.4.0}/redis/crc.py +0 -0
  101. {redis-6.2.0 → redis-6.4.0}/redis/credentials.py +0 -0
  102. {redis-6.2.0 → redis-6.4.0}/redis/lock.py +0 -0
  103. {redis-6.2.0 → redis-6.4.0}/redis/ocsp.py +0 -0
  104. {redis-6.2.0 → redis-6.4.0}/redis/py.typed +0 -0
  105. {redis-6.2.0 → redis-6.4.0}/redis/typing.py +0 -0
  106. {redis-6.2.0 → redis-6.4.0}/tests/__init__.py +0 -0
  107. {redis-6.2.0 → redis-6.4.0}/tests/conftest.py +0 -0
  108. {redis-6.2.0 → redis-6.4.0}/tests/entraid_utils.py +0 -0
  109. {redis-6.2.0 → redis-6.4.0}/tests/mocks.py +0 -0
  110. {redis-6.2.0 → redis-6.4.0}/tests/ssl_utils.py +0 -0
  111. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/__init__.py +0 -0
  112. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/mocks.py +0 -0
  113. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_bloom.py +0 -0
  114. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_cluster_transaction.py +0 -0
  115. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_connect.py +0 -0
  116. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_credentials.py +0 -0
  117. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_cwe_404.py +0 -0
  118. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_encoding.py +0 -0
  119. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_hash.py +0 -0
  120. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_json.py +0 -0
  121. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_lock.py +0 -0
  122. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_monitor.py +0 -0
  123. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_retry.py +0 -0
  124. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_scripting.py +0 -0
  125. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_ssl.py +0 -0
  126. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_timeseries.py +0 -0
  127. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/test_utils.py +0 -0
  128. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/testdata/jsontestdata.py +0 -0
  129. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/testdata/titles.csv +0 -0
  130. {redis-6.2.0 → redis-6.4.0}/tests/test_asyncio/testdata/will_play_text.csv.bz2 +0 -0
  131. {redis-6.2.0 → redis-6.4.0}/tests/test_auth/__init__.py +0 -0
  132. {redis-6.2.0 → redis-6.4.0}/tests/test_auth/test_token.py +0 -0
  133. {redis-6.2.0 → redis-6.4.0}/tests/test_auth/test_token_manager.py +0 -0
  134. {redis-6.2.0 → redis-6.4.0}/tests/test_backoff.py +0 -0
  135. {redis-6.2.0 → redis-6.4.0}/tests/test_bloom.py +0 -0
  136. {redis-6.2.0 → redis-6.4.0}/tests/test_cache.py +0 -0
  137. {redis-6.2.0 → redis-6.4.0}/tests/test_command_parser.py +0 -0
  138. {redis-6.2.0 → redis-6.4.0}/tests/test_connect.py +0 -0
  139. {redis-6.2.0 → redis-6.4.0}/tests/test_encoding.py +0 -0
  140. {redis-6.2.0 → redis-6.4.0}/tests/test_function.py +0 -0
  141. {redis-6.2.0 → redis-6.4.0}/tests/test_hash.py +0 -0
  142. {redis-6.2.0 → redis-6.4.0}/tests/test_helpers.py +0 -0
  143. {redis-6.2.0 → redis-6.4.0}/tests/test_json.py +0 -0
  144. {redis-6.2.0 → redis-6.4.0}/tests/test_lock.py +0 -0
  145. {redis-6.2.0 → redis-6.4.0}/tests/test_monitor.py +0 -0
  146. {redis-6.2.0 → redis-6.4.0}/tests/test_multiprocessing.py +0 -0
  147. {redis-6.2.0 → redis-6.4.0}/tests/test_parsers/test_helpers.py +0 -0
  148. {redis-6.2.0 → redis-6.4.0}/tests/test_pipeline.py +0 -0
  149. {redis-6.2.0 → redis-6.4.0}/tests/test_pubsub.py +0 -0
  150. {redis-6.2.0 → redis-6.4.0}/tests/test_scripting.py +0 -0
  151. {redis-6.2.0 → redis-6.4.0}/tests/test_ssl.py +0 -0
  152. {redis-6.2.0 → redis-6.4.0}/tests/test_timeseries.py +0 -0
  153. {redis-6.2.0 → redis-6.4.0}/tests/test_utils.py +0 -0
  154. {redis-6.2.0 → redis-6.4.0}/tests/testdata/jsontestdata.py +0 -0
  155. {redis-6.2.0 → redis-6.4.0}/tests/testdata/titles.csv +0 -0
  156. {redis-6.2.0 → redis-6.4.0}/tests/testdata/will_play_text.csv.bz2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redis
3
- Version: 6.2.0
3
+ Version: 6.4.0
4
4
  Summary: Python client for Redis database and key-value store
5
5
  Project-URL: Changes, https://github.com/redis/redis-py/releases
6
6
  Project-URL: Code, https://github.com/redis/redis-py
@@ -43,7 +43,7 @@ Description-Content-Type: text/markdown
43
43
  The Python interface to the Redis key-value store.
44
44
 
45
45
  [![CI](https://github.com/redis/redis-py/workflows/CI/badge.svg?branch=master)](https://github.com/redis/redis-py/actions?query=workflow%3ACI+branch%3Amaster)
46
- [![docs](https://readthedocs.org/projects/redis/badge/?version=stable&style=flat)](https://redis-py.readthedocs.io/en/stable/)
46
+ [![docs](https://readthedocs.org/projects/redis/badge/?version=stable&style=flat)](https://redis.readthedocs.io/en/stable/)
47
47
  [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
48
48
  [![pypi](https://badge.fury.io/py/redis.svg)](https://pypi.org/project/redis/)
49
49
  [![pre-release](https://img.shields.io/github/v/release/redis/redis-py?include_prereleases&label=latest-prerelease)](https://github.com/redis/redis-py/releases)
@@ -81,7 +81,7 @@ Start a redis via docker (for Redis versions < 8.0):
81
81
 
82
82
  ``` bash
83
83
  docker run -p 6379:6379 -it redis/redis-stack:latest
84
-
84
+ ```
85
85
  To install redis-py, simply:
86
86
 
87
87
  ``` bash
@@ -249,4 +249,4 @@ Special thanks to:
249
249
  system.
250
250
  - Paul Hubbard for initial packaging support.
251
251
 
252
- [![Redis](./docs/_static/logo-redis.svg)](https://redis.io)
252
+ [![Redis](./docs/_static/logo-redis.svg)](https://redis.io)
@@ -3,7 +3,7 @@
3
3
  The Python interface to the Redis key-value store.
4
4
 
5
5
  [![CI](https://github.com/redis/redis-py/workflows/CI/badge.svg?branch=master)](https://github.com/redis/redis-py/actions?query=workflow%3ACI+branch%3Amaster)
6
- [![docs](https://readthedocs.org/projects/redis/badge/?version=stable&style=flat)](https://redis-py.readthedocs.io/en/stable/)
6
+ [![docs](https://readthedocs.org/projects/redis/badge/?version=stable&style=flat)](https://redis.readthedocs.io/en/stable/)
7
7
  [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
8
8
  [![pypi](https://badge.fury.io/py/redis.svg)](https://pypi.org/project/redis/)
9
9
  [![pre-release](https://img.shields.io/github/v/release/redis/redis-py?include_prereleases&label=latest-prerelease)](https://github.com/redis/redis-py/releases)
@@ -41,7 +41,7 @@ Start a redis via docker (for Redis versions < 8.0):
41
41
 
42
42
  ``` bash
43
43
  docker run -p 6379:6379 -it redis/redis-stack:latest
44
-
44
+ ```
45
45
  To install redis-py, simply:
46
46
 
47
47
  ``` bash
@@ -209,4 +209,4 @@ Special thanks to:
209
209
  system.
210
210
  - Paul Hubbard for initial packaging support.
211
211
 
212
- [![Redis](./docs/_static/logo-redis.svg)](https://redis.io)
212
+ [![Redis](./docs/_static/logo-redis.svg)](https://redis.io)
@@ -20,6 +20,7 @@ from redis.exceptions import (
20
20
  DataError,
21
21
  InvalidPipelineStack,
22
22
  InvalidResponse,
23
+ MaxConnectionsError,
23
24
  OutOfMemoryError,
24
25
  PubSubError,
25
26
  ReadOnlyError,
@@ -45,8 +46,7 @@ def int_or_str(value):
45
46
  return value
46
47
 
47
48
 
48
- # This version is used when building the package for publishing
49
- __version__ = "6.2.0"
49
+ __version__ = "6.4.0"
50
50
  VERSION = tuple(map(int_or_str, __version__.split(".")))
51
51
 
52
52
 
@@ -66,6 +66,7 @@ __all__ = [
66
66
  "default_backoff",
67
67
  "InvalidPipelineStack",
68
68
  "InvalidResponse",
69
+ "MaxConnectionsError",
69
70
  "OutOfMemoryError",
70
71
  "PubSubError",
71
72
  "ReadOnlyError",
@@ -676,7 +676,8 @@ def parse_client_info(value):
676
676
  "omem",
677
677
  "tot-mem",
678
678
  }:
679
- client_info[int_key] = int(client_info[int_key])
679
+ if int_key in client_info:
680
+ client_info[int_key] = int(client_info[int_key])
680
681
  return client_info
681
682
 
682
683
 
@@ -814,7 +814,13 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
814
814
  moved = False
815
815
 
816
816
  return await target_node.execute_command(*args, **kwargs)
817
- except (BusyLoadingError, MaxConnectionsError):
817
+ except BusyLoadingError:
818
+ raise
819
+ except MaxConnectionsError:
820
+ # MaxConnectionsError indicates client-side resource exhaustion
821
+ # (too many connections in the pool), not a node failure.
822
+ # Don't treat this as a node failure - just re-raise the error
823
+ # without reinitializing the cluster.
818
824
  raise
819
825
  except (ConnectionError, TimeoutError):
820
826
  # Connection retries are being handled in the node's
@@ -1581,15 +1587,6 @@ class ClusterPipeline(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterComm
1581
1587
  def __await__(self) -> Generator[Any, None, "ClusterPipeline"]:
1582
1588
  return self.initialize().__await__()
1583
1589
 
1584
- def __enter__(self) -> "ClusterPipeline":
1585
- # TODO: Remove this method before 7.0.0
1586
- self._execution_strategy._command_queue = []
1587
- return self
1588
-
1589
- def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None:
1590
- # TODO: Remove this method before 7.0.0
1591
- self._execution_strategy._command_queue = []
1592
-
1593
1590
  def __bool__(self) -> bool:
1594
1591
  "Pipeline instances should always evaluate to True on Python 3+"
1595
1592
  return True
@@ -2350,10 +2347,11 @@ class TransactionStrategy(AbstractStrategy):
2350
2347
  # watching something
2351
2348
  if self._transaction_connection:
2352
2349
  try:
2353
- # call this manually since our unwatch or
2354
- # immediate_execute_command methods can call reset()
2355
- await self._transaction_connection.send_command("UNWATCH")
2356
- await self._transaction_connection.read_response()
2350
+ if self._watching:
2351
+ # call this manually since our unwatch or
2352
+ # immediate_execute_command methods can call reset()
2353
+ await self._transaction_connection.send_command("UNWATCH")
2354
+ await self._transaction_connection.read_response()
2357
2355
  # we can safely return the connection to the pool here since we're
2358
2356
  # sure we're no longer WATCHing anything
2359
2357
  self._transaction_node.release(self._transaction_connection)
@@ -295,13 +295,18 @@ class AbstractConnection:
295
295
  """Connects to the Redis server if not already connected"""
296
296
  await self.connect_check_health(check_health=True)
297
297
 
298
- async def connect_check_health(self, check_health: bool = True):
298
+ async def connect_check_health(
299
+ self, check_health: bool = True, retry_socket_connect: bool = True
300
+ ):
299
301
  if self.is_connected:
300
302
  return
301
303
  try:
302
- await self.retry.call_with_retry(
303
- lambda: self._connect(), lambda error: self.disconnect()
304
- )
304
+ if retry_socket_connect:
305
+ await self.retry.call_with_retry(
306
+ lambda: self._connect(), lambda error: self.disconnect()
307
+ )
308
+ else:
309
+ await self._connect()
305
310
  except asyncio.CancelledError:
306
311
  raise # in 3.7 and earlier, this is an Exception, not BaseException
307
312
  except (socket.timeout, asyncio.TimeoutError):
@@ -1037,6 +1042,7 @@ class ConnectionPool:
1037
1042
  By default, TCP connections are created unless ``connection_class``
1038
1043
  is specified. Use :py:class:`~redis.UnixDomainSocketConnection` for
1039
1044
  unix sockets.
1045
+ :py:class:`~redis.SSLConnection` can be used for SSL enabled connections.
1040
1046
 
1041
1047
  Any additional keyword arguments are passed to the constructor of
1042
1048
  ``connection_class``.
@@ -1112,9 +1118,11 @@ class ConnectionPool:
1112
1118
  self._event_dispatcher = EventDispatcher()
1113
1119
 
1114
1120
  def __repr__(self):
1121
+ conn_kwargs = ",".join([f"{k}={v}" for k, v in self.connection_kwargs.items()])
1115
1122
  return (
1116
1123
  f"<{self.__class__.__module__}.{self.__class__.__name__}"
1117
- f"({self.connection_class(**self.connection_kwargs)!r})>"
1124
+ f"(<{self.connection_class.__module__}.{self.connection_class.__name__}"
1125
+ f"({conn_kwargs})>)>"
1118
1126
  )
1119
1127
 
1120
1128
  def reset(self):
@@ -2,18 +2,16 @@ from asyncio import sleep
2
2
  from typing import TYPE_CHECKING, Any, Awaitable, Callable, Tuple, Type, TypeVar
3
3
 
4
4
  from redis.exceptions import ConnectionError, RedisError, TimeoutError
5
-
6
- if TYPE_CHECKING:
7
- from redis.backoff import AbstractBackoff
8
-
5
+ from redis.retry import AbstractRetry
9
6
 
10
7
  T = TypeVar("T")
11
8
 
9
+ if TYPE_CHECKING:
10
+ from redis.backoff import AbstractBackoff
12
11
 
13
- class Retry:
14
- """Retry a specific number of times after a failure"""
15
12
 
16
- __slots__ = "_backoff", "_retries", "_supported_errors"
13
+ class Retry(AbstractRetry[RedisError]):
14
+ __hash__ = AbstractRetry.__hash__
17
15
 
18
16
  def __init__(
19
17
  self,
@@ -24,36 +22,17 @@ class Retry:
24
22
  TimeoutError,
25
23
  ),
26
24
  ):
27
- """
28
- Initialize a `Retry` object with a `Backoff` object
29
- that retries a maximum of `retries` times.
30
- `retries` can be negative to retry forever.
31
- You can specify the types of supported errors which trigger
32
- a retry with the `supported_errors` parameter.
33
- """
34
- self._backoff = backoff
35
- self._retries = retries
36
- self._supported_errors = supported_errors
25
+ super().__init__(backoff, retries, supported_errors)
37
26
 
38
- def update_supported_errors(self, specified_errors: list):
39
- """
40
- Updates the supported errors with the specified error types
41
- """
42
- self._supported_errors = tuple(
43
- set(self._supported_errors + tuple(specified_errors))
44
- )
45
-
46
- def get_retries(self) -> int:
47
- """
48
- Get the number of retries.
49
- """
50
- return self._retries
27
+ def __eq__(self, other: Any) -> bool:
28
+ if not isinstance(other, Retry):
29
+ return NotImplemented
51
30
 
52
- def update_retries(self, value: int) -> None:
53
- """
54
- Set the number of retries.
55
- """
56
- self._retries = value
31
+ return (
32
+ self._backoff == other._backoff
33
+ and self._retries == other._retries
34
+ and set(self._supported_errors) == set(other._supported_errors)
35
+ )
57
36
 
58
37
  async def call_with_retry(
59
38
  self, do: Callable[[], Awaitable[T]], fail: Callable[[RedisError], Any]
@@ -11,8 +11,12 @@ from redis.asyncio.connection import (
11
11
  SSLConnection,
12
12
  )
13
13
  from redis.commands import AsyncSentinelCommands
14
- from redis.exceptions import ConnectionError, ReadOnlyError, ResponseError, TimeoutError
15
- from redis.utils import str_if_bytes
14
+ from redis.exceptions import (
15
+ ConnectionError,
16
+ ReadOnlyError,
17
+ ResponseError,
18
+ TimeoutError,
19
+ )
16
20
 
17
21
 
18
22
  class MasterNotFoundError(ConnectionError):
@@ -37,11 +41,10 @@ class SentinelManagedConnection(Connection):
37
41
 
38
42
  async def connect_to(self, address):
39
43
  self.host, self.port = address
40
- await super().connect()
41
- if self.connection_pool.check_connection:
42
- await self.send_command("PING")
43
- if str_if_bytes(await self.read_response()) != "PONG":
44
- raise ConnectionError("PING failed")
44
+ await self.connect_check_health(
45
+ check_health=self.connection_pool.check_connection,
46
+ retry_socket_connect=False,
47
+ )
45
48
 
46
49
  async def _connect_retry(self):
47
50
  if self._reader:
@@ -223,19 +226,31 @@ class Sentinel(AsyncSentinelCommands):
223
226
  once - If set to True, then execute the resulting command on a single
224
227
  node at random, rather than across the entire sentinel cluster.
225
228
  """
226
- once = bool(kwargs.get("once", False))
227
- if "once" in kwargs.keys():
228
- kwargs.pop("once")
229
+ once = bool(kwargs.pop("once", False))
230
+
231
+ # Check if command is supposed to return the original
232
+ # responses instead of boolean value.
233
+ return_responses = bool(kwargs.pop("return_responses", False))
229
234
 
230
235
  if once:
231
- await random.choice(self.sentinels).execute_command(*args, **kwargs)
232
- else:
233
- tasks = [
234
- asyncio.Task(sentinel.execute_command(*args, **kwargs))
235
- for sentinel in self.sentinels
236
- ]
237
- await asyncio.gather(*tasks)
238
- return True
236
+ response = await random.choice(self.sentinels).execute_command(
237
+ *args, **kwargs
238
+ )
239
+ if return_responses:
240
+ return [response]
241
+ else:
242
+ return True if response else False
243
+
244
+ tasks = [
245
+ asyncio.Task(sentinel.execute_command(*args, **kwargs))
246
+ for sentinel in self.sentinels
247
+ ]
248
+ responses = await asyncio.gather(*tasks)
249
+
250
+ if return_responses:
251
+ return responses
252
+
253
+ return all(responses)
239
254
 
240
255
  def __repr__(self):
241
256
  sentinel_addresses = []
@@ -170,7 +170,7 @@ class ExponentialWithJitterBackoff(AbstractBackoff):
170
170
  return hash((self._base, self._cap))
171
171
 
172
172
  def __eq__(self, other) -> bool:
173
- if not isinstance(other, EqualJitterBackoff):
173
+ if not isinstance(other, ExponentialWithJitterBackoff):
174
174
  return NotImplemented
175
175
 
176
176
  return self._base == other._base and self._cap == other._cap
@@ -368,9 +368,7 @@ class Redis(RedisModuleCommands, CoreCommands, SentinelCommands):
368
368
  ]:
369
369
  raise RedisError("Client caching is only supported with RESP version 3")
370
370
 
371
- # TODO: To avoid breaking changes during the bug fix, we have to keep non-reentrant lock.
372
- # TODO: Remove this before next major version (7.0.0)
373
- self.single_connection_lock = threading.Lock()
371
+ self.single_connection_lock = threading.RLock()
374
372
  self.connection = None
375
373
  self._single_connection_client = single_connection_client
376
374
  if self._single_connection_client:
@@ -450,7 +448,7 @@ class Redis(RedisModuleCommands, CoreCommands, SentinelCommands):
450
448
 
451
449
  def transaction(
452
450
  self, func: Callable[["Pipeline"], None], *watches, **kwargs
453
- ) -> None:
451
+ ) -> Union[List[Any], Any, None]:
454
452
  """
455
453
  Convenience method for executing the callable `func` as a transaction
456
454
  while watching all keys specified in `watches`. The 'func' callable
@@ -776,9 +774,7 @@ class PubSub:
776
774
  else:
777
775
  self._event_dispatcher = event_dispatcher
778
776
 
779
- # TODO: To avoid breaking changes during the bug fix, we have to keep non-reentrant lock.
780
- # TODO: Remove this before next major version (7.0.0)
781
- self._lock = threading.Lock()
777
+ self._lock = threading.RLock()
782
778
  if self.encoder is None:
783
779
  self.encoder = self.connection_pool.get_encoder()
784
780
  self.health_check_response_b = self.encoder.encode(self.HEALTH_CHECK_MESSAGE)
@@ -39,6 +39,7 @@ from redis.exceptions import (
39
39
  DataError,
40
40
  ExecAbortError,
41
41
  InvalidPipelineStack,
42
+ MaxConnectionsError,
42
43
  MovedError,
43
44
  RedisClusterException,
44
45
  RedisError,
@@ -856,7 +857,6 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
856
857
  startup_nodes=self.nodes_manager.startup_nodes,
857
858
  result_callbacks=self.result_callbacks,
858
859
  cluster_response_callbacks=self.cluster_response_callbacks,
859
- cluster_error_retry_attempts=self.retry.get_retries(),
860
860
  read_from_replicas=self.read_from_replicas,
861
861
  load_balancing_strategy=self.load_balancing_strategy,
862
862
  reinitialize_steps=self.reinitialize_steps,
@@ -1236,6 +1236,12 @@ class RedisCluster(AbstractRedisCluster, RedisClusterCommands):
1236
1236
  return response
1237
1237
  except AuthenticationError:
1238
1238
  raise
1239
+ except MaxConnectionsError:
1240
+ # MaxConnectionsError indicates client-side resource exhaustion
1241
+ # (too many connections in the pool), not a node failure.
1242
+ # Don't treat this as a node failure - just re-raise the error
1243
+ # without reinitializing the cluster.
1244
+ raise
1239
1245
  except (ConnectionError, TimeoutError) as e:
1240
1246
  # ConnectionError can also be raised if we couldn't get a
1241
1247
  # connection from the pool before timing out, so check that
@@ -3290,10 +3296,11 @@ class TransactionStrategy(AbstractStrategy):
3290
3296
  # watching something
3291
3297
  if self._transaction_connection:
3292
3298
  try:
3293
- # call this manually since our unwatch or
3294
- # immediate_execute_command methods can call reset()
3295
- self._transaction_connection.send_command("UNWATCH")
3296
- self._transaction_connection.read_response()
3299
+ if self._watching:
3300
+ # call this manually since our unwatch or
3301
+ # immediate_execute_command methods can call reset()
3302
+ self._transaction_connection.send_command("UNWATCH")
3303
+ self._transaction_connection.read_response()
3297
3304
  # we can safely return the connection to the pool here since we're
3298
3305
  # sure we're no longer WATCHing anything
3299
3306
  node = self._nodes_manager.find_connection_owner(
@@ -3290,7 +3290,7 @@ class SetCommands(CommandsProtocol):
3290
3290
  see: https://redis.io/topics/data-types#sets
3291
3291
  """
3292
3292
 
3293
- def sadd(self, name: str, *values: FieldT) -> Union[Awaitable[int], int]:
3293
+ def sadd(self, name: KeyT, *values: FieldT) -> Union[Awaitable[int], int]:
3294
3294
  """
3295
3295
  Add ``value(s)`` to set ``name``
3296
3296
 
@@ -3298,7 +3298,7 @@ class SetCommands(CommandsProtocol):
3298
3298
  """
3299
3299
  return self.execute_command("SADD", name, *values)
3300
3300
 
3301
- def scard(self, name: str) -> Union[Awaitable[int], int]:
3301
+ def scard(self, name: KeyT) -> Union[Awaitable[int], int]:
3302
3302
  """
3303
3303
  Return the number of elements in set ``name``
3304
3304
 
@@ -3337,7 +3337,7 @@ class SetCommands(CommandsProtocol):
3337
3337
  return self.execute_command("SINTER", *args, keys=args)
3338
3338
 
3339
3339
  def sintercard(
3340
- self, numkeys: int, keys: List[str], limit: int = 0
3340
+ self, numkeys: int, keys: List[KeyT], limit: int = 0
3341
3341
  ) -> Union[Awaitable[int], int]:
3342
3342
  """
3343
3343
  Return the cardinality of the intersect of multiple sets specified by ``keys``.
@@ -3352,7 +3352,7 @@ class SetCommands(CommandsProtocol):
3352
3352
  return self.execute_command("SINTERCARD", *args, keys=keys)
3353
3353
 
3354
3354
  def sinterstore(
3355
- self, dest: str, keys: List, *args: List
3355
+ self, dest: KeyT, keys: List, *args: List
3356
3356
  ) -> Union[Awaitable[int], int]:
3357
3357
  """
3358
3358
  Store the intersection of sets specified by ``keys`` into a new
@@ -3364,7 +3364,7 @@ class SetCommands(CommandsProtocol):
3364
3364
  return self.execute_command("SINTERSTORE", dest, *args)
3365
3365
 
3366
3366
  def sismember(
3367
- self, name: str, value: str
3367
+ self, name: KeyT, value: str
3368
3368
  ) -> Union[Awaitable[Union[Literal[0], Literal[1]]], Union[Literal[0], Literal[1]]]:
3369
3369
  """
3370
3370
  Return whether ``value`` is a member of set ``name``:
@@ -3375,7 +3375,7 @@ class SetCommands(CommandsProtocol):
3375
3375
  """
3376
3376
  return self.execute_command("SISMEMBER", name, value, keys=[name])
3377
3377
 
3378
- def smembers(self, name: str) -> Union[Awaitable[Set], Set]:
3378
+ def smembers(self, name: KeyT) -> Union[Awaitable[Set], Set]:
3379
3379
  """
3380
3380
  Return all members of the set ``name``
3381
3381
 
@@ -3384,7 +3384,7 @@ class SetCommands(CommandsProtocol):
3384
3384
  return self.execute_command("SMEMBERS", name, keys=[name])
3385
3385
 
3386
3386
  def smismember(
3387
- self, name: str, values: List, *args: List
3387
+ self, name: KeyT, values: List, *args: List
3388
3388
  ) -> Union[
3389
3389
  Awaitable[List[Union[Literal[0], Literal[1]]]],
3390
3390
  List[Union[Literal[0], Literal[1]]],
@@ -3400,7 +3400,7 @@ class SetCommands(CommandsProtocol):
3400
3400
  args = list_or_args(values, args)
3401
3401
  return self.execute_command("SMISMEMBER", name, *args, keys=[name])
3402
3402
 
3403
- def smove(self, src: str, dst: str, value: str) -> Union[Awaitable[bool], bool]:
3403
+ def smove(self, src: KeyT, dst: KeyT, value: str) -> Union[Awaitable[bool], bool]:
3404
3404
  """
3405
3405
  Move ``value`` from set ``src`` to set ``dst`` atomically
3406
3406
 
@@ -3408,7 +3408,7 @@ class SetCommands(CommandsProtocol):
3408
3408
  """
3409
3409
  return self.execute_command("SMOVE", src, dst, value)
3410
3410
 
3411
- def spop(self, name: str, count: Optional[int] = None) -> Union[str, List, None]:
3411
+ def spop(self, name: KeyT, count: Optional[int] = None) -> Union[str, List, None]:
3412
3412
  """
3413
3413
  Remove and return a random member of set ``name``
3414
3414
 
@@ -3418,7 +3418,7 @@ class SetCommands(CommandsProtocol):
3418
3418
  return self.execute_command("SPOP", name, *args)
3419
3419
 
3420
3420
  def srandmember(
3421
- self, name: str, number: Optional[int] = None
3421
+ self, name: KeyT, number: Optional[int] = None
3422
3422
  ) -> Union[str, List, None]:
3423
3423
  """
3424
3424
  If ``number`` is None, returns a random member of set ``name``.
@@ -3432,7 +3432,7 @@ class SetCommands(CommandsProtocol):
3432
3432
  args = (number is not None) and [number] or []
3433
3433
  return self.execute_command("SRANDMEMBER", name, *args)
3434
3434
 
3435
- def srem(self, name: str, *values: FieldT) -> Union[Awaitable[int], int]:
3435
+ def srem(self, name: KeyT, *values: FieldT) -> Union[Awaitable[int], int]:
3436
3436
  """
3437
3437
  Remove ``values`` from set ``name``
3438
3438
 
@@ -3450,7 +3450,7 @@ class SetCommands(CommandsProtocol):
3450
3450
  return self.execute_command("SUNION", *args, keys=args)
3451
3451
 
3452
3452
  def sunionstore(
3453
- self, dest: str, keys: List, *args: List
3453
+ self, dest: KeyT, keys: List, *args: List
3454
3454
  ) -> Union[Awaitable[int], int]:
3455
3455
  """
3456
3456
  Store the union of sets specified by ``keys`` into a new
@@ -3484,6 +3484,28 @@ class StreamCommands(CommandsProtocol):
3484
3484
  """
3485
3485
  return self.execute_command("XACK", name, groupname, *ids)
3486
3486
 
3487
+ def xackdel(
3488
+ self,
3489
+ name: KeyT,
3490
+ groupname: GroupT,
3491
+ *ids: StreamIdT,
3492
+ ref_policy: Literal["KEEPREF", "DELREF", "ACKED"] = "KEEPREF",
3493
+ ) -> ResponseT:
3494
+ """
3495
+ Combines the functionality of XACK and XDEL. Acknowledges the specified
3496
+ message IDs in the given consumer group and simultaneously attempts to
3497
+ delete the corresponding entries from the stream.
3498
+ """
3499
+ if not ids:
3500
+ raise DataError("XACKDEL requires at least one message ID")
3501
+
3502
+ if ref_policy not in {"KEEPREF", "DELREF", "ACKED"}:
3503
+ raise DataError("XACKDEL ref_policy must be one of: KEEPREF, DELREF, ACKED")
3504
+
3505
+ pieces = [name, groupname, ref_policy, "IDS", len(ids)]
3506
+ pieces.extend(ids)
3507
+ return self.execute_command("XACKDEL", *pieces)
3508
+
3487
3509
  def xadd(
3488
3510
  self,
3489
3511
  name: KeyT,
@@ -3494,6 +3516,7 @@ class StreamCommands(CommandsProtocol):
3494
3516
  nomkstream: bool = False,
3495
3517
  minid: Union[StreamIdT, None] = None,
3496
3518
  limit: Optional[int] = None,
3519
+ ref_policy: Optional[Literal["KEEPREF", "DELREF", "ACKED"]] = None,
3497
3520
  ) -> ResponseT:
3498
3521
  """
3499
3522
  Add to a stream.
@@ -3507,6 +3530,10 @@ class StreamCommands(CommandsProtocol):
3507
3530
  minid: the minimum id in the stream to query.
3508
3531
  Can't be specified with maxlen.
3509
3532
  limit: specifies the maximum number of entries to retrieve
3533
+ ref_policy: optional reference policy for consumer groups when trimming:
3534
+ - KEEPREF (default): When trimming, preserves references in consumer groups' PEL
3535
+ - DELREF: When trimming, removes all references from consumer groups' PEL
3536
+ - ACKED: When trimming, only removes entries acknowledged by all consumer groups
3510
3537
 
3511
3538
  For more information see https://redis.io/commands/xadd
3512
3539
  """
@@ -3514,6 +3541,9 @@ class StreamCommands(CommandsProtocol):
3514
3541
  if maxlen is not None and minid is not None:
3515
3542
  raise DataError("Only one of ```maxlen``` or ```minid``` may be specified")
3516
3543
 
3544
+ if ref_policy is not None and ref_policy not in {"KEEPREF", "DELREF", "ACKED"}:
3545
+ raise DataError("XADD ref_policy must be one of: KEEPREF, DELREF, ACKED")
3546
+
3517
3547
  if maxlen is not None:
3518
3548
  if not isinstance(maxlen, int) or maxlen < 0:
3519
3549
  raise DataError("XADD maxlen must be non-negative integer")
@@ -3530,6 +3560,8 @@ class StreamCommands(CommandsProtocol):
3530
3560
  pieces.extend([b"LIMIT", limit])
3531
3561
  if nomkstream:
3532
3562
  pieces.append(b"NOMKSTREAM")
3563
+ if ref_policy is not None:
3564
+ pieces.append(ref_policy)
3533
3565
  pieces.append(id)
3534
3566
  if not isinstance(fields, dict) or len(fields) == 0:
3535
3567
  raise DataError("XADD fields must be a non-empty dict")
@@ -3683,6 +3715,26 @@ class StreamCommands(CommandsProtocol):
3683
3715
  """
3684
3716
  return self.execute_command("XDEL", name, *ids)
3685
3717
 
3718
+ def xdelex(
3719
+ self,
3720
+ name: KeyT,
3721
+ *ids: StreamIdT,
3722
+ ref_policy: Literal["KEEPREF", "DELREF", "ACKED"] = "KEEPREF",
3723
+ ) -> ResponseT:
3724
+ """
3725
+ Extended version of XDEL that provides more control over how message entries
3726
+ are deleted concerning consumer groups.
3727
+ """
3728
+ if not ids:
3729
+ raise DataError("XDELEX requires at least one message ID")
3730
+
3731
+ if ref_policy not in {"KEEPREF", "DELREF", "ACKED"}:
3732
+ raise DataError("XDELEX ref_policy must be one of: KEEPREF, DELREF, ACKED")
3733
+
3734
+ pieces = [name, ref_policy, "IDS", len(ids)]
3735
+ pieces.extend(ids)
3736
+ return self.execute_command("XDELEX", *pieces)
3737
+
3686
3738
  def xgroup_create(
3687
3739
  self,
3688
3740
  name: KeyT,
@@ -4034,6 +4086,7 @@ class StreamCommands(CommandsProtocol):
4034
4086
  approximate: bool = True,
4035
4087
  minid: Union[StreamIdT, None] = None,
4036
4088
  limit: Optional[int] = None,
4089
+ ref_policy: Optional[Literal["KEEPREF", "DELREF", "ACKED"]] = None,
4037
4090
  ) -> ResponseT:
4038
4091
  """
4039
4092
  Trims old messages from a stream.
@@ -4044,6 +4097,10 @@ class StreamCommands(CommandsProtocol):
4044
4097
  minid: the minimum id in the stream to query
4045
4098
  Can't be specified with maxlen.
4046
4099
  limit: specifies the maximum number of entries to retrieve
4100
+ ref_policy: optional reference policy for consumer groups:
4101
+ - KEEPREF (default): Trims entries but preserves references in consumer groups' PEL
4102
+ - DELREF: Trims entries and removes all references from consumer groups' PEL
4103
+ - ACKED: Only trims entries that were read and acknowledged by all consumer groups
4047
4104
 
4048
4105
  For more information see https://redis.io/commands/xtrim
4049
4106
  """
@@ -4054,6 +4111,9 @@ class StreamCommands(CommandsProtocol):
4054
4111
  if maxlen is None and minid is None:
4055
4112
  raise DataError("One of ``maxlen`` or ``minid`` must be specified")
4056
4113
 
4114
+ if ref_policy is not None and ref_policy not in {"KEEPREF", "DELREF", "ACKED"}:
4115
+ raise DataError("XTRIM ref_policy must be one of: KEEPREF, DELREF, ACKED")
4116
+
4057
4117
  if maxlen is not None:
4058
4118
  pieces.append(b"MAXLEN")
4059
4119
  if minid is not None:
@@ -4067,6 +4127,8 @@ class StreamCommands(CommandsProtocol):
4067
4127
  if limit is not None:
4068
4128
  pieces.append(b"LIMIT")
4069
4129
  pieces.append(limit)
4130
+ if ref_policy is not None:
4131
+ pieces.append(ref_policy)
4070
4132
 
4071
4133
  return self.execute_command("XTRIM", name, *pieces)
4072
4134
 
@@ -0,0 +1,5 @@
1
+ from typing import List, Mapping, Union
2
+
3
+ JsonType = Union[
4
+ str, int, float, bool, None, Mapping[str, "JsonType"], List["JsonType"]
5
+ ]