redis 6.3.0__tar.gz → 7.0.0b1__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 (165) hide show
  1. {redis-6.3.0 → redis-7.0.0b1}/PKG-INFO +1 -1
  2. {redis-6.3.0 → redis-7.0.0b1}/redis/__init__.py +1 -2
  3. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/base.py +173 -8
  4. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/hiredis.py +16 -10
  5. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/resp3.py +11 -5
  6. {redis-6.3.0 → redis-7.0.0b1}/redis/asyncio/client.py +45 -2
  7. {redis-6.3.0 → redis-7.0.0b1}/redis/asyncio/cluster.py +46 -12
  8. {redis-6.3.0 → redis-7.0.0b1}/redis/cache.py +1 -0
  9. {redis-6.3.0 → redis-7.0.0b1}/redis/client.py +70 -19
  10. {redis-6.3.0 → redis-7.0.0b1}/redis/cluster.py +3 -2
  11. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/core.py +285 -285
  12. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/helpers.py +0 -20
  13. redis-7.0.0b1/redis/commands/json/_util.py +5 -0
  14. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/aggregation.py +3 -3
  15. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/commands.py +3 -3
  16. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/query.py +12 -12
  17. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/vectorset/commands.py +50 -25
  18. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/vectorset/utils.py +40 -4
  19. {redis-6.3.0 → redis-7.0.0b1}/redis/connection.py +830 -67
  20. {redis-6.3.0 → redis-7.0.0b1}/redis/event.py +4 -4
  21. redis-7.0.0b1/redis/maintenance_events.py +785 -0
  22. redis-7.0.0b1/tests/test_asyncio/test_usage_counter.py +16 -0
  23. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_vsets.py +128 -1
  24. {redis-6.3.0 → redis-7.0.0b1}/tests/test_cluster.py +16 -0
  25. {redis-6.3.0 → redis-7.0.0b1}/tests/test_cluster_transaction.py +2 -2
  26. {redis-6.3.0 → redis-7.0.0b1}/tests/test_connection.py +3 -3
  27. {redis-6.3.0 → redis-7.0.0b1}/tests/test_connection_pool.py +9 -1
  28. {redis-6.3.0 → redis-7.0.0b1}/tests/test_credentials.py +5 -5
  29. redis-7.0.0b1/tests/test_maintenance_events.py +869 -0
  30. redis-7.0.0b1/tests/test_maintenance_events_handling.py +2175 -0
  31. redis-7.0.0b1/tests/test_scenario/__init__.py +0 -0
  32. redis-7.0.0b1/tests/test_scenario/conftest.py +120 -0
  33. redis-7.0.0b1/tests/test_scenario/fault_injector_client.py +149 -0
  34. redis-7.0.0b1/tests/test_scenario/hitless_upgrade_helpers.py +287 -0
  35. redis-7.0.0b1/tests/test_scenario/test_hitless_upgrade.py +795 -0
  36. {redis-6.3.0 → redis-7.0.0b1}/tests/test_search.py +49 -0
  37. {redis-6.3.0 → redis-7.0.0b1}/tests/test_vsets.py +126 -1
  38. redis-6.3.0/redis/commands/json/_util.py +0 -3
  39. {redis-6.3.0 → redis-7.0.0b1}/.gitignore +0 -0
  40. {redis-6.3.0 → redis-7.0.0b1}/LICENSE +0 -0
  41. {redis-6.3.0 → redis-7.0.0b1}/README.md +0 -0
  42. {redis-6.3.0 → redis-7.0.0b1}/dev_requirements.txt +0 -0
  43. {redis-6.3.0 → redis-7.0.0b1}/pyproject.toml +0 -0
  44. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/__init__.py +0 -0
  45. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/commands.py +0 -0
  46. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/encoders.py +0 -0
  47. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/helpers.py +0 -0
  48. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/resp2.py +0 -0
  49. {redis-6.3.0 → redis-7.0.0b1}/redis/_parsers/socket.py +0 -0
  50. {redis-6.3.0 → redis-7.0.0b1}/redis/asyncio/__init__.py +0 -0
  51. {redis-6.3.0 → redis-7.0.0b1}/redis/asyncio/connection.py +0 -0
  52. {redis-6.3.0 → redis-7.0.0b1}/redis/asyncio/lock.py +0 -0
  53. {redis-6.3.0 → redis-7.0.0b1}/redis/asyncio/retry.py +0 -0
  54. {redis-6.3.0 → redis-7.0.0b1}/redis/asyncio/sentinel.py +0 -0
  55. {redis-6.3.0 → redis-7.0.0b1}/redis/asyncio/utils.py +0 -0
  56. {redis-6.3.0 → redis-7.0.0b1}/redis/auth/__init__.py +0 -0
  57. {redis-6.3.0 → redis-7.0.0b1}/redis/auth/err.py +0 -0
  58. {redis-6.3.0 → redis-7.0.0b1}/redis/auth/idp.py +0 -0
  59. {redis-6.3.0 → redis-7.0.0b1}/redis/auth/token.py +0 -0
  60. {redis-6.3.0 → redis-7.0.0b1}/redis/auth/token_manager.py +0 -0
  61. {redis-6.3.0 → redis-7.0.0b1}/redis/backoff.py +0 -0
  62. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/__init__.py +0 -0
  63. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/bf/__init__.py +0 -0
  64. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/bf/commands.py +0 -0
  65. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/bf/info.py +0 -0
  66. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/cluster.py +0 -0
  67. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/json/__init__.py +0 -0
  68. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/json/commands.py +0 -0
  69. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/json/decoders.py +0 -0
  70. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/json/path.py +0 -0
  71. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/redismodules.py +0 -0
  72. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/__init__.py +0 -0
  73. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/_util.py +0 -0
  74. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/dialect.py +0 -0
  75. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/document.py +0 -0
  76. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/field.py +0 -0
  77. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/index_definition.py +0 -0
  78. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/profile_information.py +0 -0
  79. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/querystring.py +0 -0
  80. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/reducers.py +0 -0
  81. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/result.py +0 -0
  82. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/search/suggestion.py +0 -0
  83. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/sentinel.py +0 -0
  84. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/timeseries/__init__.py +0 -0
  85. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/timeseries/commands.py +0 -0
  86. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/timeseries/info.py +0 -0
  87. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/timeseries/utils.py +0 -0
  88. {redis-6.3.0 → redis-7.0.0b1}/redis/commands/vectorset/__init__.py +1 -1
  89. {redis-6.3.0 → redis-7.0.0b1}/redis/crc.py +0 -0
  90. {redis-6.3.0 → redis-7.0.0b1}/redis/credentials.py +0 -0
  91. {redis-6.3.0 → redis-7.0.0b1}/redis/exceptions.py +0 -0
  92. {redis-6.3.0 → redis-7.0.0b1}/redis/lock.py +0 -0
  93. {redis-6.3.0 → redis-7.0.0b1}/redis/ocsp.py +0 -0
  94. {redis-6.3.0 → redis-7.0.0b1}/redis/py.typed +0 -0
  95. {redis-6.3.0 → redis-7.0.0b1}/redis/retry.py +0 -0
  96. {redis-6.3.0 → redis-7.0.0b1}/redis/sentinel.py +0 -0
  97. {redis-6.3.0 → redis-7.0.0b1}/redis/typing.py +0 -0
  98. {redis-6.3.0 → redis-7.0.0b1}/redis/utils.py +0 -0
  99. {redis-6.3.0 → redis-7.0.0b1}/tests/__init__.py +0 -0
  100. {redis-6.3.0 → redis-7.0.0b1}/tests/conftest.py +0 -0
  101. {redis-6.3.0 → redis-7.0.0b1}/tests/entraid_utils.py +0 -0
  102. {redis-6.3.0 → redis-7.0.0b1}/tests/mocks.py +0 -0
  103. {redis-6.3.0 → redis-7.0.0b1}/tests/ssl_utils.py +0 -0
  104. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/__init__.py +0 -0
  105. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/compat.py +0 -0
  106. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/conftest.py +0 -0
  107. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/mocks.py +0 -0
  108. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_bloom.py +0 -0
  109. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_cluster.py +0 -0
  110. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_cluster_transaction.py +0 -0
  111. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_commands.py +0 -0
  112. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_connect.py +0 -0
  113. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_connection.py +0 -0
  114. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_connection_pool.py +0 -0
  115. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_credentials.py +0 -0
  116. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_cwe_404.py +0 -0
  117. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_encoding.py +0 -0
  118. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_hash.py +0 -0
  119. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_json.py +0 -0
  120. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_lock.py +0 -0
  121. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_monitor.py +0 -0
  122. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_pipeline.py +0 -0
  123. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_pubsub.py +0 -0
  124. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_retry.py +0 -0
  125. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_scripting.py +0 -0
  126. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_search.py +0 -0
  127. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_sentinel.py +0 -0
  128. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_sentinel_managed_connection.py +0 -0
  129. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_ssl.py +0 -0
  130. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_timeseries.py +0 -0
  131. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/test_utils.py +0 -0
  132. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/testdata/jsontestdata.py +0 -0
  133. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/testdata/titles.csv +0 -0
  134. {redis-6.3.0 → redis-7.0.0b1}/tests/test_asyncio/testdata/will_play_text.csv.bz2 +0 -0
  135. {redis-6.3.0 → redis-7.0.0b1}/tests/test_auth/__init__.py +0 -0
  136. {redis-6.3.0 → redis-7.0.0b1}/tests/test_auth/test_token.py +0 -0
  137. {redis-6.3.0 → redis-7.0.0b1}/tests/test_auth/test_token_manager.py +0 -0
  138. {redis-6.3.0 → redis-7.0.0b1}/tests/test_backoff.py +0 -0
  139. {redis-6.3.0 → redis-7.0.0b1}/tests/test_bloom.py +0 -0
  140. {redis-6.3.0 → redis-7.0.0b1}/tests/test_cache.py +0 -0
  141. {redis-6.3.0 → redis-7.0.0b1}/tests/test_command_parser.py +0 -0
  142. {redis-6.3.0 → redis-7.0.0b1}/tests/test_commands.py +0 -0
  143. {redis-6.3.0 → redis-7.0.0b1}/tests/test_connect.py +0 -0
  144. {redis-6.3.0 → redis-7.0.0b1}/tests/test_encoding.py +0 -0
  145. {redis-6.3.0 → redis-7.0.0b1}/tests/test_function.py +0 -0
  146. {redis-6.3.0 → redis-7.0.0b1}/tests/test_hash.py +0 -0
  147. {redis-6.3.0 → redis-7.0.0b1}/tests/test_helpers.py +0 -0
  148. {redis-6.3.0 → redis-7.0.0b1}/tests/test_json.py +0 -0
  149. {redis-6.3.0 → redis-7.0.0b1}/tests/test_lock.py +0 -0
  150. {redis-6.3.0 → redis-7.0.0b1}/tests/test_max_connections_error.py +0 -0
  151. {redis-6.3.0 → redis-7.0.0b1}/tests/test_monitor.py +0 -0
  152. {redis-6.3.0 → redis-7.0.0b1}/tests/test_multiprocessing.py +0 -0
  153. {redis-6.3.0 → redis-7.0.0b1}/tests/test_parsers/test_helpers.py +0 -0
  154. {redis-6.3.0 → redis-7.0.0b1}/tests/test_pipeline.py +0 -0
  155. {redis-6.3.0 → redis-7.0.0b1}/tests/test_pubsub.py +0 -0
  156. {redis-6.3.0 → redis-7.0.0b1}/tests/test_retry.py +0 -0
  157. {redis-6.3.0 → redis-7.0.0b1}/tests/test_scripting.py +0 -0
  158. {redis-6.3.0 → redis-7.0.0b1}/tests/test_sentinel.py +0 -0
  159. {redis-6.3.0 → redis-7.0.0b1}/tests/test_sentinel_managed_connection.py +0 -0
  160. {redis-6.3.0 → redis-7.0.0b1}/tests/test_ssl.py +0 -0
  161. {redis-6.3.0 → redis-7.0.0b1}/tests/test_timeseries.py +0 -0
  162. {redis-6.3.0 → redis-7.0.0b1}/tests/test_utils.py +0 -0
  163. {redis-6.3.0 → redis-7.0.0b1}/tests/testdata/jsontestdata.py +0 -0
  164. {redis-6.3.0 → redis-7.0.0b1}/tests/testdata/titles.csv +0 -0
  165. {redis-6.3.0 → redis-7.0.0b1}/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.3.0
3
+ Version: 7.0.0b1
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
@@ -46,8 +46,7 @@ def int_or_str(value):
46
46
  return value
47
47
 
48
48
 
49
- # This version is used when building the package for publishing
50
- __version__ = "6.3.0"
49
+ __version__ = "7.0.0b1"
51
50
  VERSION = tuple(map(int_or_str, __version__.split(".")))
52
51
 
53
52
 
@@ -1,7 +1,17 @@
1
+ import logging
1
2
  import sys
2
3
  from abc import ABC
3
4
  from asyncio import IncompleteReadError, StreamReader, TimeoutError
4
- from typing import Callable, List, Optional, Protocol, Union
5
+ from typing import Awaitable, Callable, List, Optional, Protocol, Union
6
+
7
+ from redis.maintenance_events import (
8
+ MaintenanceEvent,
9
+ NodeFailedOverEvent,
10
+ NodeFailingOverEvent,
11
+ NodeMigratedEvent,
12
+ NodeMigratingEvent,
13
+ NodeMovingEvent,
14
+ )
5
15
 
6
16
  if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
7
17
  from asyncio import timeout as async_timeout
@@ -50,6 +60,8 @@ NO_AUTH_SET_ERROR = {
50
60
  "Client sent AUTH, but no password is set": AuthenticationError,
51
61
  }
52
62
 
63
+ logger = logging.getLogger(__name__)
64
+
53
65
 
54
66
  class BaseParser(ABC):
55
67
  EXCEPTION_CLASSES = {
@@ -158,7 +170,75 @@ class AsyncBaseParser(BaseParser):
158
170
  raise NotImplementedError()
159
171
 
160
172
 
161
- _INVALIDATION_MESSAGE = [b"invalidate", "invalidate"]
173
+ class MaintenanceNotificationsParser:
174
+ """Protocol defining maintenance push notification parsing functionality"""
175
+
176
+ @staticmethod
177
+ def parse_maintenance_start_msg(response, notification_type):
178
+ # Expected message format is: <event_type> <seq_number> <time>
179
+ id = response[1]
180
+ ttl = response[2]
181
+ return notification_type(id, ttl)
182
+
183
+ @staticmethod
184
+ def parse_maintenance_completed_msg(response, notification_type):
185
+ # Expected message format is: <event_type> <seq_number>
186
+ id = response[1]
187
+ return notification_type(id)
188
+
189
+ @staticmethod
190
+ def parse_moving_msg(response):
191
+ # Expected message format is: MOVING <seq_number> <time> <endpoint>
192
+ id = response[1]
193
+ ttl = response[2]
194
+ if response[3] is None:
195
+ host, port = None, None
196
+ else:
197
+ value = response[3]
198
+ if isinstance(value, bytes):
199
+ value = value.decode()
200
+ host, port = value.split(":")
201
+ port = int(port) if port is not None else None
202
+
203
+ return NodeMovingEvent(id, host, port, ttl)
204
+
205
+
206
+ _INVALIDATION_MESSAGE = "invalidate"
207
+ _MOVING_MESSAGE = "MOVING"
208
+ _MIGRATING_MESSAGE = "MIGRATING"
209
+ _MIGRATED_MESSAGE = "MIGRATED"
210
+ _FAILING_OVER_MESSAGE = "FAILING_OVER"
211
+ _FAILED_OVER_MESSAGE = "FAILED_OVER"
212
+
213
+ _MAINTENANCE_MESSAGES = (
214
+ _MIGRATING_MESSAGE,
215
+ _MIGRATED_MESSAGE,
216
+ _FAILING_OVER_MESSAGE,
217
+ _FAILED_OVER_MESSAGE,
218
+ )
219
+
220
+ MSG_TYPE_TO_EVENT_PARSER_MAPPING: dict[str, tuple[type[MaintenanceEvent], Callable]] = {
221
+ _MIGRATING_MESSAGE: (
222
+ NodeMigratingEvent,
223
+ MaintenanceNotificationsParser.parse_maintenance_start_msg,
224
+ ),
225
+ _MIGRATED_MESSAGE: (
226
+ NodeMigratedEvent,
227
+ MaintenanceNotificationsParser.parse_maintenance_completed_msg,
228
+ ),
229
+ _FAILING_OVER_MESSAGE: (
230
+ NodeFailingOverEvent,
231
+ MaintenanceNotificationsParser.parse_maintenance_start_msg,
232
+ ),
233
+ _FAILED_OVER_MESSAGE: (
234
+ NodeFailedOverEvent,
235
+ MaintenanceNotificationsParser.parse_maintenance_completed_msg,
236
+ ),
237
+ _MOVING_MESSAGE: (
238
+ NodeMovingEvent,
239
+ MaintenanceNotificationsParser.parse_moving_msg,
240
+ ),
241
+ }
162
242
 
163
243
 
164
244
  class PushNotificationsParser(Protocol):
@@ -166,16 +246,51 @@ class PushNotificationsParser(Protocol):
166
246
 
167
247
  pubsub_push_handler_func: Callable
168
248
  invalidation_push_handler_func: Optional[Callable] = None
249
+ node_moving_push_handler_func: Optional[Callable] = None
250
+ maintenance_push_handler_func: Optional[Callable] = None
169
251
 
170
252
  def handle_pubsub_push_response(self, response):
171
253
  """Handle pubsub push responses"""
172
254
  raise NotImplementedError()
173
255
 
174
256
  def handle_push_response(self, response, **kwargs):
175
- if response[0] not in _INVALIDATION_MESSAGE:
257
+ msg_type = response[0]
258
+ if isinstance(msg_type, bytes):
259
+ msg_type = msg_type.decode()
260
+
261
+ if msg_type not in (
262
+ _INVALIDATION_MESSAGE,
263
+ *_MAINTENANCE_MESSAGES,
264
+ _MOVING_MESSAGE,
265
+ ):
176
266
  return self.pubsub_push_handler_func(response)
177
- if self.invalidation_push_handler_func:
178
- return self.invalidation_push_handler_func(response)
267
+
268
+ try:
269
+ if (
270
+ msg_type == _INVALIDATION_MESSAGE
271
+ and self.invalidation_push_handler_func
272
+ ):
273
+ return self.invalidation_push_handler_func(response)
274
+
275
+ if msg_type == _MOVING_MESSAGE and self.node_moving_push_handler_func:
276
+ parser_function = MSG_TYPE_TO_EVENT_PARSER_MAPPING[msg_type][1]
277
+
278
+ notification = parser_function(response)
279
+ return self.node_moving_push_handler_func(notification)
280
+
281
+ if msg_type in _MAINTENANCE_MESSAGES and self.maintenance_push_handler_func:
282
+ parser_function = MSG_TYPE_TO_EVENT_PARSER_MAPPING[msg_type][1]
283
+ notification_type = MSG_TYPE_TO_EVENT_PARSER_MAPPING[msg_type][0]
284
+ notification = parser_function(response, notification_type)
285
+
286
+ if notification is not None:
287
+ return self.maintenance_push_handler_func(notification)
288
+ except Exception as e:
289
+ logger.error(
290
+ "Error handling {} message ({}): {}".format(msg_type, response, e)
291
+ )
292
+
293
+ return None
179
294
 
180
295
  def set_pubsub_push_handler(self, pubsub_push_handler_func):
181
296
  self.pubsub_push_handler_func = pubsub_push_handler_func
@@ -183,12 +298,20 @@ class PushNotificationsParser(Protocol):
183
298
  def set_invalidation_push_handler(self, invalidation_push_handler_func):
184
299
  self.invalidation_push_handler_func = invalidation_push_handler_func
185
300
 
301
+ def set_node_moving_push_handler(self, node_moving_push_handler_func):
302
+ self.node_moving_push_handler_func = node_moving_push_handler_func
303
+
304
+ def set_maintenance_push_handler(self, maintenance_push_handler_func):
305
+ self.maintenance_push_handler_func = maintenance_push_handler_func
306
+
186
307
 
187
308
  class AsyncPushNotificationsParser(Protocol):
188
309
  """Protocol defining async RESP3-specific parsing functionality"""
189
310
 
190
311
  pubsub_push_handler_func: Callable
191
312
  invalidation_push_handler_func: Optional[Callable] = None
313
+ node_moving_push_handler_func: Optional[Callable[..., Awaitable[None]]] = None
314
+ maintenance_push_handler_func: Optional[Callable[..., Awaitable[None]]] = None
192
315
 
193
316
  async def handle_pubsub_push_response(self, response):
194
317
  """Handle pubsub push responses asynchronously"""
@@ -196,10 +319,46 @@ class AsyncPushNotificationsParser(Protocol):
196
319
 
197
320
  async def handle_push_response(self, response, **kwargs):
198
321
  """Handle push responses asynchronously"""
199
- if response[0] not in _INVALIDATION_MESSAGE:
322
+
323
+ msg_type = response[0]
324
+ if isinstance(msg_type, bytes):
325
+ msg_type = msg_type.decode()
326
+
327
+ if msg_type not in (
328
+ _INVALIDATION_MESSAGE,
329
+ *_MAINTENANCE_MESSAGES,
330
+ _MOVING_MESSAGE,
331
+ ):
200
332
  return await self.pubsub_push_handler_func(response)
201
- if self.invalidation_push_handler_func:
202
- return await self.invalidation_push_handler_func(response)
333
+
334
+ try:
335
+ if (
336
+ msg_type == _INVALIDATION_MESSAGE
337
+ and self.invalidation_push_handler_func
338
+ ):
339
+ return await self.invalidation_push_handler_func(response)
340
+
341
+ if isinstance(msg_type, bytes):
342
+ msg_type = msg_type.decode()
343
+
344
+ if msg_type == _MOVING_MESSAGE and self.node_moving_push_handler_func:
345
+ parser_function = MSG_TYPE_TO_EVENT_PARSER_MAPPING[msg_type][1]
346
+ notification = parser_function(response)
347
+ return await self.node_moving_push_handler_func(notification)
348
+
349
+ if msg_type in _MAINTENANCE_MESSAGES and self.maintenance_push_handler_func:
350
+ parser_function = MSG_TYPE_TO_EVENT_PARSER_MAPPING[msg_type][1]
351
+ notification_type = MSG_TYPE_TO_EVENT_PARSER_MAPPING[msg_type][0]
352
+ notification = parser_function(response, notification_type)
353
+
354
+ if notification is not None:
355
+ return await self.maintenance_push_handler_func(notification)
356
+ except Exception as e:
357
+ logger.error(
358
+ "Error handling {} message ({}): {}".format(msg_type, response, e)
359
+ )
360
+
361
+ return None
203
362
 
204
363
  def set_pubsub_push_handler(self, pubsub_push_handler_func):
205
364
  """Set the pubsub push handler function"""
@@ -209,6 +368,12 @@ class AsyncPushNotificationsParser(Protocol):
209
368
  """Set the invalidation push handler function"""
210
369
  self.invalidation_push_handler_func = invalidation_push_handler_func
211
370
 
371
+ def set_node_moving_push_handler(self, node_moving_push_handler_func):
372
+ self.node_moving_push_handler_func = node_moving_push_handler_func
373
+
374
+ def set_maintenance_push_handler(self, maintenance_push_handler_func):
375
+ self.maintenance_push_handler_func = maintenance_push_handler_func
376
+
212
377
 
213
378
  class _AsyncRESPBase(AsyncBaseParser):
214
379
  """Base class for async resp parsing"""
@@ -47,6 +47,8 @@ class _HiredisParser(BaseParser, PushNotificationsParser):
47
47
  self.socket_read_size = socket_read_size
48
48
  self._buffer = bytearray(socket_read_size)
49
49
  self.pubsub_push_handler_func = self.handle_pubsub_push_response
50
+ self.node_moving_push_handler_func = None
51
+ self.maintenance_push_handler_func = None
50
52
  self.invalidation_push_handler_func = None
51
53
  self._hiredis_PushNotificationType = None
52
54
 
@@ -141,12 +143,15 @@ class _HiredisParser(BaseParser, PushNotificationsParser):
141
143
  response, self._hiredis_PushNotificationType
142
144
  ):
143
145
  response = self.handle_push_response(response)
144
- if not push_request:
145
- return self.read_response(
146
- disable_decoding=disable_decoding, push_request=push_request
147
- )
148
- else:
146
+
147
+ # if this is a push request return the push response
148
+ if push_request:
149
149
  return response
150
+
151
+ return self.read_response(
152
+ disable_decoding=disable_decoding,
153
+ push_request=push_request,
154
+ )
150
155
  return response
151
156
 
152
157
  if disable_decoding:
@@ -169,12 +174,13 @@ class _HiredisParser(BaseParser, PushNotificationsParser):
169
174
  response, self._hiredis_PushNotificationType
170
175
  ):
171
176
  response = self.handle_push_response(response)
172
- if not push_request:
173
- return self.read_response(
174
- disable_decoding=disable_decoding, push_request=push_request
175
- )
176
- else:
177
+ if push_request:
177
178
  return response
179
+ return self.read_response(
180
+ disable_decoding=disable_decoding,
181
+ push_request=push_request,
182
+ )
183
+
178
184
  elif (
179
185
  isinstance(response, list)
180
186
  and response
@@ -18,6 +18,8 @@ class _RESP3Parser(_RESPBase, PushNotificationsParser):
18
18
  def __init__(self, socket_read_size):
19
19
  super().__init__(socket_read_size)
20
20
  self.pubsub_push_handler_func = self.handle_pubsub_push_response
21
+ self.node_moving_push_handler_func = None
22
+ self.maintenance_push_handler_func = None
21
23
  self.invalidation_push_handler_func = None
22
24
 
23
25
  def handle_pubsub_push_response(self, response):
@@ -117,17 +119,21 @@ class _RESP3Parser(_RESPBase, PushNotificationsParser):
117
119
  for _ in range(int(response))
118
120
  ]
119
121
  response = self.handle_push_response(response)
120
- if not push_request:
121
- return self._read_response(
122
- disable_decoding=disable_decoding, push_request=push_request
123
- )
124
- else:
122
+
123
+ # if this is a push request return the push response
124
+ if push_request:
125
125
  return response
126
+
127
+ return self._read_response(
128
+ disable_decoding=disable_decoding,
129
+ push_request=push_request,
130
+ )
126
131
  else:
127
132
  raise InvalidResponse(f"Protocol Error: {raw!r}")
128
133
 
129
134
  if isinstance(response, bytes) and disable_decoding is False:
130
135
  response = self.encoder.decode(response)
136
+
131
137
  return response
132
138
 
133
139
 
@@ -387,6 +387,12 @@ class Redis(
387
387
  # on a set of redis commands
388
388
  self._single_conn_lock = asyncio.Lock()
389
389
 
390
+ # When used as an async context manager, we need to increment and decrement
391
+ # a usage counter so that we can close the connection pool when no one is
392
+ # using the client.
393
+ self._usage_counter = 0
394
+ self._usage_lock = asyncio.Lock()
395
+
390
396
  def __repr__(self):
391
397
  return (
392
398
  f"<{self.__class__.__module__}.{self.__class__.__name__}"
@@ -594,10 +600,47 @@ class Redis(
594
600
  )
595
601
 
596
602
  async def __aenter__(self: _RedisT) -> _RedisT:
597
- return await self.initialize()
603
+ """
604
+ Async context manager entry. Increments a usage counter so that the
605
+ connection pool is only closed (via aclose()) when no context is using
606
+ the client.
607
+ """
608
+ await self._increment_usage()
609
+ try:
610
+ # Initialize the client (i.e. establish connection, etc.)
611
+ return await self.initialize()
612
+ except Exception:
613
+ # If initialization fails, decrement the counter to keep it in sync
614
+ await self._decrement_usage()
615
+ raise
616
+
617
+ async def _increment_usage(self) -> int:
618
+ """
619
+ Helper coroutine to increment the usage counter while holding the lock.
620
+ Returns the new value of the usage counter.
621
+ """
622
+ async with self._usage_lock:
623
+ self._usage_counter += 1
624
+ return self._usage_counter
625
+
626
+ async def _decrement_usage(self) -> int:
627
+ """
628
+ Helper coroutine to decrement the usage counter while holding the lock.
629
+ Returns the new value of the usage counter.
630
+ """
631
+ async with self._usage_lock:
632
+ self._usage_counter -= 1
633
+ return self._usage_counter
598
634
 
599
635
  async def __aexit__(self, exc_type, exc_value, traceback):
600
- await self.aclose()
636
+ """
637
+ Async context manager exit. Decrements a usage counter. If this is the
638
+ last exit (counter becomes zero), the client closes its connection pool.
639
+ """
640
+ current_usage = await asyncio.shield(self._decrement_usage())
641
+ if current_usage == 0:
642
+ # This was the last active context, so disconnect the pool.
643
+ await asyncio.shield(self.aclose())
601
644
 
602
645
  _DEL_MESSAGE = "Unclosed Redis client"
603
646
 
@@ -431,6 +431,12 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
431
431
  self._initialize = True
432
432
  self._lock: Optional[asyncio.Lock] = None
433
433
 
434
+ # When used as an async context manager, we need to increment and decrement
435
+ # a usage counter so that we can close the connection pool when no one is
436
+ # using the client.
437
+ self._usage_counter = 0
438
+ self._usage_lock = asyncio.Lock()
439
+
434
440
  async def initialize(self) -> "RedisCluster":
435
441
  """Get all nodes from startup nodes & creates connections if not initialized."""
436
442
  if self._initialize:
@@ -467,10 +473,47 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
467
473
  await self.aclose()
468
474
 
469
475
  async def __aenter__(self) -> "RedisCluster":
470
- return await self.initialize()
476
+ """
477
+ Async context manager entry. Increments a usage counter so that the
478
+ connection pool is only closed (via aclose()) when no context is using
479
+ the client.
480
+ """
481
+ await self._increment_usage()
482
+ try:
483
+ # Initialize the client (i.e. establish connection, etc.)
484
+ return await self.initialize()
485
+ except Exception:
486
+ # If initialization fails, decrement the counter to keep it in sync
487
+ await self._decrement_usage()
488
+ raise
471
489
 
472
- async def __aexit__(self, exc_type: None, exc_value: None, traceback: None) -> None:
473
- await self.aclose()
490
+ async def _increment_usage(self) -> int:
491
+ """
492
+ Helper coroutine to increment the usage counter while holding the lock.
493
+ Returns the new value of the usage counter.
494
+ """
495
+ async with self._usage_lock:
496
+ self._usage_counter += 1
497
+ return self._usage_counter
498
+
499
+ async def _decrement_usage(self) -> int:
500
+ """
501
+ Helper coroutine to decrement the usage counter while holding the lock.
502
+ Returns the new value of the usage counter.
503
+ """
504
+ async with self._usage_lock:
505
+ self._usage_counter -= 1
506
+ return self._usage_counter
507
+
508
+ async def __aexit__(self, exc_type, exc_value, traceback):
509
+ """
510
+ Async context manager exit. Decrements a usage counter. If this is the
511
+ last exit (counter becomes zero), the client closes its connection pool.
512
+ """
513
+ current_usage = await asyncio.shield(self._decrement_usage())
514
+ if current_usage == 0:
515
+ # This was the last active context, so disconnect the pool.
516
+ await asyncio.shield(self.aclose())
474
517
 
475
518
  def __await__(self) -> Generator[Any, None, "RedisCluster"]:
476
519
  return self.initialize().__await__()
@@ -1587,15 +1630,6 @@ class ClusterPipeline(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterComm
1587
1630
  def __await__(self) -> Generator[Any, None, "ClusterPipeline"]:
1588
1631
  return self.initialize().__await__()
1589
1632
 
1590
- def __enter__(self) -> "ClusterPipeline":
1591
- # TODO: Remove this method before 7.0.0
1592
- self._execution_strategy._command_queue = []
1593
- return self
1594
-
1595
- def __exit__(self, exc_type: None, exc_value: None, traceback: None) -> None:
1596
- # TODO: Remove this method before 7.0.0
1597
- self._execution_strategy._command_queue = []
1598
-
1599
1633
  def __bool__(self) -> bool:
1600
1634
  "Pipeline instances should always evaluate to True on Python 3+"
1601
1635
  return True
@@ -50,6 +50,7 @@ class EvictionPolicyInterface(ABC):
50
50
  pass
51
51
 
52
52
  @cache.setter
53
+ @abstractmethod
53
54
  def cache(self, value):
54
55
  pass
55
56