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.
Files changed (41) hide show
  1. redis/__init__.py +2 -11
  2. redis/_parsers/base.py +14 -2
  3. redis/asyncio/client.py +20 -12
  4. redis/asyncio/cluster.py +79 -56
  5. redis/asyncio/connection.py +40 -11
  6. redis/asyncio/lock.py +26 -5
  7. redis/asyncio/sentinel.py +9 -1
  8. redis/asyncio/utils.py +1 -1
  9. redis/auth/token.py +6 -2
  10. redis/backoff.py +15 -0
  11. redis/client.py +21 -14
  12. redis/cluster.py +111 -49
  13. redis/commands/cluster.py +1 -11
  14. redis/commands/core.py +218 -206
  15. redis/commands/helpers.py +0 -70
  16. redis/commands/redismodules.py +0 -20
  17. redis/commands/search/aggregation.py +3 -1
  18. redis/commands/search/commands.py +41 -14
  19. redis/commands/search/dialect.py +3 -0
  20. redis/commands/search/profile_information.py +14 -0
  21. redis/commands/search/query.py +5 -1
  22. redis/connection.py +37 -19
  23. redis/exceptions.py +4 -1
  24. redis/lock.py +24 -4
  25. redis/ocsp.py +2 -1
  26. redis/sentinel.py +1 -1
  27. redis/utils.py +107 -1
  28. {redis-5.3.0b5.dist-info → redis-6.0.0b1.dist-info}/METADATA +57 -23
  29. {redis-5.3.0b5.dist-info → redis-6.0.0b1.dist-info}/RECORD +32 -39
  30. {redis-5.3.0b5.dist-info → redis-6.0.0b1.dist-info}/WHEEL +1 -2
  31. redis/commands/graph/__init__.py +0 -263
  32. redis/commands/graph/commands.py +0 -313
  33. redis/commands/graph/edge.py +0 -91
  34. redis/commands/graph/exceptions.py +0 -3
  35. redis/commands/graph/execution_plan.py +0 -211
  36. redis/commands/graph/node.py +0 -88
  37. redis/commands/graph/path.py +0 -78
  38. redis/commands/graph/query_result.py +0 -588
  39. redis-5.3.0b5.dist-info/top_level.txt +0 -1
  40. /redis/commands/search/{indexDefinition.py → index_definition.py} +0 -0
  41. {redis-5.3.0b5.dist-info → redis-6.0.0b1.dist-info/licenses}/LICENSE +0 -0
@@ -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 = None
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, parse_to_dict
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 .indexDefinition import IndexDefinition
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, parse_to_dict(res[1])
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
- delete_str = "DD" if delete_documents else ""
258
- return self.execute_command(DROPINDEX_CMD, self.index_name, delete_str)
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.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.time() - st) * 1000.0
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[str, Query, AggregateRequest],
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`, `Query` or string.
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.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.time() - st) * 1000.0
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.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.time() - st) * 1000.0
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
 
@@ -0,0 +1,3 @@
1
+ # Value for the default dialect to be used as a part of
2
+ # Search or Aggregate query.
3
+ DEFAULT_DIALECT = 2
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+
3
+
4
+ class ProfileInformation:
5
+ """
6
+ Wrapper around FT.PROFILE response
7
+ """
8
+
9
+ def __init__(self, info: Any) -> None:
10
+ self._info: Any = info
11
+
12
+ @property
13
+ def info(self) -> Any:
14
+ return self._info
@@ -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: Optional[int] = None
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
- self.send_command("HELLO", self.protocol, "AUTH", *auth_args)
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 or str:
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
- def get_connection(self, command_name: str, *keys, **options) -> "Connection":
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
- pass
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
- # pool doesn't own this connection. do not add it back
1537
- # to the pool and decrement the count so that another
1538
- # connection can take its place if needed
1539
- self._created_connections -= 1
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
- def get_connection(self, command_name, *keys, **options):
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 (no longer) owned"
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
- self.release()
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("Cannot release an unlocked lock", lock_name=self.name)
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: int, replace_ttl: bool = False) -> bool:
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: int, replace_ttl: bool) -> bool:
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'Received an {str(ocsp_response.certificate_status).split(".")[1]} '
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'(sentinels=[{",".join(sentinel_addresses)}])>'
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