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.
- {redis-5.2.0/redis.egg-info → redis-5.3.0b1}/PKG-INFO +1 -1
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/helpers.py +11 -4
- {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/client.py +42 -1
- {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/cluster.py +54 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/connection.py +72 -9
- {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/sentinel.py +1 -5
- redis-5.3.0b1/redis/auth/err.py +31 -0
- redis-5.3.0b1/redis/auth/idp.py +28 -0
- redis-5.3.0b1/redis/auth/token.py +126 -0
- redis-5.3.0b1/redis/auth/token_manager.py +370 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/client.py +51 -3
- {redis-5.2.0 → redis-5.3.0b1}/redis/cluster.py +39 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/connection.py +73 -0
- redis-5.3.0b1/redis/credentials.py +65 -0
- redis-5.3.0b1/redis/event.py +394 -0
- {redis-5.2.0 → redis-5.3.0b1/redis.egg-info}/PKG-INFO +1 -1
- {redis-5.2.0 → redis-5.3.0b1}/redis.egg-info/SOURCES.txt +9 -0
- {redis-5.2.0 → redis-5.3.0b1}/setup.py +2 -1
- {redis-5.2.0 → redis-5.3.0b1}/tests/conftest.py +191 -24
- redis-5.3.0b1/tests/ssl_utils.py +43 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/conftest.py +139 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_cluster.py +22 -29
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_connect.py +16 -13
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_connection_pool.py +7 -0
- redis-5.3.0b1/tests/test_asyncio/test_credentials.py +696 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_cwe_404.py +2 -2
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_graph.py +20 -20
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_hash.py +7 -6
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_search.py +2 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_sentinel.py +20 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_timeseries.py +5 -0
- redis-5.3.0b1/tests/test_auth/__init__.py +0 -0
- redis-5.3.0b1/tests/test_auth/test_token.py +76 -0
- redis-5.3.0b1/tests/test_auth/test_token_manager.py +566 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_cluster.py +7 -6
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_connect.py +14 -13
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_connection.py +4 -0
- redis-5.3.0b1/tests/test_credentials.py +659 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_graph.py +23 -23
- redis-5.3.0b1/tests/test_graph_utils/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_graph_utils/test_edge.py +4 -4
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_graph_utils/test_node.py +3 -3
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_graph_utils/test_path.py +5 -5
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_hash.py +7 -6
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_json.py +2 -2
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_search.py +13 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_ssl.py +29 -26
- redis-5.2.0/redis/credentials.py +0 -26
- redis-5.2.0/tests/ssl_utils.py +0 -14
- redis-5.2.0/tests/test_asyncio/test_credentials.py +0 -283
- redis-5.2.0/tests/test_credentials.py +0 -250
- {redis-5.2.0 → redis-5.3.0b1}/INSTALL +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/LICENSE +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/MANIFEST.in +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/README.md +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/base.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/commands.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/encoders.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/hiredis.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/resp2.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/resp3.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/_parsers/socket.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/lock.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/retry.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/asyncio/utils.py +0 -0
- {redis-5.2.0/tests → redis-5.3.0b1/redis/auth}/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/backoff.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/cache.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/bf/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/bf/commands.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/bf/info.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/cluster.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/core.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/commands.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/edge.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/exceptions.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/execution_plan.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/node.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/path.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/graph/query_result.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/helpers.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/_util.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/commands.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/decoders.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/json/path.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/redismodules.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/_util.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/aggregation.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/commands.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/document.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/field.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/indexDefinition.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/query.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/querystring.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/reducers.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/result.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/search/suggestion.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/sentinel.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/timeseries/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/timeseries/commands.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/timeseries/info.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/commands/timeseries/utils.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/crc.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/exceptions.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/lock.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/ocsp.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/retry.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/sentinel.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/typing.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis/utils.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis.egg-info/dependency_links.txt +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis.egg-info/requires.txt +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/redis.egg-info/top_level.txt +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/setup.cfg +0 -0
- {redis-5.2.0/tests/test_asyncio → redis-5.3.0b1/tests}/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/mocks.py +0 -0
- {redis-5.2.0/tests/test_graph_utils → redis-5.3.0b1/tests/test_asyncio}/__init__.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/compat.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/mocks.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_bloom.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_commands.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_connection.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_encoding.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_json.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_lock.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_monitor.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_pipeline.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_pubsub.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_retry.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_scripting.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/test_sentinel_managed_connection.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/testdata/jsontestdata.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/testdata/titles.csv +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_asyncio/testdata/will_play_text.csv.bz2 +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_bloom.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_cache.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_command_parser.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_commands.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_connection_pool.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_encoding.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_function.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_helpers.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_lock.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_monitor.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_multiprocessing.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_parsers/test_helpers.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_pipeline.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_pubsub.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_retry.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_scripting.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_sentinel.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_timeseries.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/test_utils.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/testdata/jsontestdata.py +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/testdata/titles.csv +0 -0
- {redis-5.2.0 → redis-5.3.0b1}/tests/testdata/will_play_text.csv.bz2 +0 -0
|
@@ -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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
-
|
|
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)
|