coredis 4.23.1__py3-none-any.whl → 5.0.0__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.

Potentially problematic release.


This version of coredis might be problematic. Click here for more details.

Files changed (78) hide show
  1. coredis/__init__.py +1 -3
  2. coredis/_packer.py +10 -10
  3. coredis/_protocols.py +19 -51
  4. coredis/_py_311_typing.py +20 -0
  5. coredis/_py_312_typing.py +17 -0
  6. coredis/_utils.py +49 -55
  7. coredis/_version.py +3 -3
  8. coredis/cache.py +57 -82
  9. coredis/client/__init__.py +1 -2
  10. coredis/client/basic.py +129 -56
  11. coredis/client/cluster.py +147 -70
  12. coredis/commands/__init__.py +27 -7
  13. coredis/commands/_key_spec.py +11 -10
  14. coredis/commands/_utils.py +1 -1
  15. coredis/commands/_validators.py +30 -20
  16. coredis/commands/_wrappers.py +19 -99
  17. coredis/commands/bitfield.py +10 -2
  18. coredis/commands/constants.py +20 -3
  19. coredis/commands/core.py +1674 -1251
  20. coredis/commands/function.py +29 -22
  21. coredis/commands/monitor.py +0 -71
  22. coredis/commands/pubsub.py +7 -142
  23. coredis/commands/request.py +108 -0
  24. coredis/commands/script.py +21 -22
  25. coredis/commands/sentinel.py +60 -49
  26. coredis/connection.py +14 -15
  27. coredis/exceptions.py +2 -2
  28. coredis/experimental/__init__.py +0 -4
  29. coredis/globals.py +3 -0
  30. coredis/modules/autocomplete.py +28 -30
  31. coredis/modules/base.py +15 -31
  32. coredis/modules/filters.py +269 -245
  33. coredis/modules/graph.py +61 -62
  34. coredis/modules/json.py +172 -140
  35. coredis/modules/response/_callbacks/autocomplete.py +5 -4
  36. coredis/modules/response/_callbacks/graph.py +34 -29
  37. coredis/modules/response/_callbacks/json.py +5 -3
  38. coredis/modules/response/_callbacks/search.py +49 -53
  39. coredis/modules/response/_callbacks/timeseries.py +18 -30
  40. coredis/modules/response/types.py +1 -5
  41. coredis/modules/search.py +186 -169
  42. coredis/modules/timeseries.py +184 -164
  43. coredis/parser.py +6 -19
  44. coredis/pipeline.py +477 -521
  45. coredis/pool/basic.py +7 -7
  46. coredis/pool/cluster.py +3 -3
  47. coredis/pool/nodemanager.py +10 -3
  48. coredis/response/_callbacks/__init__.py +76 -57
  49. coredis/response/_callbacks/acl.py +0 -3
  50. coredis/response/_callbacks/cluster.py +25 -16
  51. coredis/response/_callbacks/command.py +8 -6
  52. coredis/response/_callbacks/connection.py +4 -3
  53. coredis/response/_callbacks/geo.py +17 -13
  54. coredis/response/_callbacks/hash.py +13 -11
  55. coredis/response/_callbacks/keys.py +9 -5
  56. coredis/response/_callbacks/module.py +2 -3
  57. coredis/response/_callbacks/script.py +6 -8
  58. coredis/response/_callbacks/sentinel.py +21 -17
  59. coredis/response/_callbacks/server.py +36 -14
  60. coredis/response/_callbacks/sets.py +3 -4
  61. coredis/response/_callbacks/sorted_set.py +27 -24
  62. coredis/response/_callbacks/streams.py +22 -13
  63. coredis/response/_callbacks/strings.py +7 -6
  64. coredis/response/_callbacks/vector_sets.py +159 -0
  65. coredis/response/types.py +13 -4
  66. coredis/retry.py +12 -13
  67. coredis/sentinel.py +11 -1
  68. coredis/stream.py +4 -3
  69. coredis/tokens.py +348 -16
  70. coredis/typing.py +432 -81
  71. {coredis-4.23.1.dist-info → coredis-5.0.0.dist-info}/METADATA +4 -9
  72. coredis-5.0.0.dist-info/RECORD +95 -0
  73. coredis/client/keydb.py +0 -336
  74. coredis/pipeline.pyi +0 -2103
  75. coredis-4.23.1.dist-info/RECORD +0 -93
  76. {coredis-4.23.1.dist-info → coredis-5.0.0.dist-info}/WHEEL +0 -0
  77. {coredis-4.23.1.dist-info → coredis-5.0.0.dist-info}/licenses/LICENSE +0 -0
  78. {coredis-4.23.1.dist-info → coredis-5.0.0.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,5 @@ from __future__ import annotations
2
2
 
3
3
  from .basic import Client, Redis
4
4
  from .cluster import RedisCluster
5
- from .keydb import KeyDB, KeyDBCluster
6
5
 
7
- __all__ = ["Client", "Redis", "RedisCluster", "KeyDB", "KeyDBCluster"]
6
+ __all__ = ["Client", "Redis", "RedisCluster"]
coredis/client/basic.py CHANGED
@@ -4,6 +4,7 @@ import asyncio
4
4
  import contextlib
5
5
  import contextvars
6
6
  import functools
7
+ import random
7
8
  import warnings
8
9
  from collections import defaultdict
9
10
  from ssl import SSLContext
@@ -14,7 +15,8 @@ from packaging import version
14
15
  from packaging.version import InvalidVersion, Version
15
16
 
16
17
  from coredis._utils import EncodingInsensitiveDict, nativestr
17
- from coredis.cache import AbstractCache, SupportsClientTracking
18
+ from coredis.cache import AbstractCache
19
+ from coredis.commands import CommandRequest
18
20
  from coredis.commands._key_spec import KeySpec
19
21
  from coredis.commands.constants import CommandFlag, CommandName
20
22
  from coredis.commands.core import CoreCommands
@@ -31,6 +33,7 @@ from coredis.connection import (
31
33
  )
32
34
  from coredis.credentials import AbstractCredentialProvider
33
35
  from coredis.exceptions import (
36
+ AuthenticationError,
34
37
  ConnectionError,
35
38
  PersistenceError,
36
39
  RedisError,
@@ -39,7 +42,7 @@ from coredis.exceptions import (
39
42
  UnknownCommandError,
40
43
  WatchError,
41
44
  )
42
- from coredis.globals import COMMAND_FLAGS, READONLY_COMMANDS
45
+ from coredis.globals import CACHEABLE_COMMANDS, COMMAND_FLAGS, READONLY_COMMANDS
43
46
  from coredis.modules import ModuleMixin
44
47
  from coredis.pool import ConnectionPool
45
48
  from coredis.response._callbacks import (
@@ -55,6 +58,7 @@ from coredis.typing import (
55
58
  AsyncIterator,
56
59
  Callable,
57
60
  Coroutine,
61
+ ExecutionParameters,
58
62
  Generator,
59
63
  Generic,
60
64
  Iterator,
@@ -63,9 +67,14 @@ from coredis.typing import (
63
67
  Mapping,
64
68
  Parameters,
65
69
  ParamSpec,
70
+ RedisCommandP,
71
+ RedisValueT,
66
72
  ResponseType,
67
73
  StringT,
74
+ T_co,
75
+ TypeAdapter,
68
76
  TypeVar,
77
+ Unpack,
69
78
  ValueT,
70
79
  )
71
80
 
@@ -75,7 +84,6 @@ R = TypeVar("R")
75
84
  if TYPE_CHECKING:
76
85
  import coredis.pipeline
77
86
 
78
-
79
87
  ClientT = TypeVar("ClientT", bound="Client[Any]")
80
88
  RedisT = TypeVar("RedisT", bound="Redis[Any]")
81
89
 
@@ -93,6 +101,7 @@ class Client(
93
101
  protocol_version: Literal[2, 3]
94
102
  server_version: Version | None
95
103
  callback_storage: dict[type[ResponseCallback[Any, Any, Any]], dict[str, Any]]
104
+ type_adapter: TypeAdapter
96
105
 
97
106
  def __init__(
98
107
  self,
@@ -126,6 +135,7 @@ class Client(
126
135
  retry_policy: RetryPolicy = NoRetryPolicy(),
127
136
  noevict: bool = False,
128
137
  notouch: bool = False,
138
+ type_adapter: TypeAdapter | None = None,
129
139
  **kwargs: Any,
130
140
  ):
131
141
  if not connection_pool:
@@ -198,6 +208,30 @@ class Client(
198
208
  self.retry_policy = retry_policy
199
209
  self._module_info: dict[str, version.Version] | None = None
200
210
  self.callback_storage = defaultdict(dict)
211
+ self.type_adapter = type_adapter or TypeAdapter()
212
+
213
+ def create_request(
214
+ self,
215
+ name: bytes,
216
+ *arguments: ValueT,
217
+ callback: Callable[..., T_co],
218
+ execution_parameters: ExecutionParameters | None = None,
219
+ ) -> CommandRequest[T_co]:
220
+ """
221
+ Factory method to create a command request awaitable.
222
+ Subclasses of :class:`coredis.client.Client` can override this method
223
+ if custom behavior is required. See :class:`~coredis.commands.CommandRequest`
224
+ for details.
225
+
226
+ :param name: The name of the command
227
+ :param arguments: all arguments sent to the command
228
+ :param callback: a callback that takes the RESP response and converts it
229
+ into a shape to be returned
230
+ :return: An instance of a command request bound to this client.
231
+ """
232
+ return CommandRequest(
233
+ self, name, *arguments, callback=callback, execution_parameters=execution_parameters
234
+ )
201
235
 
202
236
  @property
203
237
  def noreply(self) -> bool:
@@ -220,9 +254,7 @@ class Client(
220
254
  return False
221
255
  return True
222
256
 
223
- async def get_server_module_version(self, module: str) -> version.Version | None:
224
- if self._module_info is None:
225
- await self._populate_module_versions()
257
+ def get_server_module_version(self, module: str) -> version.Version | None:
226
258
  return (self._module_info or {}).get(module)
227
259
 
228
260
  def _ensure_server_version(self, version: str | None) -> None:
@@ -246,7 +278,7 @@ class Client(
246
278
  self.server_version = None
247
279
 
248
280
  async def _ensure_wait(
249
- self, command: bytes, connection: BaseConnection
281
+ self, command: RedisCommandP, connection: BaseConnection
250
282
  ) -> asyncio.Future[None]:
251
283
  maybe_wait: asyncio.Future[None] = asyncio.get_running_loop().create_future()
252
284
  wait = self._waitcontext.get()
@@ -257,7 +289,7 @@ class Client(
257
289
  if exc:
258
290
  maybe_wait.set_exception(exc)
259
291
  elif not cast(int, response.result()) >= wait[0]:
260
- maybe_wait.set_exception(ReplicationError(command, wait[0], wait[1]))
292
+ maybe_wait.set_exception(ReplicationError(command.name, wait[0], wait[1]))
261
293
  else:
262
294
  maybe_wait.set_result(None)
263
295
 
@@ -268,7 +300,7 @@ class Client(
268
300
  return maybe_wait
269
301
 
270
302
  async def _ensure_persistence(
271
- self, command: bytes, connection: BaseConnection
303
+ self, command: RedisCommandP, connection: BaseConnection
272
304
  ) -> asyncio.Future[None]:
273
305
  maybe_wait: asyncio.Future[None] = asyncio.get_running_loop().create_future()
274
306
  waitaof = self._waitaof_context.get()
@@ -283,7 +315,7 @@ class Client(
283
315
  else:
284
316
  res = cast(tuple[int, int], response.result())
285
317
  if not (res[0] >= waitaof[0] and res[1] >= waitaof[1]):
286
- maybe_wait.set_exception(PersistenceError(command, *waitaof))
318
+ maybe_wait.set_exception(PersistenceError(command.name, *waitaof))
287
319
  else:
288
320
  maybe_wait.set_result(None)
289
321
 
@@ -294,7 +326,7 @@ class Client(
294
326
  return maybe_wait
295
327
 
296
328
  async def _populate_module_versions(self) -> None:
297
- if self.noreply:
329
+ if self.noreply or getattr(self, "_module_info", None) is not None:
298
330
  return
299
331
  try:
300
332
  modules = await self.module_list()
@@ -307,11 +339,12 @@ class Client(
307
339
  ver, minor = divmod(ver, 100)
308
340
  ver, major = divmod(ver, 100)
309
341
  self._module_info[name] = version.Version(f"{major}.{minor}.{patch}")
310
- except UnknownCommandError:
342
+ except (UnknownCommandError, AuthenticationError):
311
343
  self._module_info = {}
312
344
 
313
345
  async def initialize(self: ClientT) -> ClientT:
314
346
  await self.connection_pool.initialize()
347
+ await self._populate_module_versions()
315
348
  return self
316
349
 
317
350
  def __await__(self: ClientT) -> Generator[Any, None, ClientT]:
@@ -422,7 +455,7 @@ class Client(
422
455
  for item in data:
423
456
  yield item
424
457
 
425
- def register_script(self, script: ValueT) -> Script[AnyStr]:
458
+ def register_script(self, script: RedisValueT) -> Script[AnyStr]:
426
459
  """
427
460
  Registers a Lua :paramref:`script`
428
461
 
@@ -538,8 +571,8 @@ class Client(
538
571
  finally:
539
572
  self._waitaof_context.set(None)
540
573
 
541
- def should_quick_release(self, command: bytes) -> bool:
542
- return CommandFlag.BLOCKING not in COMMAND_FLAGS[command]
574
+ def should_quick_release(self, command: RedisCommandP) -> bool:
575
+ return CommandFlag.BLOCKING not in COMMAND_FLAGS[command.name]
543
576
 
544
577
 
545
578
  class Redis(Client[AnyStr]):
@@ -580,6 +613,7 @@ class Redis(Client[AnyStr]):
580
613
  noevict: bool = ...,
581
614
  notouch: bool = ...,
582
615
  retry_policy: RetryPolicy = ...,
616
+ type_adapter: TypeAdapter | None = ...,
583
617
  **kwargs: Any,
584
618
  ) -> None: ...
585
619
 
@@ -618,6 +652,7 @@ class Redis(Client[AnyStr]):
618
652
  noevict: bool = ...,
619
653
  notouch: bool = ...,
620
654
  retry_policy: RetryPolicy = ...,
655
+ type_adapter: TypeAdapter | None = ...,
621
656
  **kwargs: Any,
622
657
  ) -> None: ...
623
658
 
@@ -655,6 +690,7 @@ class Redis(Client[AnyStr]):
655
690
  noevict: bool = False,
656
691
  notouch: bool = False,
657
692
  retry_policy: RetryPolicy = ConstantRetryPolicy((ConnectionError, TimeoutError), 2, 0.01),
693
+ type_adapter: TypeAdapter | None = None,
658
694
  **kwargs: Any,
659
695
  ) -> None:
660
696
  """
@@ -775,6 +811,8 @@ class Redis(Client[AnyStr]):
775
811
  :param notouch: Ensures that commands sent by the client will not alter the LRU/LFU of
776
812
  the keys they access.
777
813
  :param retry_policy: The retry policy to use when interacting with the redis server
814
+ :param type_adapter: The adapter to use for serializing / deserializing customs types
815
+ when interacting with redis commands.
778
816
 
779
817
  """
780
818
  super().__init__(
@@ -796,7 +834,7 @@ class Redis(Client[AnyStr]):
796
834
  ssl_keyfile=ssl_keyfile,
797
835
  ssl_certfile=ssl_certfile,
798
836
  ssl_cert_reqs=ssl_cert_reqs,
799
- ssh_check_hostname=ssl_check_hostname,
837
+ ssl_check_hostname=ssl_check_hostname,
800
838
  ssl_ca_certs=ssl_ca_certs,
801
839
  max_connections=max_connections,
802
840
  max_idle_time=max_idle_time,
@@ -808,6 +846,7 @@ class Redis(Client[AnyStr]):
808
846
  noevict=noevict,
809
847
  notouch=notouch,
810
848
  retry_policy=retry_policy,
849
+ type_adapter=type_adapter,
811
850
  **kwargs,
812
851
  )
813
852
  self.cache = cache
@@ -867,6 +906,7 @@ class Redis(Client[AnyStr]):
867
906
  noevict: bool = False,
868
907
  notouch: bool = False,
869
908
  retry_policy: RetryPolicy = ConstantRetryPolicy((ConnectionError, TimeoutError), 2, 0.01),
909
+ type_adapter: TypeAdapter | None = None,
870
910
  cache: AbstractCache | None = None,
871
911
  **kwargs: Any,
872
912
  ) -> RedisT:
@@ -892,6 +932,7 @@ class Redis(Client[AnyStr]):
892
932
  verify_version=verify_version,
893
933
  noreply=noreply,
894
934
  retry_policy=retry_policy,
935
+ type_adapter=type_adapter,
895
936
  cache=cache,
896
937
  connection_pool=ConnectionPool.from_url(
897
938
  url,
@@ -911,6 +952,7 @@ class Redis(Client[AnyStr]):
911
952
  verify_version=verify_version,
912
953
  noreply=noreply,
913
954
  retry_policy=retry_policy,
955
+ type_adapter=type_adapter,
914
956
  cache=cache,
915
957
  connection_pool=ConnectionPool.from_url(
916
958
  url,
@@ -933,66 +975,97 @@ class Redis(Client[AnyStr]):
933
975
 
934
976
  async def execute_command(
935
977
  self,
936
- command: bytes,
937
- *args: ValueT,
978
+ command: RedisCommandP,
938
979
  callback: Callable[..., R] = NoopCallback(),
939
- **options: ValueT | None,
980
+ **options: Unpack[ExecutionParameters],
940
981
  ) -> R:
941
982
  """
942
983
  Executes a command with configured retries and returns
943
984
  the parsed response
944
985
  """
945
986
  return await self.retry_policy.call_with_retries(
946
- lambda: self._execute_command(command, *args, callback=callback, **options),
987
+ lambda: self._execute_command(command, callback=callback, **options),
947
988
  before_hook=self.initialize,
948
989
  )
949
990
 
950
991
  async def _execute_command(
951
992
  self,
952
- command: bytes,
953
- *args: ValueT,
993
+ command: RedisCommandP,
954
994
  callback: Callable[..., R] = NoopCallback(),
955
- **options: ValueT | None,
995
+ **options: Unpack[ExecutionParameters],
956
996
  ) -> R:
957
997
  pool = self.connection_pool
958
998
  quick_release = self.should_quick_release(command)
959
999
  connection = await pool.get_connection(
960
- command,
961
- *args,
1000
+ command.name,
1001
+ *command.arguments,
962
1002
  acquire=not quick_release or self.requires_wait or self.requires_waitaof,
963
1003
  )
964
- if (
965
- self.cache
966
- and isinstance(self.cache, SupportsClientTracking)
967
- and connection.tracking_client_id != self.cache.get_client_id(connection)
968
- ):
969
- self.cache.reset()
970
- await connection.update_tracking_client(True, self.cache.get_client_id(connection))
971
1004
  try:
972
- if self.cache and command not in READONLY_COMMANDS:
973
- self.cache.invalidate(*KeySpec.extract_keys(command, *args))
974
- request = await connection.create_request(
975
- command,
976
- *args,
977
- noreply=self.noreply,
978
- decode=options.get("decode", self._decodecontext.get()),
979
- encoding=self._encodingcontext.get(),
980
- )
981
- maybe_wait = [
982
- await self._ensure_wait(command, connection),
983
- await self._ensure_persistence(command, connection),
984
- ]
985
- reply = await request
986
- await asyncio.gather(*maybe_wait)
987
- if self.noreply:
988
- return None # type: ignore
989
- if isinstance(callback, AsyncPreProcessingCallback):
990
- await callback.pre_process(self, reply, version=self.protocol_version, **options)
991
- return callback(
992
- reply,
993
- version=self.protocol_version,
994
- **options,
1005
+ keys = KeySpec.extract_keys(command.name, *command.arguments)
1006
+ cacheable = (
1007
+ command.name in CACHEABLE_COMMANDS
1008
+ and len(keys) == 1
1009
+ and not self.noreply
1010
+ and self._decodecontext.get() is None
995
1011
  )
1012
+ cached_reply = None
1013
+ cache_hit = False
1014
+ use_cached = False
1015
+ reply = None
1016
+ if self.cache:
1017
+ if connection.tracking_client_id != self.cache.get_client_id(connection):
1018
+ self.cache.reset()
1019
+ await connection.update_tracking_client(
1020
+ True, self.cache.get_client_id(connection)
1021
+ )
1022
+ if command.name not in READONLY_COMMANDS:
1023
+ self.cache.invalidate(*keys)
1024
+ elif cacheable:
1025
+ try:
1026
+ cached_reply = cast(
1027
+ R,
1028
+ self.cache.get(
1029
+ command.name,
1030
+ keys[0],
1031
+ *command.arguments,
1032
+ ),
1033
+ )
1034
+ use_cached = random.random() * 100.0 < min(100.0, self.cache.confidence)
1035
+ cache_hit = True
1036
+ except KeyError:
1037
+ pass
1038
+ if not (use_cached and cached_reply):
1039
+ request = await connection.create_request(
1040
+ command.name,
1041
+ *command.arguments,
1042
+ noreply=self.noreply,
1043
+ decode=options.get("decode", self._decodecontext.get()),
1044
+ encoding=self._encodingcontext.get(),
1045
+ )
1046
+ maybe_wait = [
1047
+ await self._ensure_wait(command, connection),
1048
+ await self._ensure_persistence(command, connection),
1049
+ ]
1050
+ reply = await request
1051
+ await asyncio.gather(*maybe_wait)
1052
+ if self.noreply:
1053
+ return None # type: ignore
1054
+ if isinstance(callback, AsyncPreProcessingCallback):
1055
+ await callback.pre_process(self, reply)
1056
+ if self.cache and cacheable:
1057
+ if cache_hit and not use_cached:
1058
+ self.cache.feedback(
1059
+ command.name, keys[0], *command.arguments, match=cached_reply == reply
1060
+ )
1061
+ if not cache_hit:
1062
+ self.cache.put(
1063
+ command.name,
1064
+ keys[0],
1065
+ *command.arguments,
1066
+ value=reply,
1067
+ )
1068
+ return callback(cached_reply if cache_hit else reply, version=self.protocol_version)
996
1069
  except RedisError:
997
1070
  connection.disconnect()
998
1071
  raise
@@ -1121,7 +1194,7 @@ class Redis(Client[AnyStr]):
1121
1194
  """
1122
1195
  from coredis.pipeline import Pipeline
1123
1196
 
1124
- return Pipeline[AnyStr].proxy(self, transaction, watches, timeout)
1197
+ return Pipeline[AnyStr](self, transaction, watches, timeout)
1125
1198
 
1126
1199
  async def transaction(
1127
1200
  self,