redis 5.2.0__tar.gz → 5.3.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 (163) hide show
  1. {redis-5.2.0/redis.egg-info → redis-5.3.0b1}/PKG-INFO +1 -1
  2. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/helpers.py +11 -4
  3. {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/client.py +42 -1
  4. {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/cluster.py +54 -0
  5. {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/connection.py +72 -9
  6. {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/sentinel.py +1 -5
  7. redis-5.3.0b1/redis/auth/err.py +31 -0
  8. redis-5.3.0b1/redis/auth/idp.py +28 -0
  9. redis-5.3.0b1/redis/auth/token.py +126 -0
  10. redis-5.3.0b1/redis/auth/token_manager.py +370 -0
  11. {redis-5.2.0 → redis-5.3.0b1}/redis/client.py +51 -3
  12. {redis-5.2.0 → redis-5.3.0b1}/redis/cluster.py +39 -0
  13. {redis-5.2.0 → redis-5.3.0b1}/redis/connection.py +73 -0
  14. redis-5.3.0b1/redis/credentials.py +65 -0
  15. redis-5.3.0b1/redis/event.py +394 -0
  16. {redis-5.2.0 → redis-5.3.0b1/redis.egg-info}/PKG-INFO +1 -1
  17. {redis-5.2.0 → redis-5.3.0b1}/redis.egg-info/SOURCES.txt +9 -0
  18. {redis-5.2.0 → redis-5.3.0b1}/setup.py +2 -1
  19. {redis-5.2.0 → redis-5.3.0b1}/tests/conftest.py +191 -24
  20. redis-5.3.0b1/tests/ssl_utils.py +43 -0
  21. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/conftest.py +139 -0
  22. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_cluster.py +22 -29
  23. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_connect.py +16 -13
  24. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_connection_pool.py +7 -0
  25. redis-5.3.0b1/tests/test_asyncio/test_credentials.py +696 -0
  26. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_cwe_404.py +2 -2
  27. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_graph.py +20 -20
  28. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_hash.py +7 -6
  29. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_search.py +2 -0
  30. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_sentinel.py +20 -0
  31. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_timeseries.py +5 -0
  32. redis-5.3.0b1/tests/test_auth/__init__.py +0 -0
  33. redis-5.3.0b1/tests/test_auth/test_token.py +76 -0
  34. redis-5.3.0b1/tests/test_auth/test_token_manager.py +566 -0
  35. {redis-5.2.0 → redis-5.3.0b1}/tests/test_cluster.py +7 -6
  36. {redis-5.2.0 → redis-5.3.0b1}/tests/test_connect.py +14 -13
  37. {redis-5.2.0 → redis-5.3.0b1}/tests/test_connection.py +4 -0
  38. redis-5.3.0b1/tests/test_credentials.py +659 -0
  39. {redis-5.2.0 → redis-5.3.0b1}/tests/test_graph.py +23 -23
  40. redis-5.3.0b1/tests/test_graph_utils/__init__.py +0 -0
  41. {redis-5.2.0 → redis-5.3.0b1}/tests/test_graph_utils/test_edge.py +4 -4
  42. {redis-5.2.0 → redis-5.3.0b1}/tests/test_graph_utils/test_node.py +3 -3
  43. {redis-5.2.0 → redis-5.3.0b1}/tests/test_graph_utils/test_path.py +5 -5
  44. {redis-5.2.0 → redis-5.3.0b1}/tests/test_hash.py +7 -6
  45. {redis-5.2.0 → redis-5.3.0b1}/tests/test_json.py +2 -2
  46. {redis-5.2.0 → redis-5.3.0b1}/tests/test_search.py +13 -0
  47. {redis-5.2.0 → redis-5.3.0b1}/tests/test_ssl.py +29 -26
  48. redis-5.2.0/redis/credentials.py +0 -26
  49. redis-5.2.0/tests/ssl_utils.py +0 -14
  50. redis-5.2.0/tests/test_asyncio/test_credentials.py +0 -283
  51. redis-5.2.0/tests/test_credentials.py +0 -250
  52. {redis-5.2.0 → redis-5.3.0b1}/INSTALL +0 -0
  53. {redis-5.2.0 → redis-5.3.0b1}/LICENSE +0 -0
  54. {redis-5.2.0 → redis-5.3.0b1}/MANIFEST.in +0 -0
  55. {redis-5.2.0 → redis-5.3.0b1}/README.md +0 -0
  56. {redis-5.2.0 → redis-5.3.0b1}/redis/__init__.py +0 -0
  57. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/__init__.py +0 -0
  58. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/base.py +0 -0
  59. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/commands.py +0 -0
  60. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/encoders.py +0 -0
  61. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/hiredis.py +0 -0
  62. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/resp2.py +0 -0
  63. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/resp3.py +0 -0
  64. {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/socket.py +0 -0
  65. {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/__init__.py +0 -0
  66. {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/lock.py +0 -0
  67. {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/retry.py +0 -0
  68. {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/utils.py +0 -0
  69. {redis-5.2.0/tests → redis-5.3.0b1/redis/auth}/__init__.py +0 -0
  70. {redis-5.2.0 → redis-5.3.0b1}/redis/backoff.py +0 -0
  71. {redis-5.2.0 → redis-5.3.0b1}/redis/cache.py +0 -0
  72. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/__init__.py +0 -0
  73. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/bf/__init__.py +0 -0
  74. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/bf/commands.py +0 -0
  75. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/bf/info.py +0 -0
  76. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/cluster.py +0 -0
  77. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/core.py +0 -0
  78. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/__init__.py +0 -0
  79. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/commands.py +0 -0
  80. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/edge.py +0 -0
  81. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/exceptions.py +0 -0
  82. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/execution_plan.py +0 -0
  83. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/node.py +0 -0
  84. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/path.py +0 -0
  85. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/query_result.py +0 -0
  86. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/helpers.py +0 -0
  87. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/__init__.py +0 -0
  88. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/_util.py +0 -0
  89. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/commands.py +0 -0
  90. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/decoders.py +0 -0
  91. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/path.py +0 -0
  92. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/redismodules.py +0 -0
  93. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/__init__.py +0 -0
  94. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/_util.py +0 -0
  95. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/aggregation.py +0 -0
  96. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/commands.py +0 -0
  97. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/document.py +0 -0
  98. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/field.py +0 -0
  99. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/indexDefinition.py +0 -0
  100. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/query.py +0 -0
  101. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/querystring.py +0 -0
  102. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/reducers.py +0 -0
  103. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/result.py +0 -0
  104. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/suggestion.py +0 -0
  105. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/sentinel.py +0 -0
  106. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/timeseries/__init__.py +0 -0
  107. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/timeseries/commands.py +0 -0
  108. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/timeseries/info.py +0 -0
  109. {redis-5.2.0 → redis-5.3.0b1}/redis/commands/timeseries/utils.py +0 -0
  110. {redis-5.2.0 → redis-5.3.0b1}/redis/crc.py +0 -0
  111. {redis-5.2.0 → redis-5.3.0b1}/redis/exceptions.py +0 -0
  112. {redis-5.2.0 → redis-5.3.0b1}/redis/lock.py +0 -0
  113. {redis-5.2.0 → redis-5.3.0b1}/redis/ocsp.py +0 -0
  114. {redis-5.2.0 → redis-5.3.0b1}/redis/retry.py +0 -0
  115. {redis-5.2.0 → redis-5.3.0b1}/redis/sentinel.py +0 -0
  116. {redis-5.2.0 → redis-5.3.0b1}/redis/typing.py +0 -0
  117. {redis-5.2.0 → redis-5.3.0b1}/redis/utils.py +0 -0
  118. {redis-5.2.0 → redis-5.3.0b1}/redis.egg-info/dependency_links.txt +0 -0
  119. {redis-5.2.0 → redis-5.3.0b1}/redis.egg-info/requires.txt +0 -0
  120. {redis-5.2.0 → redis-5.3.0b1}/redis.egg-info/top_level.txt +0 -0
  121. {redis-5.2.0 → redis-5.3.0b1}/setup.cfg +0 -0
  122. {redis-5.2.0/tests/test_asyncio → redis-5.3.0b1/tests}/__init__.py +0 -0
  123. {redis-5.2.0 → redis-5.3.0b1}/tests/mocks.py +0 -0
  124. {redis-5.2.0/tests/test_graph_utils → redis-5.3.0b1/tests/test_asyncio}/__init__.py +0 -0
  125. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/compat.py +0 -0
  126. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/mocks.py +0 -0
  127. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_bloom.py +0 -0
  128. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_commands.py +0 -0
  129. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_connection.py +0 -0
  130. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_encoding.py +0 -0
  131. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_json.py +0 -0
  132. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_lock.py +0 -0
  133. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_monitor.py +0 -0
  134. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_pipeline.py +0 -0
  135. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_pubsub.py +0 -0
  136. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_retry.py +0 -0
  137. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_scripting.py +0 -0
  138. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_sentinel_managed_connection.py +0 -0
  139. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/testdata/jsontestdata.py +0 -0
  140. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/testdata/titles.csv +0 -0
  141. {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/testdata/will_play_text.csv.bz2 +0 -0
  142. {redis-5.2.0 → redis-5.3.0b1}/tests/test_bloom.py +0 -0
  143. {redis-5.2.0 → redis-5.3.0b1}/tests/test_cache.py +0 -0
  144. {redis-5.2.0 → redis-5.3.0b1}/tests/test_command_parser.py +0 -0
  145. {redis-5.2.0 → redis-5.3.0b1}/tests/test_commands.py +0 -0
  146. {redis-5.2.0 → redis-5.3.0b1}/tests/test_connection_pool.py +0 -0
  147. {redis-5.2.0 → redis-5.3.0b1}/tests/test_encoding.py +0 -0
  148. {redis-5.2.0 → redis-5.3.0b1}/tests/test_function.py +0 -0
  149. {redis-5.2.0 → redis-5.3.0b1}/tests/test_helpers.py +0 -0
  150. {redis-5.2.0 → redis-5.3.0b1}/tests/test_lock.py +0 -0
  151. {redis-5.2.0 → redis-5.3.0b1}/tests/test_monitor.py +0 -0
  152. {redis-5.2.0 → redis-5.3.0b1}/tests/test_multiprocessing.py +0 -0
  153. {redis-5.2.0 → redis-5.3.0b1}/tests/test_parsers/test_helpers.py +0 -0
  154. {redis-5.2.0 → redis-5.3.0b1}/tests/test_pipeline.py +0 -0
  155. {redis-5.2.0 → redis-5.3.0b1}/tests/test_pubsub.py +0 -0
  156. {redis-5.2.0 → redis-5.3.0b1}/tests/test_retry.py +0 -0
  157. {redis-5.2.0 → redis-5.3.0b1}/tests/test_scripting.py +0 -0
  158. {redis-5.2.0 → redis-5.3.0b1}/tests/test_sentinel.py +0 -0
  159. {redis-5.2.0 → redis-5.3.0b1}/tests/test_timeseries.py +0 -0
  160. {redis-5.2.0 → redis-5.3.0b1}/tests/test_utils.py +0 -0
  161. {redis-5.2.0 → redis-5.3.0b1}/tests/testdata/jsontestdata.py +0 -0
  162. {redis-5.2.0 → redis-5.3.0b1}/tests/testdata/titles.csv +0 -0
  163. {redis-5.2.0 → redis-5.3.0b1}/tests/testdata/will_play_text.csv.bz2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: redis
3
- Version: 5.2.0
3
+ Version: 5.3.0b1
4
4
  Summary: Python client for Redis database and key-value store
5
5
  Home-page: https://github.com/redis/redis-py
6
6
  Author: Redis Inc.
@@ -396,13 +396,20 @@ def parse_slowlog_get(response, **options):
396
396
  # an O(N) complexity) instead of the command.
397
397
  if isinstance(item[3], list):
398
398
  result["command"] = space.join(item[3])
399
- result["client_address"] = item[4]
400
- result["client_name"] = item[5]
399
+
400
+ # These fields are optional, depends on environment.
401
+ if len(item) >= 6:
402
+ result["client_address"] = item[4]
403
+ result["client_name"] = item[5]
401
404
  else:
402
405
  result["complexity"] = item[3]
403
406
  result["command"] = space.join(item[4])
404
- result["client_address"] = item[5]
405
- result["client_name"] = item[6]
407
+
408
+ # These fields are optional, depends on environment.
409
+ if len(item) >= 7:
410
+ result["client_address"] = item[5]
411
+ result["client_name"] = item[6]
412
+
406
413
  return result
407
414
 
408
415
  return [parse_item(item) for item in response]
@@ -53,6 +53,13 @@ from redis.commands import (
53
53
  list_or_args,
54
54
  )
55
55
  from redis.credentials import CredentialProvider
56
+ from redis.event import (
57
+ AfterPooledConnectionsInstantiationEvent,
58
+ AfterPubSubConnectionInstantiationEvent,
59
+ AfterSingleConnectionInstantiationEvent,
60
+ ClientType,
61
+ EventDispatcher,
62
+ )
56
63
  from redis.exceptions import (
57
64
  ConnectionError,
58
65
  ExecAbortError,
@@ -233,6 +240,7 @@ class Redis(
233
240
  redis_connect_func=None,
234
241
  credential_provider: Optional[CredentialProvider] = None,
235
242
  protocol: Optional[int] = 2,
243
+ event_dispatcher: Optional[EventDispatcher] = None,
236
244
  ):
237
245
  """
238
246
  Initialize a new Redis client.
@@ -242,6 +250,10 @@ class Redis(
242
250
  To retry on TimeoutError, `retry_on_timeout` can also be set to `True`.
243
251
  """
244
252
  kwargs: Dict[str, Any]
253
+ if event_dispatcher is None:
254
+ self._event_dispatcher = EventDispatcher()
255
+ else:
256
+ self._event_dispatcher = event_dispatcher
245
257
  # auto_close_connection_pool only has an effect if connection_pool is
246
258
  # None. It is assumed that if connection_pool is not None, the user
247
259
  # wants to manage the connection pool themselves.
@@ -320,9 +332,19 @@ class Redis(
320
332
  # This arg only used if no pool is passed in
321
333
  self.auto_close_connection_pool = auto_close_connection_pool
322
334
  connection_pool = ConnectionPool(**kwargs)
335
+ self._event_dispatcher.dispatch(
336
+ AfterPooledConnectionsInstantiationEvent(
337
+ [connection_pool], ClientType.ASYNC, credential_provider
338
+ )
339
+ )
323
340
  else:
324
341
  # If a pool is passed in, do not close it
325
342
  self.auto_close_connection_pool = False
343
+ self._event_dispatcher.dispatch(
344
+ AfterPooledConnectionsInstantiationEvent(
345
+ [connection_pool], ClientType.ASYNC, credential_provider
346
+ )
347
+ )
326
348
 
327
349
  self.connection_pool = connection_pool
328
350
  self.single_connection_client = single_connection_client
@@ -354,6 +376,12 @@ class Redis(
354
376
  async with self._single_conn_lock:
355
377
  if self.connection is None:
356
378
  self.connection = await self.connection_pool.get_connection("_")
379
+
380
+ self._event_dispatcher.dispatch(
381
+ AfterSingleConnectionInstantiationEvent(
382
+ self.connection, ClientType.ASYNC, self._single_conn_lock
383
+ )
384
+ )
357
385
  return self
358
386
 
359
387
  def set_response_callback(self, command: str, callback: ResponseCallbackT):
@@ -521,7 +549,9 @@ class Redis(
521
549
  subscribe to channels and listen for messages that get published to
522
550
  them.
523
551
  """
524
- return PubSub(self.connection_pool, **kwargs)
552
+ return PubSub(
553
+ self.connection_pool, event_dispatcher=self._event_dispatcher, **kwargs
554
+ )
525
555
 
526
556
  def monitor(self) -> "Monitor":
527
557
  return Monitor(self.connection_pool)
@@ -759,7 +789,12 @@ class PubSub:
759
789
  ignore_subscribe_messages: bool = False,
760
790
  encoder=None,
761
791
  push_handler_func: Optional[Callable] = None,
792
+ event_dispatcher: Optional["EventDispatcher"] = None,
762
793
  ):
794
+ if event_dispatcher is None:
795
+ self._event_dispatcher = EventDispatcher()
796
+ else:
797
+ self._event_dispatcher = event_dispatcher
763
798
  self.connection_pool = connection_pool
764
799
  self.shard_hint = shard_hint
765
800
  self.ignore_subscribe_messages = ignore_subscribe_messages
@@ -876,6 +911,12 @@ class PubSub:
876
911
  if self.push_handler_func is not None and not HIREDIS_AVAILABLE:
877
912
  self.connection._parser.set_pubsub_push_handler(self.push_handler_func)
878
913
 
914
+ self._event_dispatcher.dispatch(
915
+ AfterPubSubConnectionInstantiationEvent(
916
+ self.connection, self.connection_pool, ClientType.ASYNC, self._lock
917
+ )
918
+ )
919
+
879
920
  async def _disconnect_raise_connect(self, conn, error):
880
921
  """
881
922
  Close the connection and raise an exception
@@ -29,6 +29,7 @@ from redis.asyncio.client import ResponseCallbackT
29
29
  from redis.asyncio.connection import Connection, DefaultParser, SSLConnection, parse_url
30
30
  from redis.asyncio.lock import Lock
31
31
  from redis.asyncio.retry import Retry
32
+ from redis.auth.token import TokenInterface
32
33
  from redis.backoff import default_backoff
33
34
  from redis.client import EMPTY_RESPONSE, NEVER_DECODE, AbstractRedis
34
35
  from redis.cluster import (
@@ -45,6 +46,7 @@ from redis.cluster import (
45
46
  from redis.commands import READ_COMMANDS, AsyncRedisClusterCommands
46
47
  from redis.crc import REDIS_CLUSTER_HASH_SLOTS, key_slot
47
48
  from redis.credentials import CredentialProvider
49
+ from redis.event import AfterAsyncClusterInstantiationEvent, EventDispatcher
48
50
  from redis.exceptions import (
49
51
  AskError,
50
52
  BusyLoadingError,
@@ -57,6 +59,7 @@ from redis.exceptions import (
57
59
  MaxConnectionsError,
58
60
  MovedError,
59
61
  RedisClusterException,
62
+ RedisError,
60
63
  ResponseError,
61
64
  SlotNotCoveredError,
62
65
  TimeoutError,
@@ -270,6 +273,7 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
270
273
  ssl_ciphers: Optional[str] = None,
271
274
  protocol: Optional[int] = 2,
272
275
  address_remap: Optional[Callable[[Tuple[str, int]], Tuple[str, int]]] = None,
276
+ event_dispatcher: Optional[EventDispatcher] = None,
273
277
  ) -> None:
274
278
  if db:
275
279
  raise RedisClusterException(
@@ -366,11 +370,17 @@ class RedisCluster(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterCommand
366
370
  if host and port:
367
371
  startup_nodes.append(ClusterNode(host, port, **self.connection_kwargs))
368
372
 
373
+ if event_dispatcher is None:
374
+ self._event_dispatcher = EventDispatcher()
375
+ else:
376
+ self._event_dispatcher = event_dispatcher
377
+
369
378
  self.nodes_manager = NodesManager(
370
379
  startup_nodes,
371
380
  require_full_coverage,
372
381
  kwargs,
373
382
  address_remap=address_remap,
383
+ event_dispatcher=self._event_dispatcher,
374
384
  )
375
385
  self.encoder = Encoder(encoding, encoding_errors, decode_responses)
376
386
  self.read_from_replicas = read_from_replicas
@@ -929,6 +939,8 @@ class ClusterNode:
929
939
  __slots__ = (
930
940
  "_connections",
931
941
  "_free",
942
+ "_lock",
943
+ "_event_dispatcher",
932
944
  "connection_class",
933
945
  "connection_kwargs",
934
946
  "host",
@@ -966,6 +978,9 @@ class ClusterNode:
966
978
 
967
979
  self._connections: List[Connection] = []
968
980
  self._free: Deque[Connection] = collections.deque(maxlen=self.max_connections)
981
+ self._event_dispatcher = self.connection_kwargs.get("event_dispatcher", None)
982
+ if self._event_dispatcher is None:
983
+ self._event_dispatcher = EventDispatcher()
969
984
 
970
985
  def __repr__(self) -> str:
971
986
  return (
@@ -1082,10 +1097,38 @@ class ClusterNode:
1082
1097
 
1083
1098
  return ret
1084
1099
 
1100
+ async def re_auth_callback(self, token: TokenInterface):
1101
+ tmp_queue = collections.deque()
1102
+ while self._free:
1103
+ conn = self._free.popleft()
1104
+ await conn.retry.call_with_retry(
1105
+ lambda: conn.send_command(
1106
+ "AUTH", token.try_get("oid"), token.get_value()
1107
+ ),
1108
+ lambda error: self._mock(error),
1109
+ )
1110
+ await conn.retry.call_with_retry(
1111
+ lambda: conn.read_response(), lambda error: self._mock(error)
1112
+ )
1113
+ tmp_queue.append(conn)
1114
+
1115
+ while tmp_queue:
1116
+ conn = tmp_queue.popleft()
1117
+ self._free.append(conn)
1118
+
1119
+ async def _mock(self, error: RedisError):
1120
+ """
1121
+ Dummy functions, needs to be passed as error callback to retry object.
1122
+ :param error:
1123
+ :return:
1124
+ """
1125
+ pass
1126
+
1085
1127
 
1086
1128
  class NodesManager:
1087
1129
  __slots__ = (
1088
1130
  "_moved_exception",
1131
+ "_event_dispatcher",
1089
1132
  "connection_kwargs",
1090
1133
  "default_node",
1091
1134
  "nodes_cache",
@@ -1102,6 +1145,7 @@ class NodesManager:
1102
1145
  require_full_coverage: bool,
1103
1146
  connection_kwargs: Dict[str, Any],
1104
1147
  address_remap: Optional[Callable[[Tuple[str, int]], Tuple[str, int]]] = None,
1148
+ event_dispatcher: Optional[EventDispatcher] = None,
1105
1149
  ) -> None:
1106
1150
  self.startup_nodes = {node.name: node for node in startup_nodes}
1107
1151
  self.require_full_coverage = require_full_coverage
@@ -1113,6 +1157,10 @@ class NodesManager:
1113
1157
  self.slots_cache: Dict[int, List["ClusterNode"]] = {}
1114
1158
  self.read_load_balancer = LoadBalancer()
1115
1159
  self._moved_exception: MovedError = None
1160
+ if event_dispatcher is None:
1161
+ self._event_dispatcher = EventDispatcher()
1162
+ else:
1163
+ self._event_dispatcher = event_dispatcher
1116
1164
 
1117
1165
  def get_node(
1118
1166
  self,
@@ -1230,6 +1278,12 @@ class NodesManager:
1230
1278
  try:
1231
1279
  # Make sure cluster mode is enabled on this node
1232
1280
  try:
1281
+ self._event_dispatcher.dispatch(
1282
+ AfterAsyncClusterInstantiationEvent(
1283
+ self.nodes_cache,
1284
+ self.connection_kwargs.get("credential_provider", None),
1285
+ )
1286
+ )
1233
1287
  cluster_slots = await startup_node.execute_command("CLUSTER SLOTS")
1234
1288
  except ResponseError:
1235
1289
  raise RedisClusterException(
@@ -27,6 +27,8 @@ from typing import (
27
27
  )
28
28
  from urllib.parse import ParseResult, parse_qs, unquote, urlparse
29
29
 
30
+ from ..auth.token import TokenInterface
31
+ from ..event import AsyncAfterConnectionReleasedEvent, EventDispatcher
30
32
  from ..utils import format_error_message
31
33
 
32
34
  # the functionality is available in 3.11.x but has a major issue before
@@ -148,6 +150,7 @@ class AbstractConnection:
148
150
  encoder_class: Type[Encoder] = Encoder,
149
151
  credential_provider: Optional[CredentialProvider] = None,
150
152
  protocol: Optional[int] = 2,
153
+ event_dispatcher: Optional[EventDispatcher] = None,
151
154
  ):
152
155
  if (username or password) and credential_provider is not None:
153
156
  raise DataError(
@@ -156,6 +159,10 @@ class AbstractConnection:
156
159
  "1. 'password' and (optional) 'username'\n"
157
160
  "2. 'credential_provider'"
158
161
  )
162
+ if event_dispatcher is None:
163
+ self._event_dispatcher = EventDispatcher()
164
+ else:
165
+ self._event_dispatcher = event_dispatcher
159
166
  self.db = db
160
167
  self.client_name = client_name
161
168
  self.lib_name = lib_name
@@ -195,6 +202,8 @@ class AbstractConnection:
195
202
  self.set_parser(parser_class)
196
203
  self._connect_callbacks: List[weakref.WeakMethod[ConnectCallbackT]] = []
197
204
  self._buffer_cutoff = 6000
205
+ self._re_auth_token: Optional[TokenInterface] = None
206
+
198
207
  try:
199
208
  p = int(protocol)
200
209
  except TypeError:
@@ -214,7 +223,13 @@ class AbstractConnection:
214
223
  _warnings.warn(
215
224
  f"unclosed Connection {self!r}", ResourceWarning, source=self
216
225
  )
217
- self._close()
226
+
227
+ try:
228
+ asyncio.get_running_loop()
229
+ self._close()
230
+ except RuntimeError:
231
+ # No actions been taken if pool already closed.
232
+ pass
218
233
 
219
234
  def _close(self):
220
235
  """
@@ -321,6 +336,9 @@ class AbstractConnection:
321
336
  def _error_message(self, exception: BaseException) -> str:
322
337
  return format_error_message(self._host_error(), exception)
323
338
 
339
+ def get_protocol(self):
340
+ return self.protocol
341
+
324
342
  async def on_connect(self) -> None:
325
343
  """Initialize the connection, authenticate and select a database"""
326
344
  self._parser.on_connect(self)
@@ -333,7 +351,8 @@ class AbstractConnection:
333
351
  self.credential_provider
334
352
  or UsernamePasswordCredentialProvider(self.username, self.password)
335
353
  )
336
- auth_args = cred_provider.get_credentials()
354
+ auth_args = await cred_provider.get_credentials_async()
355
+
337
356
  # if resp version is specified and we have auth args,
338
357
  # we need to send them via HELLO
339
358
  if auth_args and self.protocol not in [2, "2"]:
@@ -655,6 +674,19 @@ class AbstractConnection:
655
674
  while not self._socket_is_empty():
656
675
  await self.read_response(push_request=True)
657
676
 
677
+ def set_re_auth_token(self, token: TokenInterface):
678
+ self._re_auth_token = token
679
+
680
+ async def re_auth(self):
681
+ if self._re_auth_token is not None:
682
+ await self.send_command(
683
+ "AUTH",
684
+ self._re_auth_token.try_get("oid"),
685
+ self._re_auth_token.get_value(),
686
+ )
687
+ await self.read_response()
688
+ self._re_auth_token = None
689
+
658
690
 
659
691
  class Connection(AbstractConnection):
660
692
  "Manages TCP communication to and from a Redis server"
@@ -1033,6 +1065,10 @@ class ConnectionPool:
1033
1065
  self._available_connections: List[AbstractConnection] = []
1034
1066
  self._in_use_connections: Set[AbstractConnection] = set()
1035
1067
  self.encoder_class = self.connection_kwargs.get("encoder_class", Encoder)
1068
+ self._lock = asyncio.Lock()
1069
+ self._event_dispatcher = self.connection_kwargs.get("event_dispatcher", None)
1070
+ if self._event_dispatcher is None:
1071
+ self._event_dispatcher = EventDispatcher()
1036
1072
 
1037
1073
  def __repr__(self):
1038
1074
  return (
@@ -1052,13 +1088,14 @@ class ConnectionPool:
1052
1088
  )
1053
1089
 
1054
1090
  async def get_connection(self, command_name, *keys, **options):
1055
- """Get a connected connection from the pool"""
1056
- connection = self.get_available_connection()
1057
- try:
1058
- await self.ensure_connection(connection)
1059
- except BaseException:
1060
- await self.release(connection)
1061
- raise
1091
+ async with self._lock:
1092
+ """Get a connected connection from the pool"""
1093
+ connection = self.get_available_connection()
1094
+ try:
1095
+ await self.ensure_connection(connection)
1096
+ except BaseException:
1097
+ await self.release(connection)
1098
+ raise
1062
1099
 
1063
1100
  return connection
1064
1101
 
@@ -1108,6 +1145,9 @@ class ConnectionPool:
1108
1145
  # not doing so is an error that will cause an exception here.
1109
1146
  self._in_use_connections.remove(connection)
1110
1147
  self._available_connections.append(connection)
1148
+ await self._event_dispatcher.dispatch_async(
1149
+ AsyncAfterConnectionReleasedEvent(connection)
1150
+ )
1111
1151
 
1112
1152
  async def disconnect(self, inuse_connections: bool = True):
1113
1153
  """
@@ -1141,6 +1181,29 @@ class ConnectionPool:
1141
1181
  for conn in self._in_use_connections:
1142
1182
  conn.retry = retry
1143
1183
 
1184
+ async def re_auth_callback(self, token: TokenInterface):
1185
+ async with self._lock:
1186
+ for conn in self._available_connections:
1187
+ await conn.retry.call_with_retry(
1188
+ lambda: conn.send_command(
1189
+ "AUTH", token.try_get("oid"), token.get_value()
1190
+ ),
1191
+ lambda error: self._mock(error),
1192
+ )
1193
+ await conn.retry.call_with_retry(
1194
+ lambda: conn.read_response(), lambda error: self._mock(error)
1195
+ )
1196
+ for conn in self._in_use_connections:
1197
+ conn.set_re_auth_token(token)
1198
+
1199
+ async def _mock(self, error: RedisError):
1200
+ """
1201
+ Dummy functions, needs to be passed as error callback to retry object.
1202
+ :param error:
1203
+ :return:
1204
+ """
1205
+ pass
1206
+
1144
1207
 
1145
1208
  class BlockingConnectionPool(ConnectionPool):
1146
1209
  """
@@ -29,11 +29,7 @@ class SentinelManagedConnection(Connection):
29
29
  super().__init__(**kwargs)
30
30
 
31
31
  def __repr__(self):
32
- pool = self.connection_pool
33
- s = (
34
- f"<{self.__class__.__module__}.{self.__class__.__name__}"
35
- f"(service={pool.service_name}"
36
- )
32
+ s = f"<{self.__class__.__module__}.{self.__class__.__name__}"
37
33
  if self.host:
38
34
  host_info = f",host={self.host},port={self.port}"
39
35
  s += host_info
@@ -0,0 +1,31 @@
1
+ from typing import Iterable
2
+
3
+
4
+ class RequestTokenErr(Exception):
5
+ """
6
+ Represents an exception during token request.
7
+ """
8
+
9
+ def __init__(self, *args):
10
+ super().__init__(*args)
11
+
12
+
13
+ class InvalidTokenSchemaErr(Exception):
14
+ """
15
+ Represents an exception related to invalid token schema.
16
+ """
17
+
18
+ def __init__(self, missing_fields: Iterable[str] = []):
19
+ super().__init__(
20
+ "Unexpected token schema. Following fields are missing: "
21
+ + ", ".join(missing_fields)
22
+ )
23
+
24
+
25
+ class TokenRenewalErr(Exception):
26
+ """
27
+ Represents an exception during token renewal process.
28
+ """
29
+
30
+ def __init__(self, *args):
31
+ super().__init__(*args)
@@ -0,0 +1,28 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from redis.auth.token import TokenInterface
4
+
5
+ """
6
+ This interface is the facade of an identity provider
7
+ """
8
+
9
+
10
+ class IdentityProviderInterface(ABC):
11
+ """
12
+ Receive a token from the identity provider.
13
+ Receiving a token only works when being authenticated.
14
+ """
15
+
16
+ @abstractmethod
17
+ def request_token(self, force_refresh=False) -> TokenInterface:
18
+ pass
19
+
20
+
21
+ class IdentityProviderConfigInterface(ABC):
22
+ """
23
+ Configuration class that provides a configured identity provider.
24
+ """
25
+
26
+ @abstractmethod
27
+ def get_provider(self) -> IdentityProviderInterface:
28
+ pass
@@ -0,0 +1,126 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime, timezone
3
+
4
+ import jwt
5
+ from redis.auth.err import InvalidTokenSchemaErr
6
+
7
+
8
+ class TokenInterface(ABC):
9
+ @abstractmethod
10
+ def is_expired(self) -> bool:
11
+ pass
12
+
13
+ @abstractmethod
14
+ def ttl(self) -> float:
15
+ pass
16
+
17
+ @abstractmethod
18
+ def try_get(self, key: str) -> str:
19
+ pass
20
+
21
+ @abstractmethod
22
+ def get_value(self) -> str:
23
+ pass
24
+
25
+ @abstractmethod
26
+ def get_expires_at_ms(self) -> float:
27
+ pass
28
+
29
+ @abstractmethod
30
+ def get_received_at_ms(self) -> float:
31
+ pass
32
+
33
+
34
+ class TokenResponse:
35
+ def __init__(self, token: TokenInterface):
36
+ self._token = token
37
+
38
+ def get_token(self) -> TokenInterface:
39
+ return self._token
40
+
41
+ def get_ttl_ms(self) -> float:
42
+ return self._token.get_expires_at_ms() - self._token.get_received_at_ms()
43
+
44
+
45
+ class SimpleToken(TokenInterface):
46
+ def __init__(
47
+ self, value: str, expires_at_ms: float, received_at_ms: float, claims: dict
48
+ ) -> None:
49
+ self.value = value
50
+ self.expires_at = expires_at_ms
51
+ self.received_at = received_at_ms
52
+ self.claims = claims
53
+
54
+ def ttl(self) -> float:
55
+ if self.expires_at == -1:
56
+ return -1
57
+
58
+ return self.expires_at - (datetime.now(timezone.utc).timestamp() * 1000)
59
+
60
+ def is_expired(self) -> bool:
61
+ if self.expires_at == -1:
62
+ return False
63
+
64
+ return self.ttl() <= 0
65
+
66
+ def try_get(self, key: str) -> str:
67
+ return self.claims.get(key)
68
+
69
+ def get_value(self) -> str:
70
+ return self.value
71
+
72
+ def get_expires_at_ms(self) -> float:
73
+ return self.expires_at
74
+
75
+ def get_received_at_ms(self) -> float:
76
+ return self.received_at
77
+
78
+
79
+ class JWToken(TokenInterface):
80
+
81
+ REQUIRED_FIELDS = {"exp"}
82
+
83
+ def __init__(self, token: str):
84
+ self._value = token
85
+ self._decoded = jwt.decode(
86
+ self._value,
87
+ options={"verify_signature": False},
88
+ algorithms=[jwt.get_unverified_header(self._value).get("alg")],
89
+ )
90
+ self._validate_token()
91
+
92
+ def is_expired(self) -> bool:
93
+ exp = self._decoded["exp"]
94
+ if exp == -1:
95
+ return False
96
+
97
+ return (
98
+ self._decoded["exp"] * 1000 <= datetime.now(timezone.utc).timestamp() * 1000
99
+ )
100
+
101
+ def ttl(self) -> float:
102
+ exp = self._decoded["exp"]
103
+ if exp == -1:
104
+ return -1
105
+
106
+ return (
107
+ self._decoded["exp"] * 1000 - datetime.now(timezone.utc).timestamp() * 1000
108
+ )
109
+
110
+ def try_get(self, key: str) -> str:
111
+ return self._decoded.get(key)
112
+
113
+ def get_value(self) -> str:
114
+ return self._value
115
+
116
+ def get_expires_at_ms(self) -> float:
117
+ return float(self._decoded["exp"] * 1000)
118
+
119
+ def get_received_at_ms(self) -> float:
120
+ return datetime.now(timezone.utc).timestamp() * 1000
121
+
122
+ def _validate_token(self):
123
+ actual_fields = {x for x in self._decoded.keys()}
124
+
125
+ if len(self.REQUIRED_FIELDS - actual_fields) != 0:
126
+ raise InvalidTokenSchemaErr(self.REQUIRED_FIELDS - actual_fields)