redis 5.3.0b5__py3-none-any.whl → 6.0.0b1__py3-none-any.whl
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/__init__.py +2 -11
- redis/_parsers/base.py +14 -2
- redis/asyncio/client.py +20 -12
- redis/asyncio/cluster.py +79 -56
- redis/asyncio/connection.py +40 -11
- redis/asyncio/lock.py +26 -5
- redis/asyncio/sentinel.py +9 -1
- redis/asyncio/utils.py +1 -1
- redis/auth/token.py +6 -2
- redis/backoff.py +15 -0
- redis/client.py +21 -14
- redis/cluster.py +111 -49
- redis/commands/cluster.py +1 -11
- redis/commands/core.py +218 -206
- redis/commands/helpers.py +0 -70
- redis/commands/redismodules.py +0 -20
- redis/commands/search/aggregation.py +3 -1
- redis/commands/search/commands.py +41 -14
- redis/commands/search/dialect.py +3 -0
- redis/commands/search/profile_information.py +14 -0
- redis/commands/search/query.py +5 -1
- redis/connection.py +37 -19
- redis/exceptions.py +4 -1
- redis/lock.py +24 -4
- redis/ocsp.py +2 -1
- redis/sentinel.py +1 -1
- redis/utils.py +107 -1
- {redis-5.3.0b5.dist-info → redis-6.0.0b1.dist-info}/METADATA +57 -23
- {redis-5.3.0b5.dist-info → redis-6.0.0b1.dist-info}/RECORD +32 -39
- {redis-5.3.0b5.dist-info → redis-6.0.0b1.dist-info}/WHEEL +1 -2
- redis/commands/graph/__init__.py +0 -263
- redis/commands/graph/commands.py +0 -313
- redis/commands/graph/edge.py +0 -91
- redis/commands/graph/exceptions.py +0 -3
- redis/commands/graph/execution_plan.py +0 -211
- redis/commands/graph/node.py +0 -88
- redis/commands/graph/path.py +0 -78
- redis/commands/graph/query_result.py +0 -588
- redis-5.3.0b5.dist-info/top_level.txt +0 -1
- /redis/commands/search/{indexDefinition.py → index_definition.py} +0 -0
- {redis-5.3.0b5.dist-info → redis-6.0.0b1.dist-info/licenses}/LICENSE +0 -0
redis/commands/redismodules.py
CHANGED
|
@@ -72,16 +72,6 @@ class RedisModuleCommands:
|
|
|
72
72
|
tdigest = TDigestBloom(client=self)
|
|
73
73
|
return tdigest
|
|
74
74
|
|
|
75
|
-
def graph(self, index_name="idx"):
|
|
76
|
-
"""Access the graph namespace, providing support for
|
|
77
|
-
redis graph data.
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
from .graph import Graph
|
|
81
|
-
|
|
82
|
-
g = Graph(client=self, name=index_name)
|
|
83
|
-
return g
|
|
84
|
-
|
|
85
75
|
|
|
86
76
|
class AsyncRedisModuleCommands(RedisModuleCommands):
|
|
87
77
|
def ft(self, index_name="idx"):
|
|
@@ -91,13 +81,3 @@ class AsyncRedisModuleCommands(RedisModuleCommands):
|
|
|
91
81
|
|
|
92
82
|
s = AsyncSearch(client=self, index_name=index_name)
|
|
93
83
|
return s
|
|
94
|
-
|
|
95
|
-
def graph(self, index_name="idx"):
|
|
96
|
-
"""Access the graph namespace, providing support for
|
|
97
|
-
redis graph data.
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
from .graph import AsyncGraph
|
|
101
|
-
|
|
102
|
-
g = AsyncGraph(client=self, name=index_name)
|
|
103
|
-
return g
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import List, Union
|
|
2
2
|
|
|
3
|
+
from redis.commands.search.dialect import DEFAULT_DIALECT
|
|
4
|
+
|
|
3
5
|
FIELDNAME = object()
|
|
4
6
|
|
|
5
7
|
|
|
@@ -110,7 +112,7 @@ class AggregateRequest:
|
|
|
110
112
|
self._with_schema = False
|
|
111
113
|
self._verbatim = False
|
|
112
114
|
self._cursor = []
|
|
113
|
-
self._dialect =
|
|
115
|
+
self._dialect = DEFAULT_DIALECT
|
|
114
116
|
self._add_scores = False
|
|
115
117
|
self._scorer = "TFIDF"
|
|
116
118
|
|
|
@@ -5,12 +5,13 @@ from typing import Dict, List, Optional, Union
|
|
|
5
5
|
from redis.client import NEVER_DECODE, Pipeline
|
|
6
6
|
from redis.utils import deprecated_function
|
|
7
7
|
|
|
8
|
-
from ..helpers import get_protocol_version
|
|
8
|
+
from ..helpers import get_protocol_version
|
|
9
9
|
from ._util import to_string
|
|
10
10
|
from .aggregation import AggregateRequest, AggregateResult, Cursor
|
|
11
11
|
from .document import Document
|
|
12
12
|
from .field import Field
|
|
13
|
-
from .
|
|
13
|
+
from .index_definition import IndexDefinition
|
|
14
|
+
from .profile_information import ProfileInformation
|
|
14
15
|
from .query import Query
|
|
15
16
|
from .result import Result
|
|
16
17
|
from .suggestion import SuggestionParser
|
|
@@ -67,7 +68,7 @@ class SearchCommands:
|
|
|
67
68
|
|
|
68
69
|
def _parse_results(self, cmd, res, **kwargs):
|
|
69
70
|
if get_protocol_version(self.client) in ["3", 3]:
|
|
70
|
-
return res
|
|
71
|
+
return ProfileInformation(res) if cmd == "FT.PROFILE" else res
|
|
71
72
|
else:
|
|
72
73
|
return self._RESP2_MODULE_CALLBACKS[cmd](res, **kwargs)
|
|
73
74
|
|
|
@@ -101,7 +102,7 @@ class SearchCommands:
|
|
|
101
102
|
with_scores=query._with_scores,
|
|
102
103
|
)
|
|
103
104
|
|
|
104
|
-
return result,
|
|
105
|
+
return result, ProfileInformation(res[1])
|
|
105
106
|
|
|
106
107
|
def _parse_spellcheck(self, res, **kwargs):
|
|
107
108
|
corrections = {}
|
|
@@ -254,8 +255,18 @@ class SearchCommands:
|
|
|
254
255
|
|
|
255
256
|
For more information see `FT.DROPINDEX <https://redis.io/commands/ft.dropindex>`_.
|
|
256
257
|
""" # noqa
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
args = [DROPINDEX_CMD, self.index_name]
|
|
259
|
+
|
|
260
|
+
delete_str = (
|
|
261
|
+
"DD"
|
|
262
|
+
if isinstance(delete_documents, bool) and delete_documents is True
|
|
263
|
+
else ""
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if delete_str:
|
|
267
|
+
args.append(delete_str)
|
|
268
|
+
|
|
269
|
+
return self.execute_command(*args)
|
|
259
270
|
|
|
260
271
|
def _add_document(
|
|
261
272
|
self,
|
|
@@ -499,7 +510,7 @@ class SearchCommands:
|
|
|
499
510
|
For more information see `FT.SEARCH <https://redis.io/commands/ft.search>`_.
|
|
500
511
|
""" # noqa
|
|
501
512
|
args, query = self._mk_query_args(query, query_params=query_params)
|
|
502
|
-
st = time.
|
|
513
|
+
st = time.monotonic()
|
|
503
514
|
|
|
504
515
|
options = {}
|
|
505
516
|
if get_protocol_version(self.client) not in ["3", 3]:
|
|
@@ -511,7 +522,7 @@ class SearchCommands:
|
|
|
511
522
|
return res
|
|
512
523
|
|
|
513
524
|
return self._parse_results(
|
|
514
|
-
SEARCH_CMD, res, query=query, duration=(time.
|
|
525
|
+
SEARCH_CMD, res, query=query, duration=(time.monotonic() - st) * 1000.0
|
|
515
526
|
)
|
|
516
527
|
|
|
517
528
|
def explain(
|
|
@@ -585,7 +596,7 @@ class SearchCommands:
|
|
|
585
596
|
|
|
586
597
|
def profile(
|
|
587
598
|
self,
|
|
588
|
-
query: Union[
|
|
599
|
+
query: Union[Query, AggregateRequest],
|
|
589
600
|
limited: bool = False,
|
|
590
601
|
query_params: Optional[Dict[str, Union[str, int, float]]] = None,
|
|
591
602
|
):
|
|
@@ -595,13 +606,13 @@ class SearchCommands:
|
|
|
595
606
|
|
|
596
607
|
### Parameters
|
|
597
608
|
|
|
598
|
-
**query**: This can be either an `AggregateRequest
|
|
609
|
+
**query**: This can be either an `AggregateRequest` or `Query`.
|
|
599
610
|
**limited**: If set to True, removes details of reader iterator.
|
|
600
611
|
**query_params**: Define one or more value parameters.
|
|
601
612
|
Each parameter has a name and a value.
|
|
602
613
|
|
|
603
614
|
"""
|
|
604
|
-
st = time.
|
|
615
|
+
st = time.monotonic()
|
|
605
616
|
cmd = [PROFILE_CMD, self.index_name, ""]
|
|
606
617
|
if limited:
|
|
607
618
|
cmd.append("LIMITED")
|
|
@@ -620,7 +631,7 @@ class SearchCommands:
|
|
|
620
631
|
res = self.execute_command(*cmd)
|
|
621
632
|
|
|
622
633
|
return self._parse_results(
|
|
623
|
-
PROFILE_CMD, res, query=query, duration=(time.
|
|
634
|
+
PROFILE_CMD, res, query=query, duration=(time.monotonic() - st) * 1000.0
|
|
624
635
|
)
|
|
625
636
|
|
|
626
637
|
def spellcheck(self, query, distance=None, include=None, exclude=None):
|
|
@@ -691,6 +702,10 @@ class SearchCommands:
|
|
|
691
702
|
cmd = [DICT_DUMP_CMD, name]
|
|
692
703
|
return self.execute_command(*cmd)
|
|
693
704
|
|
|
705
|
+
@deprecated_function(
|
|
706
|
+
version="8.0.0",
|
|
707
|
+
reason="deprecated since Redis 8.0, call config_set from core module instead",
|
|
708
|
+
)
|
|
694
709
|
def config_set(self, option: str, value: str) -> bool:
|
|
695
710
|
"""Set runtime configuration option.
|
|
696
711
|
|
|
@@ -705,6 +720,10 @@ class SearchCommands:
|
|
|
705
720
|
raw = self.execute_command(*cmd)
|
|
706
721
|
return raw == "OK"
|
|
707
722
|
|
|
723
|
+
@deprecated_function(
|
|
724
|
+
version="8.0.0",
|
|
725
|
+
reason="deprecated since Redis 8.0, call config_get from core module instead",
|
|
726
|
+
)
|
|
708
727
|
def config_get(self, option: str) -> str:
|
|
709
728
|
"""Get runtime configuration option value.
|
|
710
729
|
|
|
@@ -931,7 +950,7 @@ class AsyncSearchCommands(SearchCommands):
|
|
|
931
950
|
For more information see `FT.SEARCH <https://redis.io/commands/ft.search>`_.
|
|
932
951
|
""" # noqa
|
|
933
952
|
args, query = self._mk_query_args(query, query_params=query_params)
|
|
934
|
-
st = time.
|
|
953
|
+
st = time.monotonic()
|
|
935
954
|
|
|
936
955
|
options = {}
|
|
937
956
|
if get_protocol_version(self.client) not in ["3", 3]:
|
|
@@ -943,7 +962,7 @@ class AsyncSearchCommands(SearchCommands):
|
|
|
943
962
|
return res
|
|
944
963
|
|
|
945
964
|
return self._parse_results(
|
|
946
|
-
SEARCH_CMD, res, query=query, duration=(time.
|
|
965
|
+
SEARCH_CMD, res, query=query, duration=(time.monotonic() - st) * 1000.0
|
|
947
966
|
)
|
|
948
967
|
|
|
949
968
|
async def aggregate(
|
|
@@ -1006,6 +1025,10 @@ class AsyncSearchCommands(SearchCommands):
|
|
|
1006
1025
|
|
|
1007
1026
|
return self._parse_results(SPELLCHECK_CMD, res)
|
|
1008
1027
|
|
|
1028
|
+
@deprecated_function(
|
|
1029
|
+
version="8.0.0",
|
|
1030
|
+
reason="deprecated since Redis 8.0, call config_set from core module instead",
|
|
1031
|
+
)
|
|
1009
1032
|
async def config_set(self, option: str, value: str) -> bool:
|
|
1010
1033
|
"""Set runtime configuration option.
|
|
1011
1034
|
|
|
@@ -1020,6 +1043,10 @@ class AsyncSearchCommands(SearchCommands):
|
|
|
1020
1043
|
raw = await self.execute_command(*cmd)
|
|
1021
1044
|
return raw == "OK"
|
|
1022
1045
|
|
|
1046
|
+
@deprecated_function(
|
|
1047
|
+
version="8.0.0",
|
|
1048
|
+
reason="deprecated since Redis 8.0, call config_get from core module instead",
|
|
1049
|
+
)
|
|
1023
1050
|
async def config_get(self, option: str) -> str:
|
|
1024
1051
|
"""Get runtime configuration option value.
|
|
1025
1052
|
|
redis/commands/search/query.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import List, Optional, Union
|
|
2
2
|
|
|
3
|
+
from redis.commands.search.dialect import DEFAULT_DIALECT
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class Query:
|
|
5
7
|
"""
|
|
@@ -40,7 +42,7 @@ class Query:
|
|
|
40
42
|
self._highlight_fields: List = []
|
|
41
43
|
self._language: Optional[str] = None
|
|
42
44
|
self._expander: Optional[str] = None
|
|
43
|
-
self._dialect:
|
|
45
|
+
self._dialect: int = DEFAULT_DIALECT
|
|
44
46
|
|
|
45
47
|
def query_string(self) -> str:
|
|
46
48
|
"""Return the query string of this query only."""
|
|
@@ -177,6 +179,8 @@ class Query:
|
|
|
177
179
|
Use a different scoring function to evaluate document relevance.
|
|
178
180
|
Default is `TFIDF`.
|
|
179
181
|
|
|
182
|
+
Since Redis 8.0 default was changed to BM25STD.
|
|
183
|
+
|
|
180
184
|
:param scorer: The scoring function to use
|
|
181
185
|
(e.g. `TFIDF.DOCNORM` or `BM25`)
|
|
182
186
|
"""
|
redis/connection.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import os
|
|
3
3
|
import socket
|
|
4
|
-
import ssl
|
|
5
4
|
import sys
|
|
6
5
|
import threading
|
|
6
|
+
import time
|
|
7
7
|
import weakref
|
|
8
8
|
from abc import abstractmethod
|
|
9
9
|
from itertools import chain
|
|
10
10
|
from queue import Empty, Full, LifoQueue
|
|
11
|
-
from time import time
|
|
12
11
|
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union
|
|
13
12
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
14
13
|
|
|
@@ -42,12 +41,18 @@ from .utils import (
|
|
|
42
41
|
HIREDIS_AVAILABLE,
|
|
43
42
|
SSL_AVAILABLE,
|
|
44
43
|
compare_versions,
|
|
44
|
+
deprecated_args,
|
|
45
45
|
ensure_string,
|
|
46
46
|
format_error_message,
|
|
47
47
|
get_lib_version,
|
|
48
48
|
str_if_bytes,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
if SSL_AVAILABLE:
|
|
52
|
+
import ssl
|
|
53
|
+
else:
|
|
54
|
+
ssl = None
|
|
55
|
+
|
|
51
56
|
if HIREDIS_AVAILABLE:
|
|
52
57
|
import hiredis
|
|
53
58
|
|
|
@@ -439,7 +444,11 @@ class AbstractConnection(ConnectionInterface):
|
|
|
439
444
|
self._parser.on_connect(self)
|
|
440
445
|
if len(auth_args) == 1:
|
|
441
446
|
auth_args = ["default", auth_args[0]]
|
|
442
|
-
|
|
447
|
+
# avoid checking health here -- PING will fail if we try
|
|
448
|
+
# to check the health prior to the AUTH
|
|
449
|
+
self.send_command(
|
|
450
|
+
"HELLO", self.protocol, "AUTH", *auth_args, check_health=False
|
|
451
|
+
)
|
|
443
452
|
self.handshake_metadata = self.read_response()
|
|
444
453
|
# if response.get(b"proto") != self.protocol and response.get(
|
|
445
454
|
# "proto"
|
|
@@ -537,7 +546,7 @@ class AbstractConnection(ConnectionInterface):
|
|
|
537
546
|
|
|
538
547
|
def check_health(self):
|
|
539
548
|
"""Check the health of the connection with a PING/PONG"""
|
|
540
|
-
if self.health_check_interval and time() > self.next_health_check:
|
|
549
|
+
if self.health_check_interval and time.monotonic() > self.next_health_check:
|
|
541
550
|
self.retry.call_with_retry(self._send_ping, self._ping_failed)
|
|
542
551
|
|
|
543
552
|
def send_packed_command(self, command, check_health=True):
|
|
@@ -617,9 +626,7 @@ class AbstractConnection(ConnectionInterface):
|
|
|
617
626
|
except OSError as e:
|
|
618
627
|
if disconnect_on_error:
|
|
619
628
|
self.disconnect()
|
|
620
|
-
raise ConnectionError(
|
|
621
|
-
f"Error while reading from {host_error}" f" : {e.args}"
|
|
622
|
-
)
|
|
629
|
+
raise ConnectionError(f"Error while reading from {host_error} : {e.args}")
|
|
623
630
|
except BaseException:
|
|
624
631
|
# Also by default close in case of BaseException. A lot of code
|
|
625
632
|
# relies on this behaviour when doing Command/Response pairs.
|
|
@@ -629,7 +636,7 @@ class AbstractConnection(ConnectionInterface):
|
|
|
629
636
|
raise
|
|
630
637
|
|
|
631
638
|
if self.health_check_interval:
|
|
632
|
-
self.next_health_check = time() + self.health_check_interval
|
|
639
|
+
self.next_health_check = time.monotonic() + self.health_check_interval
|
|
633
640
|
|
|
634
641
|
if isinstance(response, ResponseError):
|
|
635
642
|
try:
|
|
@@ -672,7 +679,7 @@ class AbstractConnection(ConnectionInterface):
|
|
|
672
679
|
output.append(SYM_EMPTY.join(pieces))
|
|
673
680
|
return output
|
|
674
681
|
|
|
675
|
-
def get_protocol(self) -> int
|
|
682
|
+
def get_protocol(self) -> Union[int, str]:
|
|
676
683
|
return self.protocol
|
|
677
684
|
|
|
678
685
|
@property
|
|
@@ -1035,7 +1042,7 @@ class SSLConnection(Connection):
|
|
|
1035
1042
|
if ssl_cert_reqs is None:
|
|
1036
1043
|
ssl_cert_reqs = ssl.CERT_NONE
|
|
1037
1044
|
elif isinstance(ssl_cert_reqs, str):
|
|
1038
|
-
CERT_REQS = {
|
|
1045
|
+
CERT_REQS = { # noqa: N806
|
|
1039
1046
|
"none": ssl.CERT_NONE,
|
|
1040
1047
|
"optional": ssl.CERT_OPTIONAL,
|
|
1041
1048
|
"required": ssl.CERT_REQUIRED,
|
|
@@ -1461,8 +1468,14 @@ class ConnectionPool:
|
|
|
1461
1468
|
finally:
|
|
1462
1469
|
self._fork_lock.release()
|
|
1463
1470
|
|
|
1464
|
-
|
|
1471
|
+
@deprecated_args(
|
|
1472
|
+
args_to_warn=["*"],
|
|
1473
|
+
reason="Use get_connection() without args instead",
|
|
1474
|
+
version="5.0.3",
|
|
1475
|
+
)
|
|
1476
|
+
def get_connection(self, command_name=None, *keys, **options) -> "Connection":
|
|
1465
1477
|
"Get a connection from the pool"
|
|
1478
|
+
|
|
1466
1479
|
self._checkpid()
|
|
1467
1480
|
with self._lock:
|
|
1468
1481
|
try:
|
|
@@ -1481,7 +1494,7 @@ class ConnectionPool:
|
|
|
1481
1494
|
try:
|
|
1482
1495
|
if connection.can_read() and self.cache is None:
|
|
1483
1496
|
raise ConnectionError("Connection has data")
|
|
1484
|
-
except (ConnectionError, OSError):
|
|
1497
|
+
except (ConnectionError, TimeoutError, OSError):
|
|
1485
1498
|
connection.disconnect()
|
|
1486
1499
|
connection.connect()
|
|
1487
1500
|
if connection.can_read():
|
|
@@ -1525,7 +1538,7 @@ class ConnectionPool:
|
|
|
1525
1538
|
except KeyError:
|
|
1526
1539
|
# Gracefully fail when a connection is returned to this pool
|
|
1527
1540
|
# that the pool doesn't actually own
|
|
1528
|
-
|
|
1541
|
+
return
|
|
1529
1542
|
|
|
1530
1543
|
if self.owns_connection(connection):
|
|
1531
1544
|
self._available_connections.append(connection)
|
|
@@ -1533,10 +1546,10 @@ class ConnectionPool:
|
|
|
1533
1546
|
AfterConnectionReleasedEvent(connection)
|
|
1534
1547
|
)
|
|
1535
1548
|
else:
|
|
1536
|
-
#
|
|
1537
|
-
# to the pool
|
|
1538
|
-
#
|
|
1539
|
-
|
|
1549
|
+
# Pool doesn't own this connection, do not add it back
|
|
1550
|
+
# to the pool.
|
|
1551
|
+
# The created connections count should not be changed,
|
|
1552
|
+
# because the connection was not created by the pool.
|
|
1540
1553
|
connection.disconnect()
|
|
1541
1554
|
return
|
|
1542
1555
|
|
|
@@ -1683,7 +1696,12 @@ class BlockingConnectionPool(ConnectionPool):
|
|
|
1683
1696
|
self._connections.append(connection)
|
|
1684
1697
|
return connection
|
|
1685
1698
|
|
|
1686
|
-
|
|
1699
|
+
@deprecated_args(
|
|
1700
|
+
args_to_warn=["*"],
|
|
1701
|
+
reason="Use get_connection() without args instead",
|
|
1702
|
+
version="5.0.3",
|
|
1703
|
+
)
|
|
1704
|
+
def get_connection(self, command_name=None, *keys, **options):
|
|
1687
1705
|
"""
|
|
1688
1706
|
Get a connection, blocking for ``self.timeout`` until a connection
|
|
1689
1707
|
is available from the pool.
|
|
@@ -1723,7 +1741,7 @@ class BlockingConnectionPool(ConnectionPool):
|
|
|
1723
1741
|
try:
|
|
1724
1742
|
if connection.can_read():
|
|
1725
1743
|
raise ConnectionError("Connection has data")
|
|
1726
|
-
except (ConnectionError, OSError):
|
|
1744
|
+
except (ConnectionError, TimeoutError, OSError):
|
|
1727
1745
|
connection.disconnect()
|
|
1728
1746
|
connection.connect()
|
|
1729
1747
|
if connection.can_read():
|
redis/exceptions.py
CHANGED
|
@@ -79,6 +79,7 @@ class ModuleError(ResponseError):
|
|
|
79
79
|
|
|
80
80
|
class LockError(RedisError, ValueError):
|
|
81
81
|
"Errors acquiring or releasing a lock"
|
|
82
|
+
|
|
82
83
|
# NOTE: For backwards compatibility, this class derives from ValueError.
|
|
83
84
|
# This was originally chosen to behave like threading.Lock.
|
|
84
85
|
|
|
@@ -88,12 +89,14 @@ class LockError(RedisError, ValueError):
|
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
class LockNotOwnedError(LockError):
|
|
91
|
-
"Error trying to extend or release a lock that is (
|
|
92
|
+
"Error trying to extend or release a lock that is not owned (anymore)"
|
|
93
|
+
|
|
92
94
|
pass
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
class ChildDeadlockedError(Exception):
|
|
96
98
|
"Error indicating that a child process is deadlocked after a fork()"
|
|
99
|
+
|
|
97
100
|
pass
|
|
98
101
|
|
|
99
102
|
|
redis/lock.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import threading
|
|
2
3
|
import time as mod_time
|
|
3
4
|
import uuid
|
|
@@ -7,6 +8,8 @@ from typing import Optional, Type
|
|
|
7
8
|
from redis.exceptions import LockError, LockNotOwnedError
|
|
8
9
|
from redis.typing import Number
|
|
9
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
class Lock:
|
|
12
15
|
"""
|
|
@@ -82,6 +85,7 @@ class Lock:
|
|
|
82
85
|
blocking: bool = True,
|
|
83
86
|
blocking_timeout: Optional[Number] = None,
|
|
84
87
|
thread_local: bool = True,
|
|
88
|
+
raise_on_release_error: bool = True,
|
|
85
89
|
):
|
|
86
90
|
"""
|
|
87
91
|
Create a new Lock instance named ``name`` using the Redis client
|
|
@@ -125,6 +129,11 @@ class Lock:
|
|
|
125
129
|
thread-1 would see the token value as "xyz" and would be
|
|
126
130
|
able to successfully release the thread-2's lock.
|
|
127
131
|
|
|
132
|
+
``raise_on_release_error`` indicates whether to raise an exception when
|
|
133
|
+
the lock is no longer owned when exiting the context manager. By default,
|
|
134
|
+
this is True, meaning an exception will be raised. If False, the warning
|
|
135
|
+
will be logged and the exception will be suppressed.
|
|
136
|
+
|
|
128
137
|
In some use cases it's necessary to disable thread local storage. For
|
|
129
138
|
example, if you have code where one thread acquires a lock and passes
|
|
130
139
|
that lock instance to a worker thread to release later. If thread
|
|
@@ -140,6 +149,7 @@ class Lock:
|
|
|
140
149
|
self.blocking = blocking
|
|
141
150
|
self.blocking_timeout = blocking_timeout
|
|
142
151
|
self.thread_local = bool(thread_local)
|
|
152
|
+
self.raise_on_release_error = raise_on_release_error
|
|
143
153
|
self.local = threading.local() if self.thread_local else SimpleNamespace()
|
|
144
154
|
self.local.token = None
|
|
145
155
|
self.register_scripts()
|
|
@@ -168,7 +178,14 @@ class Lock:
|
|
|
168
178
|
exc_value: Optional[BaseException],
|
|
169
179
|
traceback: Optional[TracebackType],
|
|
170
180
|
) -> None:
|
|
171
|
-
|
|
181
|
+
try:
|
|
182
|
+
self.release()
|
|
183
|
+
except LockError:
|
|
184
|
+
if self.raise_on_release_error:
|
|
185
|
+
raise
|
|
186
|
+
logger.warning(
|
|
187
|
+
"Lock was unlocked or no longer owned when exiting context manager."
|
|
188
|
+
)
|
|
172
189
|
|
|
173
190
|
def acquire(
|
|
174
191
|
self,
|
|
@@ -251,7 +268,10 @@ class Lock:
|
|
|
251
268
|
"""
|
|
252
269
|
expected_token = self.local.token
|
|
253
270
|
if expected_token is None:
|
|
254
|
-
raise LockError(
|
|
271
|
+
raise LockError(
|
|
272
|
+
"Cannot release a lock that's not owned or is already unlocked.",
|
|
273
|
+
lock_name=self.name,
|
|
274
|
+
)
|
|
255
275
|
self.local.token = None
|
|
256
276
|
self.do_release(expected_token)
|
|
257
277
|
|
|
@@ -264,7 +284,7 @@ class Lock:
|
|
|
264
284
|
lock_name=self.name,
|
|
265
285
|
)
|
|
266
286
|
|
|
267
|
-
def extend(self, additional_time:
|
|
287
|
+
def extend(self, additional_time: Number, replace_ttl: bool = False) -> bool:
|
|
268
288
|
"""
|
|
269
289
|
Adds more time to an already acquired lock.
|
|
270
290
|
|
|
@@ -281,7 +301,7 @@ class Lock:
|
|
|
281
301
|
raise LockError("Cannot extend a lock with no timeout", lock_name=self.name)
|
|
282
302
|
return self.do_extend(additional_time, replace_ttl)
|
|
283
303
|
|
|
284
|
-
def do_extend(self, additional_time:
|
|
304
|
+
def do_extend(self, additional_time: Number, replace_ttl: bool) -> bool:
|
|
285
305
|
additional_time = int(additional_time * 1000)
|
|
286
306
|
if not bool(
|
|
287
307
|
self.lua_extend(
|
redis/ocsp.py
CHANGED
|
@@ -15,6 +15,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
|
|
15
15
|
from cryptography.hazmat.primitives.hashes import SHA1, Hash
|
|
16
16
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
|
17
17
|
from cryptography.x509 import ocsp
|
|
18
|
+
|
|
18
19
|
from redis.exceptions import AuthorizationError, ConnectionError
|
|
19
20
|
|
|
20
21
|
|
|
@@ -56,7 +57,7 @@ def _check_certificate(issuer_cert, ocsp_bytes, validate=True):
|
|
|
56
57
|
if ocsp_response.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL:
|
|
57
58
|
if ocsp_response.certificate_status != ocsp.OCSPCertStatus.GOOD:
|
|
58
59
|
raise ConnectionError(
|
|
59
|
-
f
|
|
60
|
+
f"Received an {str(ocsp_response.certificate_status).split('.')[1]} "
|
|
60
61
|
"ocsp certificate status"
|
|
61
62
|
)
|
|
62
63
|
else:
|
redis/sentinel.py
CHANGED
|
@@ -273,7 +273,7 @@ class Sentinel(SentinelCommands):
|
|
|
273
273
|
)
|
|
274
274
|
return (
|
|
275
275
|
f"<{type(self).__module__}.{type(self).__name__}"
|
|
276
|
-
f
|
|
276
|
+
f"(sentinels=[{','.join(sentinel_addresses)}])>"
|
|
277
277
|
)
|
|
278
278
|
|
|
279
279
|
def check_master_state(self, state, service_name):
|
redis/utils.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import datetime
|
|
1
2
|
import logging
|
|
2
3
|
from contextlib import contextmanager
|
|
3
4
|
from functools import wraps
|
|
4
|
-
from typing import Any, Dict, Mapping, Union
|
|
5
|
+
from typing import Any, Dict, List, Mapping, Optional, Union
|
|
6
|
+
|
|
7
|
+
from redis.exceptions import DataError
|
|
8
|
+
from redis.typing import AbsExpiryT, EncodableT, ExpiryT
|
|
5
9
|
|
|
6
10
|
try:
|
|
7
11
|
import hiredis # noqa
|
|
@@ -122,6 +126,71 @@ def deprecated_function(reason="", version="", name=None):
|
|
|
122
126
|
return decorator
|
|
123
127
|
|
|
124
128
|
|
|
129
|
+
def warn_deprecated_arg_usage(
|
|
130
|
+
arg_name: Union[list, str],
|
|
131
|
+
function_name: str,
|
|
132
|
+
reason: str = "",
|
|
133
|
+
version: str = "",
|
|
134
|
+
stacklevel: int = 2,
|
|
135
|
+
):
|
|
136
|
+
import warnings
|
|
137
|
+
|
|
138
|
+
msg = (
|
|
139
|
+
f"Call to '{function_name}' function with deprecated"
|
|
140
|
+
f" usage of input argument/s '{arg_name}'."
|
|
141
|
+
)
|
|
142
|
+
if reason:
|
|
143
|
+
msg += f" ({reason})"
|
|
144
|
+
if version:
|
|
145
|
+
msg += f" -- Deprecated since version {version}."
|
|
146
|
+
warnings.warn(msg, category=DeprecationWarning, stacklevel=stacklevel)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def deprecated_args(
|
|
150
|
+
args_to_warn: list = ["*"],
|
|
151
|
+
allowed_args: list = [],
|
|
152
|
+
reason: str = "",
|
|
153
|
+
version: str = "",
|
|
154
|
+
):
|
|
155
|
+
"""
|
|
156
|
+
Decorator to mark specified args of a function as deprecated.
|
|
157
|
+
If '*' is in args_to_warn, all arguments will be marked as deprecated.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def decorator(func):
|
|
161
|
+
@wraps(func)
|
|
162
|
+
def wrapper(*args, **kwargs):
|
|
163
|
+
# Get function argument names
|
|
164
|
+
arg_names = func.__code__.co_varnames[: func.__code__.co_argcount]
|
|
165
|
+
|
|
166
|
+
provided_args = dict(zip(arg_names, args))
|
|
167
|
+
provided_args.update(kwargs)
|
|
168
|
+
|
|
169
|
+
provided_args.pop("self", None)
|
|
170
|
+
for allowed_arg in allowed_args:
|
|
171
|
+
provided_args.pop(allowed_arg, None)
|
|
172
|
+
|
|
173
|
+
for arg in args_to_warn:
|
|
174
|
+
if arg == "*" and len(provided_args) > 0:
|
|
175
|
+
warn_deprecated_arg_usage(
|
|
176
|
+
list(provided_args.keys()),
|
|
177
|
+
func.__name__,
|
|
178
|
+
reason,
|
|
179
|
+
version,
|
|
180
|
+
stacklevel=3,
|
|
181
|
+
)
|
|
182
|
+
elif arg in provided_args:
|
|
183
|
+
warn_deprecated_arg_usage(
|
|
184
|
+
arg, func.__name__, reason, version, stacklevel=3
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return func(*args, **kwargs)
|
|
188
|
+
|
|
189
|
+
return wrapper
|
|
190
|
+
|
|
191
|
+
return decorator
|
|
192
|
+
|
|
193
|
+
|
|
125
194
|
def _set_info_logger():
|
|
126
195
|
"""
|
|
127
196
|
Set up a logger that log info logs to stdout.
|
|
@@ -192,3 +261,40 @@ def ensure_string(key):
|
|
|
192
261
|
return key
|
|
193
262
|
else:
|
|
194
263
|
raise TypeError("Key must be either a string or bytes")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def extract_expire_flags(
|
|
267
|
+
ex: Optional[ExpiryT] = None,
|
|
268
|
+
px: Optional[ExpiryT] = None,
|
|
269
|
+
exat: Optional[AbsExpiryT] = None,
|
|
270
|
+
pxat: Optional[AbsExpiryT] = None,
|
|
271
|
+
) -> List[EncodableT]:
|
|
272
|
+
exp_options: list[EncodableT] = []
|
|
273
|
+
if ex is not None:
|
|
274
|
+
exp_options.append("EX")
|
|
275
|
+
if isinstance(ex, datetime.timedelta):
|
|
276
|
+
exp_options.append(int(ex.total_seconds()))
|
|
277
|
+
elif isinstance(ex, int):
|
|
278
|
+
exp_options.append(ex)
|
|
279
|
+
elif isinstance(ex, str) and ex.isdigit():
|
|
280
|
+
exp_options.append(int(ex))
|
|
281
|
+
else:
|
|
282
|
+
raise DataError("ex must be datetime.timedelta or int")
|
|
283
|
+
elif px is not None:
|
|
284
|
+
exp_options.append("PX")
|
|
285
|
+
if isinstance(px, datetime.timedelta):
|
|
286
|
+
exp_options.append(int(px.total_seconds() * 1000))
|
|
287
|
+
elif isinstance(px, int):
|
|
288
|
+
exp_options.append(px)
|
|
289
|
+
else:
|
|
290
|
+
raise DataError("px must be datetime.timedelta or int")
|
|
291
|
+
elif exat is not None:
|
|
292
|
+
if isinstance(exat, datetime.datetime):
|
|
293
|
+
exat = int(exat.timestamp())
|
|
294
|
+
exp_options.extend(["EXAT", exat])
|
|
295
|
+
elif pxat is not None:
|
|
296
|
+
if isinstance(pxat, datetime.datetime):
|
|
297
|
+
pxat = int(pxat.timestamp() * 1000)
|
|
298
|
+
exp_options.extend(["PXAT", pxat])
|
|
299
|
+
|
|
300
|
+
return exp_options
|