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.
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
@@ -9,6 +9,7 @@ from typing import Any, ClassVar, cast
9
9
  from deprecated.sphinx import versionadded
10
10
 
11
11
  from coredis._utils import EncodingInsensitiveDict, nativestr
12
+ from coredis.commands.request import CommandRequest
12
13
  from coredis.exceptions import FunctionError
13
14
  from coredis.typing import (
14
15
  TYPE_CHECKING,
@@ -18,6 +19,7 @@ from coredis.typing import (
18
19
  Generator,
19
20
  Generic,
20
21
  KeyT,
22
+ MutableMapping,
21
23
  P,
22
24
  Parameters,
23
25
  R,
@@ -90,7 +92,7 @@ class Library(Generic[AnyStr]):
90
92
  return c
91
93
 
92
94
  @property
93
- def functions(self) -> dict[str, Function[AnyStr]]:
95
+ def functions(self) -> MutableMapping[str, Function[AnyStr]]:
94
96
  """
95
97
  mapping of function names to :class:`~coredis.commands.function.Function`
96
98
  instances that can be directly called.
@@ -107,11 +109,17 @@ class Library(Generic[AnyStr]):
107
109
  return False
108
110
 
109
111
  async def initialize(self: LibraryT, replace: bool = False) -> LibraryT:
112
+ from coredis.pipeline import ClusterPipeline, Pipeline
113
+
110
114
  self._functions.clear()
111
- library = (await self.client.function_list(self.name)).get(self.name)
115
+ if isinstance(self.client, (Pipeline, ClusterPipeline)):
116
+ redis_client = self.client.client
117
+ else:
118
+ redis_client = self.client
119
+ library = (await redis_client.function_list(self.name)).get(self.name)
112
120
  if (not library and self.code) or (replace or self.replace):
113
- await self.client.function_load(self.code, replace=replace or self.replace)
114
- library = (await self.client.function_list(self.name)).get(self.name)
121
+ await redis_client.function_load(self.code, replace=replace or self.replace)
122
+ library = (await redis_client.function_list(self.name)).get(self.name)
115
123
 
116
124
  if not library:
117
125
  raise FunctionError(f"No library found for {self.name}")
@@ -142,7 +150,7 @@ class Library(Generic[AnyStr]):
142
150
  ),
143
151
  runtime_checks: bool = False,
144
152
  readonly: bool | None = None,
145
- ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
153
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, CommandRequest[R]]]:
146
154
  """
147
155
  Decorator for wrapping methods of subclasses of :class:`Library`
148
156
  as entry points to the functions contained in the library. This allows
@@ -165,7 +173,7 @@ class Library(Generic[AnyStr]):
165
173
 
166
174
  import coredis
167
175
  from coredis.commands import Library
168
- from coredis.typing import KeyT, ValueT
176
+ from coredis.typing import KeyT, RedisValueT
169
177
  from typing import List
170
178
 
171
179
  class MyAwesomeLibrary(Library):
@@ -213,22 +221,24 @@ class Library(Generic[AnyStr]):
213
221
  \"\"\"
214
222
 
215
223
  @Library.wraps("echo")
216
- async def echo(self, value: ValueT) -> ValueT: ...
224
+ def echo(self, value: ValueT) -> CommandRequest[RedisValueT]: ...
217
225
 
218
- @Library.wraps("ping")
219
- async def ping(self) -> str: ...
226
+ @Library.wraps("ping"print(c)
227
+ )
228
+ def ping(self) -> CommandRequest[str]: ...
220
229
 
221
230
  @Library.wraps("get")
222
- async def get(self, key: KeyT) -> ValueT: ...
231
+ def get(self, key: KeyT) -> CommandRequest[ValueT]: ...
223
232
 
224
233
  @Library.wraps("hmmget")
225
- async def hmmget(self, *keys: KeyT, **fields_with_values: ValueT): ...
234
+ def hmmget(self, *keys: KeyT, **fields_with_values: RedisValueT):
226
235
  \"\"\"
227
236
  Return values of ``fields_with_values`` on a first come first serve
228
237
  basis from the hashes at ``keys``. Since ``fields_with_values`` is a mapping
229
238
  the keys are mapped to hash fields and the values are used
230
239
  as defaults if they are not found in any of the hashes at ``keys``
231
240
  \"\"\"
241
+ ...
232
242
 
233
243
  client = coredis.Redis()
234
244
  lib = await MyAwesomeLibrary(client, replace=True)
@@ -264,7 +274,7 @@ class Library(Generic[AnyStr]):
264
274
  :return: A function that has a signature mirroring the decorated function.
265
275
  """
266
276
 
267
- def wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
277
+ def wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, CommandRequest[R]]:
268
278
  sig = inspect.signature(func)
269
279
  first_arg: str = list(sig.parameters.keys())[0]
270
280
  runtime_check_wrapper = add_runtime_checks if not runtime_checks else safe_beartype
@@ -318,16 +328,13 @@ class Library(Generic[AnyStr]):
318
328
 
319
329
  @runtime_check_wrapper
320
330
  @functools.wraps(func)
321
- async def _inner(*args: P.args, **kwargs: P.kwargs) -> R:
331
+ def _inner(*args: P.args, **kwargs: P.kwargs) -> CommandRequest[R]:
322
332
  instance, keys, arguments = split_args(*args, **kwargs)
323
- func = instance.functions[function_name]
324
- if not func:
333
+ if (func := instance.functions.get(function_name, None)) is None:
325
334
  raise AttributeError(
326
335
  f"Library {instance.name} has no registered function {function_name}"
327
336
  )
328
- # TODO: atleast lie with a cast.
329
- # mypy doesn't like the cast
330
- return await func(keys, arguments, readonly=readonly) # type: ignore
337
+ return cast(CommandRequest[R], func(keys, arguments, readonly=readonly))
331
338
 
332
339
  return _inner
333
340
 
@@ -373,14 +380,14 @@ class Function(Generic[AnyStr]):
373
380
  def __await__(self) -> Generator[Any, None, Function[AnyStr]]:
374
381
  return self.initialize().__await__()
375
382
 
376
- async def __call__(
383
+ def __call__(
377
384
  self,
378
385
  keys: Parameters[KeyT] | None = None,
379
386
  args: Parameters[ValueT] | None = None,
380
387
  *,
381
388
  client: coredis.client.Client[AnyStr] | None = None,
382
389
  readonly: bool | None = None,
383
- ) -> ResponseType:
390
+ ) -> CommandRequest[ResponseType]:
384
391
  """
385
392
  Wrapper to call :meth:`~coredis.Redis.fcall` with the
386
393
  function named :paramref:`Function.name` registered under
@@ -396,6 +403,6 @@ class Function(Generic[AnyStr]):
396
403
  readonly = self.readonly
397
404
 
398
405
  if readonly:
399
- return await client.fcall_ro(self.name, keys or [], args or [])
406
+ return client.fcall_ro(self.name, keys or [], args or [])
400
407
  else:
401
- return await client.fcall(self.name, keys or [], args or [])
408
+ return client.fcall(self.name, keys or [], args or [])
@@ -1,9 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import threading
5
- from asyncio import AbstractEventLoop
6
- from concurrent.futures import Future
7
4
  from types import TracebackType
8
5
  from typing import TYPE_CHECKING, Any
9
6
 
@@ -113,44 +110,6 @@ class Monitor(Generic[AnyStr]):
113
110
  """
114
111
  return await self.aclose()
115
112
 
116
- @deprecated(
117
- """
118
- Running the monitor in a thread is no longer necessary
119
- since the constructor for :class:`Monitor` will accept a callback
120
- to be triggered when a command is received by the monitor and this will
121
- automatically happen in a background async task that is started as soon
122
- as :class:`Monitor` is initialized.
123
-
124
- To achieve identical results simply await an instance of :class:`Monitor`
125
- instantiated with :paramref:`Monitor.response_handler` and call :meth:`aclose`
126
- when done.
127
-
128
-
129
- .. code:: python
130
-
131
- monitor = await client.monitor(response_handler=my_handler)
132
- # when done
133
- await pubsub.aclose()
134
- """,
135
- version="4.21.0",
136
- )
137
- def run_in_thread(
138
- self,
139
- response_handler: Callable[[MonitorResult], None],
140
- loop: AbstractEventLoop | None = None,
141
- ) -> MonitorThread:
142
- """
143
- Runs the monitor in a :class:`MonitorThread` and invokes :paramref:`response_handler`
144
- for every command received.
145
-
146
- To stop the processing call :meth:`MonitorThread.stop` on the instance
147
- returned by this method.
148
-
149
- """
150
- monitor_thread = MonitorThread(self, loop or asyncio.get_event_loop(), response_handler)
151
- monitor_thread.start()
152
- return monitor_thread
153
-
154
113
  async def __connect(self) -> None:
155
114
  if self.connection is None:
156
115
  self.connection = await self.client.connection_pool.get_connection()
@@ -206,33 +165,3 @@ class Monitor(Generic[AnyStr]):
206
165
  except ConnectionError:
207
166
  break
208
167
  self.__reset()
209
-
210
-
211
- class MonitorThread(threading.Thread):
212
- """
213
- Thread to be used to run monitor
214
- """
215
-
216
- def __init__(
217
- self,
218
- monitor: Monitor[Any],
219
- loop: asyncio.events.AbstractEventLoop,
220
- response_handler: Callable[[MonitorResult], None],
221
- ):
222
- self._monitor = monitor
223
- self._loop = loop
224
- self._response_handler = response_handler
225
- self._future: Future[None] | None = None
226
- super().__init__()
227
-
228
- def run(self) -> None:
229
- self._future = asyncio.run_coroutine_threadsafe(self._run(), self._loop)
230
-
231
- async def _run(self) -> None:
232
- async with self._monitor:
233
- async for command in self._monitor:
234
- self._response_handler(command)
235
-
236
- def stop(self) -> None:
237
- if self._future:
238
- self._future.cancel()
@@ -2,16 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import inspect
5
- import threading
6
5
  from asyncio import CancelledError
7
- from concurrent.futures import Future
8
- from contextlib import aclosing, suppress
6
+ from contextlib import suppress
9
7
  from functools import partial
10
8
  from types import TracebackType
11
9
  from typing import TYPE_CHECKING, Any, cast
12
10
 
13
11
  import async_timeout
14
- from deprecated.sphinx import deprecated, versionadded
12
+ from deprecated.sphinx import versionadded
15
13
 
16
14
  from coredis._utils import CaseAndEncodingInsensitiveEnum, b, hash_slot, nativestr
17
15
  from coredis.commands.constants import CommandName
@@ -33,12 +31,12 @@ from coredis.typing import (
33
31
  Mapping,
34
32
  MutableMapping,
35
33
  Parameters,
34
+ RedisValueT,
36
35
  ResponsePrimitive,
37
36
  ResponseType,
38
37
  Self,
39
38
  StringT,
40
39
  TypeVar,
41
- ValueT,
42
40
  )
43
41
 
44
42
  if TYPE_CHECKING:
@@ -216,61 +214,6 @@ class BasePubSub(Generic[AnyStr, PoolT]):
216
214
 
217
215
  await self.execute_command(CommandName.UNSUBSCRIBE, *channels)
218
216
 
219
- @deprecated(
220
- """
221
- Use :meth:`get_message` with :paramref:`get_message.timeout` as `None`
222
- or the instance itself as an async iterator to infinitely consume messages
223
-
224
- .. code:: python
225
-
226
- pubsub = client.pubsub()
227
-
228
- # instead of
229
- while True:
230
- message = await pubsub.listen()
231
-
232
- # do
233
- async for message in pubsub:
234
- ....
235
- # or
236
- while True:
237
- message = await pubsub.get_message(timeout=None)
238
-
239
- If you were using this method to simply pull messages and/or
240
- ensure callbacks were being triggered when the message arrives
241
- this isn't necessary anymore and simply creating a pubsub instance
242
- and registering the handlers is sufficient to ensure the messages
243
- are fetched and callbacks are triggered.
244
-
245
- .. code:: python
246
-
247
- # instead of
248
- pubsub = client.pubsub()
249
- await pubsub.subscribe(**{"topic-a": topic_a_handler})
250
- while True:
251
- await pubsub.listen()
252
-
253
- # do
254
- pubsub = await client.pubsub(
255
- channel_handlers={"topic-a": topic_a_handler}
256
- )
257
- # when done
258
- await pubsub.aclose()
259
-
260
-
261
- """,
262
- version="4.21.0",
263
- )
264
- async def listen(self) -> PubSubMessage | None:
265
- """
266
- Listens for messages on channels this client has been subscribed to
267
- """
268
-
269
- await self.initialize()
270
- if self.subscribed:
271
- return self._filter_ignored_messages(await self._message_queue.get())
272
- return None
273
-
274
217
  async def get_message(
275
218
  self,
276
219
  ignore_subscribe_messages: bool = False,
@@ -333,7 +276,7 @@ class BasePubSub(Generic[AnyStr, PoolT]):
333
276
  return value
334
277
 
335
278
  async def execute_command(
336
- self, command: bytes, *args: ValueT, **options: ValueT
279
+ self, command: bytes, *args: RedisValueT, **options: RedisValueT
337
280
  ) -> ResponseType | None:
338
281
  """
339
282
  Executes a publish/subscribe command
@@ -440,44 +383,6 @@ class BasePubSub(Generic[AnyStr, PoolT]):
440
383
 
441
384
  return message
442
385
 
443
- @deprecated(
444
- """
445
- Registered handlers are called in a background async task automatically
446
- as soon as this pubsub instance is initialized. To achieve identical results
447
- just subscribe to the channels or patterns with the appropriate handlers
448
- and call :meth:`aclose` when done.
449
-
450
-
451
- .. code:: python
452
-
453
- pubsub = client.pubsub()
454
- await client.subscribe(topic=topic_handler)
455
- await client.psubscribe(topic=pattern_handler)
456
- # when done
457
- await pubsub.aclose()
458
- """,
459
- version="4.21.0",
460
- )
461
- def run_in_thread(self, poll_timeout: float = 1.0) -> PubSubWorkerThread:
462
- """
463
- Run the listeners in a thread. For each message received on a
464
- subscribed channel or pattern the registered handlers will be invoked.
465
-
466
- To stop listening invoke :meth:`~coredis.commands.pubsub.PubSubWorkerThread.stop`
467
- on the returned instance
468
- """
469
- for channel, handler in self.channels.items():
470
- if handler is None:
471
- raise PubSubError(f"Channel: {channel!r} has no handler registered")
472
-
473
- for pattern, handler in self.patterns.items():
474
- if handler is None:
475
- raise PubSubError(f"Pattern: {pattern!r} has no handler registered")
476
- thread = PubSubWorkerThread(self, poll_timeout=poll_timeout)
477
- thread.start()
478
-
479
- return thread
480
-
481
386
  async def _consumer(self) -> None:
482
387
  while self.initialized:
483
388
  try:
@@ -509,7 +414,7 @@ class BasePubSub(Generic[AnyStr, PoolT]):
509
414
  self,
510
415
  connection: BaseConnection,
511
416
  command: Callable[..., Awaitable[None]] | Callable[..., Awaitable[ResponseType]],
512
- *args: ValueT,
417
+ *args: RedisValueT,
513
418
  ) -> ResponseType | None:
514
419
  try:
515
420
  return await command(*args)
@@ -653,7 +558,7 @@ class ClusterPubSub(BasePubSub[AnyStr, "coredis.pool.ClusterConnectionPool"]):
653
558
  """
654
559
 
655
560
  async def execute_command(
656
- self, command: bytes, *args: ValueT, **options: ValueT
561
+ self, command: bytes, *args: RedisValueT, **options: RedisValueT
657
562
  ) -> ResponseType | None:
658
563
  await self.initialize()
659
564
  assert self.connection
@@ -807,7 +712,7 @@ class ShardedPubSub(BasePubSub[AnyStr, "coredis.pool.ClusterConnectionPool"]):
807
712
  raise NotImplementedError("Sharded PubSub does not support subscription by pattern")
808
713
 
809
714
  async def execute_command(
810
- self, command: bytes, *args: ValueT, **options: ValueT
715
+ self, command: bytes, *args: RedisValueT, **options: RedisValueT
811
716
  ) -> ResponseType | None:
812
717
  await self.initialize()
813
718
 
@@ -997,43 +902,3 @@ class ShardedPubSub(BasePubSub[AnyStr, "coredis.pool.ClusterConnectionPool"]):
997
902
  if self.shard_connections:
998
903
  await self.unsubscribe()
999
904
  self.close()
1000
-
1001
-
1002
- class PubSubWorkerThread(threading.Thread):
1003
- def __init__(
1004
- self,
1005
- pubsub: BasePubSub[Any, Any],
1006
- poll_timeout: float = 1.0,
1007
- ):
1008
- super().__init__()
1009
- self._pubsub = pubsub
1010
- self._poll_timeout = poll_timeout
1011
- self._running = False
1012
- self._loop = asyncio.get_running_loop()
1013
- self._future: Future[None] | None = None
1014
-
1015
- async def _run(self) -> None:
1016
- async with aclosing(self._pubsub) as pubsub:
1017
- try:
1018
- while pubsub.subscribed:
1019
- await pubsub.get_message(
1020
- ignore_subscribe_messages=True, timeout=self._poll_timeout
1021
- )
1022
- except CancelledError:
1023
- self._running = False
1024
-
1025
- def run(self) -> None:
1026
- """
1027
- :meta private:
1028
- """
1029
- if self._running:
1030
- return
1031
- self._running = True
1032
- self._future = asyncio.run_coroutine_threadsafe(self._run(), self._loop)
1033
-
1034
- def stop(self) -> None:
1035
- """
1036
- Stop the worker thread from processing any more messages
1037
- """
1038
- if self._future:
1039
- self._future.cancel()
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ from typing import Any, cast
5
+
6
+ from coredis._protocols import AbstractExecutor
7
+ from coredis.typing import (
8
+ Awaitable,
9
+ Callable,
10
+ ExecutionParameters,
11
+ Generator,
12
+ Serializable,
13
+ TypeAdapter,
14
+ TypeVar,
15
+ ValueT,
16
+ )
17
+
18
+ #: Covariant type used for generalizing :class:`~coredis.command.CommandRequest`
19
+ CommandResponseT = TypeVar("CommandResponseT", covariant=True)
20
+
21
+ TransformedResponse = TypeVar("TransformedResponse")
22
+ empty_adapter = TypeAdapter()
23
+
24
+
25
+ class CommandRequest(Awaitable[CommandResponseT]):
26
+ response: Awaitable[CommandResponseT]
27
+
28
+ def __init__(
29
+ self,
30
+ client: AbstractExecutor,
31
+ name: bytes,
32
+ *arguments: ValueT,
33
+ callback: Callable[..., CommandResponseT],
34
+ execution_parameters: ExecutionParameters | None = None,
35
+ ) -> None:
36
+ """
37
+ The default command request object which is returned by all
38
+ methods mirroring redis commands.
39
+
40
+ :param client: The instance of the :class:`coredis.Redis` that
41
+ will be used to call :meth:`~coredis.Redis.execute_command`
42
+ :param name: The name of the command
43
+ :param arguments: All arguments (in redis format) to be passed to the command
44
+ :param callback: The callback to be used to transform the RESP response
45
+ :param execution_parameters: Any additional parameters to be passed to
46
+ :meth:`coredis.Redis.execute_command`
47
+ """
48
+ self.name = name
49
+ self.callback = callback
50
+ self.execution_parameters = execution_parameters or {}
51
+ self.client: AbstractExecutor = client
52
+ self.arguments = tuple(
53
+ self.type_adapter.serialize(k) if isinstance(k, Serializable) else k for k in arguments
54
+ )
55
+
56
+ def run(self) -> Awaitable[CommandResponseT]:
57
+ if not hasattr(self, "response"):
58
+ self.response = self.client.execute_command(
59
+ self, self.callback, **self.execution_parameters
60
+ )
61
+
62
+ return self.response
63
+
64
+ def transform(
65
+ self, transformer: type[TransformedResponse]
66
+ ) -> CommandRequest[TransformedResponse]:
67
+ """
68
+ :param transformer: A type that was registered with the client
69
+ using :meth:`~coredis.typing.TypeAdapter.register_deserializer`
70
+ or decorated by :meth:`~coredis.typing.TypeAdapter.deserializer`
71
+
72
+ :return: a command request object that when awaited will return the
73
+ transformed response
74
+
75
+ For example when used with a redis command::
76
+
77
+ client = coredis.Redis(....)
78
+ @client.type_adapter.deserializer
79
+ def _(value: bytes) -> int:
80
+ return int(value)
81
+
82
+ await client.set("fubar", 1)
83
+ raw: bytes = await client.get("fubar")
84
+ int_value: int = await client.get("fubar").transform(int)
85
+ """
86
+ transform_func = functools.partial(
87
+ self.type_adapter.deserialize,
88
+ return_type=transformer,
89
+ )
90
+ return cast(type[CommandRequest[TransformedResponse]], self.__class__)(
91
+ self.client,
92
+ self.name,
93
+ *self.arguments,
94
+ callback=lambda resp, **kwargs: transform_func(self.callback(resp, **kwargs)),
95
+ execution_parameters=self.execution_parameters,
96
+ )
97
+
98
+ @property
99
+ def type_adapter(self) -> TypeAdapter:
100
+ from coredis.client import Client
101
+
102
+ if isinstance(self.client, Client):
103
+ return self.client.type_adapter
104
+
105
+ return empty_adapter
106
+
107
+ def __await__(self) -> Generator[Any, Any, CommandResponseT]:
108
+ return self.run().__await__()
@@ -8,9 +8,9 @@ from typing import TYPE_CHECKING, Any, cast
8
8
 
9
9
  from deprecated.sphinx import versionadded
10
10
 
11
- from coredis._protocols import SupportsScript
12
11
  from coredis._utils import b
13
12
  from coredis.exceptions import NoScriptError
13
+ from coredis.retry import ConstantRetryPolicy, retryable
14
14
  from coredis.typing import (
15
15
  AnyStr,
16
16
  Awaitable,
@@ -20,6 +20,7 @@ from coredis.typing import (
20
20
  P,
21
21
  Parameters,
22
22
  R,
23
+ RedisValueT,
23
24
  ResponseType,
24
25
  StringT,
25
26
  ValueT,
@@ -51,7 +52,7 @@ class Script(Generic[AnyStr]):
51
52
 
52
53
  def __init__(
53
54
  self,
54
- registered_client: SupportsScript[AnyStr] | None = None,
55
+ registered_client: coredis.client.Client[AnyStr] | None = None,
55
56
  script: StringT | None = None,
56
57
  readonly: bool = False,
57
58
  ):
@@ -64,7 +65,7 @@ class Script(Generic[AnyStr]):
64
65
  :param readonly: If ``True`` the script will be called with
65
66
  :meth:`coredis.Redis.evalsha_ro` instead of :meth:`coredis.Redis.evalsha`
66
67
  """
67
- self.registered_client: SupportsScript[AnyStr] | None = registered_client
68
+ self.registered_client: coredis.client.Client[AnyStr] | None = registered_client
68
69
  self.script: StringT
69
70
  if not script:
70
71
  raise RuntimeError("No script provided")
@@ -72,16 +73,16 @@ class Script(Generic[AnyStr]):
72
73
  self.sha = hashlib.sha1(b(script)).hexdigest() # type: ignore
73
74
  self.readonly = readonly
74
75
 
75
- async def __call__(
76
+ def __call__(
76
77
  self,
77
78
  keys: Parameters[KeyT] | None = None,
78
79
  args: Parameters[ValueT] | None = None,
79
- client: SupportsScript[AnyStr] | None = None,
80
+ client: coredis.client.Client[AnyStr] | None = None,
80
81
  readonly: bool | None = None,
81
- ) -> ResponseType:
82
+ ) -> Awaitable[ResponseType]:
82
83
  """
83
84
  Executes the script registered in :paramref:`Script.script` using
84
- :meth:`coredis.Redis.evalsha`. Additionally if the script was not yet
85
+ :meth:`coredis.Redis.evalsha`. Additionally, if the script was not yet
85
86
  registered on the instance, it will automatically do that as well
86
87
  and cache the sha at :data:`Script.sha`
87
88
 
@@ -103,26 +104,24 @@ class Script(Generic[AnyStr]):
103
104
  if readonly is None:
104
105
  readonly = self.readonly
105
106
 
107
+ method = client.evalsha_ro if readonly else client.evalsha
108
+
106
109
  # make sure the Redis server knows about the script
107
110
  if isinstance(client, Pipeline):
108
111
  # make sure this script is good to go on pipeline
109
112
  cast(Pipeline[AnyStr], client).scripts.add(self)
110
-
111
- method = client.evalsha_ro if readonly else client.evalsha
112
- try:
113
- return cast(ResponseType, await method(self.sha, keys=keys, args=args))
114
- except NoScriptError:
115
- # Maybe the client is pointed to a different server than the client
116
- # that created this instance?
117
- # Overwrite the sha just in case there was a discrepancy.
118
- self.sha = await client.script_load(self.script)
119
- return cast(ResponseType, await method(self.sha, keys=keys, args=args))
113
+ return method(self.sha, keys=keys, args=args)
114
+ else:
115
+ return retryable(
116
+ ConstantRetryPolicy((NoScriptError,), 1, 0),
117
+ failure_hook=lambda _: client.script_load(self.script),
118
+ )(method)(self.sha, keys=keys, args=args)
120
119
 
121
120
  async def execute(
122
121
  self,
123
122
  keys: Parameters[KeyT] | None = None,
124
123
  args: Parameters[ValueT] | None = None,
125
- client: SupportsScript[AnyStr] | None = None,
124
+ client: coredis.client.Client[AnyStr] | None = None,
126
125
  readonly: bool | None = None,
127
126
  ) -> ResponseType:
128
127
  """
@@ -169,12 +168,12 @@ class Script(Generic[AnyStr]):
169
168
  passed to redis as an ``arg``::
170
169
 
171
170
  import coredis
172
- from coredis.typing import KeyT, ValueT
171
+ from coredis.typing import KeyT, RedisValueT
173
172
  from typing import List
174
173
 
175
174
  client = coredis.Redis()
176
175
  @client.register_script("return {KEYS[1], ARGV[1]}").wraps()
177
- async def echo_key_value(key: KeyT, value: ValueT) -> List[ValueT]: ...
176
+ async def echo_key_value(key: KeyT, value: RedisValueT) -> List[RedisValueT]: ...
178
177
 
179
178
  k, v = await echo_key_value("co", "redis")
180
179
  # (b"co", b"redis")
@@ -260,13 +259,13 @@ class Script(Generic[AnyStr]):
260
259
  bound_arguments: inspect.BoundArguments,
261
260
  ) -> tuple[
262
261
  Parameters[KeyT],
263
- Parameters[ValueT],
262
+ Parameters[RedisValueT],
264
263
  coredis.client.Client[AnyStr] | None,
265
264
  ]:
266
265
  bound_arguments.apply_defaults()
267
266
  arguments = bound_arguments.arguments
268
267
  keys: list[KeyT] = []
269
- args: list[ValueT] = []
268
+ args: list[RedisValueT] = []
270
269
  for name in sig.parameters:
271
270
  if name not in arg_fetch:
272
271
  continue