redis 5.2.1__tar.gz → 5.3.0b3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {redis-5.2.1/redis.egg-info → redis-5.3.0b3}/PKG-INFO +1 -1
- {redis-5.2.1 → redis-5.3.0b3}/redis/asyncio/client.py +42 -1
- {redis-5.2.1 → redis-5.3.0b3}/redis/asyncio/cluster.py +54 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/asyncio/connection.py +65 -8
- redis-5.3.0b3/redis/auth/err.py +31 -0
- redis-5.3.0b3/redis/auth/idp.py +28 -0
- redis-5.3.0b3/redis/auth/token.py +126 -0
- redis-5.3.0b3/redis/auth/token_manager.py +370 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/client.py +51 -3
- {redis-5.2.1 → redis-5.3.0b3}/redis/cluster.py +39 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/connection.py +73 -0
- redis-5.3.0b3/redis/credentials.py +65 -0
- redis-5.3.0b3/redis/event.py +394 -0
- {redis-5.2.1 → redis-5.3.0b3/redis.egg-info}/PKG-INFO +1 -1
- {redis-5.2.1 → redis-5.3.0b3}/redis.egg-info/SOURCES.txt +9 -0
- {redis-5.2.1 → redis-5.3.0b3}/setup.py +2 -1
- {redis-5.2.1 → redis-5.3.0b3}/tests/conftest.py +171 -6
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/conftest.py +139 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_cluster.py +9 -8
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_connection_pool.py +7 -0
- redis-5.3.0b3/tests/test_asyncio/test_credentials.py +696 -0
- redis-5.3.0b3/tests/test_auth/__init__.py +0 -0
- redis-5.3.0b3/tests/test_auth/test_token.py +76 -0
- redis-5.3.0b3/tests/test_auth/test_token_manager.py +566 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_connection.py +4 -0
- redis-5.3.0b3/tests/test_credentials.py +659 -0
- redis-5.3.0b3/tests/test_graph_utils/__init__.py +0 -0
- redis-5.2.1/redis/credentials.py +0 -26
- redis-5.2.1/tests/test_asyncio/test_credentials.py +0 -283
- redis-5.2.1/tests/test_credentials.py +0 -250
- {redis-5.2.1 → redis-5.3.0b3}/INSTALL +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/LICENSE +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/MANIFEST.in +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/README.md +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/base.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/commands.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/encoders.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/helpers.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/hiredis.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/resp2.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/resp3.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/_parsers/socket.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/asyncio/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/asyncio/lock.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/asyncio/retry.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/asyncio/sentinel.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/asyncio/utils.py +0 -0
- {redis-5.2.1/tests → redis-5.3.0b3/redis/auth}/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/backoff.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/cache.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/bf/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/bf/commands.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/bf/info.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/cluster.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/core.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/graph/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/graph/commands.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/graph/edge.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/graph/exceptions.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/graph/execution_plan.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/graph/node.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/graph/path.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/graph/query_result.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/helpers.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/json/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/json/_util.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/json/commands.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/json/decoders.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/json/path.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/redismodules.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/_util.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/aggregation.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/commands.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/document.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/field.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/indexDefinition.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/query.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/querystring.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/reducers.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/result.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/search/suggestion.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/sentinel.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/timeseries/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/timeseries/commands.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/timeseries/info.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/commands/timeseries/utils.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/crc.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/exceptions.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/lock.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/ocsp.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/retry.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/sentinel.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/typing.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis/utils.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis.egg-info/dependency_links.txt +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis.egg-info/requires.txt +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/redis.egg-info/top_level.txt +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/setup.cfg +0 -0
- {redis-5.2.1/tests/test_asyncio → redis-5.3.0b3/tests}/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/mocks.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/ssl_utils.py +0 -0
- {redis-5.2.1/tests/test_graph_utils → redis-5.3.0b3/tests/test_asyncio}/__init__.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/compat.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/mocks.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_bloom.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_commands.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_connect.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_connection.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_cwe_404.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_encoding.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_graph.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_hash.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_json.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_lock.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_monitor.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_pipeline.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_pubsub.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_retry.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_scripting.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_search.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_sentinel.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_sentinel_managed_connection.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/test_timeseries.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/testdata/jsontestdata.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/testdata/titles.csv +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_asyncio/testdata/will_play_text.csv.bz2 +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_bloom.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_cache.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_cluster.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_command_parser.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_commands.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_connect.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_connection_pool.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_encoding.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_function.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_graph.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_graph_utils/test_edge.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_graph_utils/test_node.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_graph_utils/test_path.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_hash.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_helpers.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_json.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_lock.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_monitor.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_multiprocessing.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_parsers/test_helpers.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_pipeline.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_pubsub.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_retry.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_scripting.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_search.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_sentinel.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_ssl.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_timeseries.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/test_utils.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/testdata/jsontestdata.py +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/testdata/titles.csv +0 -0
- {redis-5.2.1 → redis-5.3.0b3}/tests/testdata/will_play_text.csv.bz2 +0 -0
|
@@ -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(
|
|
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:
|
|
@@ -327,6 +336,9 @@ class AbstractConnection:
|
|
|
327
336
|
def _error_message(self, exception: BaseException) -> str:
|
|
328
337
|
return format_error_message(self._host_error(), exception)
|
|
329
338
|
|
|
339
|
+
def get_protocol(self):
|
|
340
|
+
return self.protocol
|
|
341
|
+
|
|
330
342
|
async def on_connect(self) -> None:
|
|
331
343
|
"""Initialize the connection, authenticate and select a database"""
|
|
332
344
|
self._parser.on_connect(self)
|
|
@@ -339,7 +351,8 @@ class AbstractConnection:
|
|
|
339
351
|
self.credential_provider
|
|
340
352
|
or UsernamePasswordCredentialProvider(self.username, self.password)
|
|
341
353
|
)
|
|
342
|
-
auth_args = cred_provider.
|
|
354
|
+
auth_args = await cred_provider.get_credentials_async()
|
|
355
|
+
|
|
343
356
|
# if resp version is specified and we have auth args,
|
|
344
357
|
# we need to send them via HELLO
|
|
345
358
|
if auth_args and self.protocol not in [2, "2"]:
|
|
@@ -661,6 +674,19 @@ class AbstractConnection:
|
|
|
661
674
|
while not self._socket_is_empty():
|
|
662
675
|
await self.read_response(push_request=True)
|
|
663
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
|
+
|
|
664
690
|
|
|
665
691
|
class Connection(AbstractConnection):
|
|
666
692
|
"Manages TCP communication to and from a Redis server"
|
|
@@ -1039,6 +1065,10 @@ class ConnectionPool:
|
|
|
1039
1065
|
self._available_connections: List[AbstractConnection] = []
|
|
1040
1066
|
self._in_use_connections: Set[AbstractConnection] = set()
|
|
1041
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()
|
|
1042
1072
|
|
|
1043
1073
|
def __repr__(self):
|
|
1044
1074
|
return (
|
|
@@ -1058,13 +1088,14 @@ class ConnectionPool:
|
|
|
1058
1088
|
)
|
|
1059
1089
|
|
|
1060
1090
|
async def get_connection(self, command_name, *keys, **options):
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
|
1068
1099
|
|
|
1069
1100
|
return connection
|
|
1070
1101
|
|
|
@@ -1114,6 +1145,9 @@ class ConnectionPool:
|
|
|
1114
1145
|
# not doing so is an error that will cause an exception here.
|
|
1115
1146
|
self._in_use_connections.remove(connection)
|
|
1116
1147
|
self._available_connections.append(connection)
|
|
1148
|
+
await self._event_dispatcher.dispatch_async(
|
|
1149
|
+
AsyncAfterConnectionReleasedEvent(connection)
|
|
1150
|
+
)
|
|
1117
1151
|
|
|
1118
1152
|
async def disconnect(self, inuse_connections: bool = True):
|
|
1119
1153
|
"""
|
|
@@ -1147,6 +1181,29 @@ class ConnectionPool:
|
|
|
1147
1181
|
for conn in self._in_use_connections:
|
|
1148
1182
|
conn.retry = retry
|
|
1149
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
|
+
|
|
1150
1207
|
|
|
1151
1208
|
class BlockingConnectionPool(ConnectionPool):
|
|
1152
1209
|
"""
|
|
@@ -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)
|