coredis 4.23.1__py3-none-any.whl → 5.0.0rc1__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.
- coredis/__init__.py +1 -3
- coredis/_packer.py +10 -10
- coredis/_protocols.py +23 -32
- coredis/_py_311_typing.py +20 -0
- coredis/_py_312_typing.py +17 -0
- coredis/_utils.py +49 -51
- coredis/_version.py +3 -3
- coredis/cache.py +57 -82
- coredis/client/__init__.py +1 -2
- coredis/client/basic.py +129 -56
- coredis/client/cluster.py +147 -70
- coredis/commands/__init__.py +27 -7
- coredis/commands/_key_spec.py +11 -10
- coredis/commands/_utils.py +1 -1
- coredis/commands/_validators.py +30 -20
- coredis/commands/_wrappers.py +19 -99
- coredis/commands/bitfield.py +10 -2
- coredis/commands/constants.py +20 -3
- coredis/commands/core.py +1627 -1246
- coredis/commands/function.py +29 -22
- coredis/commands/monitor.py +0 -71
- coredis/commands/pubsub.py +7 -142
- coredis/commands/request.py +108 -0
- coredis/commands/script.py +9 -9
- coredis/commands/sentinel.py +60 -49
- coredis/connection.py +14 -15
- coredis/exceptions.py +2 -2
- coredis/experimental/__init__.py +0 -4
- coredis/globals.py +3 -0
- coredis/modules/autocomplete.py +28 -30
- coredis/modules/base.py +15 -31
- coredis/modules/filters.py +269 -245
- coredis/modules/graph.py +61 -62
- coredis/modules/json.py +172 -140
- coredis/modules/response/_callbacks/autocomplete.py +5 -4
- coredis/modules/response/_callbacks/graph.py +34 -29
- coredis/modules/response/_callbacks/json.py +5 -3
- coredis/modules/response/_callbacks/search.py +49 -53
- coredis/modules/response/_callbacks/timeseries.py +18 -30
- coredis/modules/response/types.py +1 -5
- coredis/modules/search.py +186 -169
- coredis/modules/timeseries.py +184 -164
- coredis/parser.py +6 -19
- coredis/pipeline.py +391 -422
- coredis/pool/basic.py +7 -7
- coredis/pool/cluster.py +3 -3
- coredis/pool/nodemanager.py +10 -3
- coredis/response/_callbacks/__init__.py +76 -57
- coredis/response/_callbacks/acl.py +0 -3
- coredis/response/_callbacks/cluster.py +25 -16
- coredis/response/_callbacks/command.py +8 -6
- coredis/response/_callbacks/connection.py +4 -3
- coredis/response/_callbacks/geo.py +17 -13
- coredis/response/_callbacks/hash.py +13 -11
- coredis/response/_callbacks/keys.py +9 -5
- coredis/response/_callbacks/module.py +2 -3
- coredis/response/_callbacks/script.py +6 -8
- coredis/response/_callbacks/sentinel.py +21 -17
- coredis/response/_callbacks/server.py +36 -14
- coredis/response/_callbacks/sets.py +3 -4
- coredis/response/_callbacks/sorted_set.py +27 -24
- coredis/response/_callbacks/streams.py +22 -13
- coredis/response/_callbacks/strings.py +7 -6
- coredis/response/_callbacks/vector_sets.py +126 -0
- coredis/response/types.py +13 -4
- coredis/sentinel.py +1 -1
- coredis/stream.py +4 -3
- coredis/tokens.py +343 -16
- coredis/typing.py +432 -79
- {coredis-4.23.1.dist-info → coredis-5.0.0rc1.dist-info}/METADATA +4 -5
- coredis-5.0.0rc1.dist-info/RECORD +95 -0
- coredis/client/keydb.py +0 -336
- coredis/pipeline.pyi +0 -2103
- coredis-4.23.1.dist-info/RECORD +0 -93
- {coredis-4.23.1.dist-info → coredis-5.0.0rc1.dist-info}/WHEEL +0 -0
- {coredis-4.23.1.dist-info → coredis-5.0.0rc1.dist-info}/licenses/LICENSE +0 -0
- {coredis-4.23.1.dist-info → coredis-5.0.0rc1.dist-info}/top_level.txt +0 -0
coredis/commands/function.py
CHANGED
|
@@ -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) ->
|
|
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
|
-
|
|
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
|
|
114
|
-
library = (await
|
|
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,
|
|
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,
|
|
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
|
-
|
|
224
|
+
def echo(self, value: ValueT) -> CommandRequest[RedisValueT]: ...
|
|
217
225
|
|
|
218
|
-
@Library.wraps("ping")
|
|
219
|
-
|
|
226
|
+
@Library.wraps("ping"print(c)
|
|
227
|
+
)
|
|
228
|
+
def ping(self) -> CommandRequest[str]: ...
|
|
220
229
|
|
|
221
230
|
@Library.wraps("get")
|
|
222
|
-
|
|
231
|
+
def get(self, key: KeyT) -> CommandRequest[ValueT]: ...
|
|
223
232
|
|
|
224
233
|
@Library.wraps("hmmget")
|
|
225
|
-
|
|
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,
|
|
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
|
-
|
|
331
|
+
def _inner(*args: P.args, **kwargs: P.kwargs) -> CommandRequest[R]:
|
|
322
332
|
instance, keys, arguments = split_args(*args, **kwargs)
|
|
323
|
-
func
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
406
|
+
return client.fcall_ro(self.name, keys or [], args or [])
|
|
400
407
|
else:
|
|
401
|
-
return
|
|
408
|
+
return client.fcall(self.name, keys or [], args or [])
|
coredis/commands/monitor.py
CHANGED
|
@@ -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()
|
coredis/commands/pubsub.py
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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__()
|
coredis/commands/script.py
CHANGED
|
@@ -20,9 +20,9 @@ from coredis.typing import (
|
|
|
20
20
|
P,
|
|
21
21
|
Parameters,
|
|
22
22
|
R,
|
|
23
|
+
RedisValueT,
|
|
23
24
|
ResponseType,
|
|
24
25
|
StringT,
|
|
25
|
-
ValueT,
|
|
26
26
|
add_runtime_checks,
|
|
27
27
|
safe_beartype,
|
|
28
28
|
)
|
|
@@ -75,7 +75,7 @@ class Script(Generic[AnyStr]):
|
|
|
75
75
|
async def __call__(
|
|
76
76
|
self,
|
|
77
77
|
keys: Parameters[KeyT] | None = None,
|
|
78
|
-
args: Parameters[
|
|
78
|
+
args: Parameters[RedisValueT] | None = None,
|
|
79
79
|
client: SupportsScript[AnyStr] | None = None,
|
|
80
80
|
readonly: bool | None = None,
|
|
81
81
|
) -> ResponseType:
|
|
@@ -110,18 +110,18 @@ class Script(Generic[AnyStr]):
|
|
|
110
110
|
|
|
111
111
|
method = client.evalsha_ro if readonly else client.evalsha
|
|
112
112
|
try:
|
|
113
|
-
return
|
|
113
|
+
return await method(self.sha, keys=keys, args=args)
|
|
114
114
|
except NoScriptError:
|
|
115
115
|
# Maybe the client is pointed to a different server than the client
|
|
116
116
|
# that created this instance?
|
|
117
117
|
# Overwrite the sha just in case there was a discrepancy.
|
|
118
118
|
self.sha = await client.script_load(self.script)
|
|
119
|
-
return
|
|
119
|
+
return await method(self.sha, keys=keys, args=args)
|
|
120
120
|
|
|
121
121
|
async def execute(
|
|
122
122
|
self,
|
|
123
123
|
keys: Parameters[KeyT] | None = None,
|
|
124
|
-
args: Parameters[
|
|
124
|
+
args: Parameters[RedisValueT] | None = None,
|
|
125
125
|
client: SupportsScript[AnyStr] | None = None,
|
|
126
126
|
readonly: bool | None = None,
|
|
127
127
|
) -> ResponseType:
|
|
@@ -169,12 +169,12 @@ class Script(Generic[AnyStr]):
|
|
|
169
169
|
passed to redis as an ``arg``::
|
|
170
170
|
|
|
171
171
|
import coredis
|
|
172
|
-
from coredis.typing import KeyT,
|
|
172
|
+
from coredis.typing import KeyT, RedisValueT
|
|
173
173
|
from typing import List
|
|
174
174
|
|
|
175
175
|
client = coredis.Redis()
|
|
176
176
|
@client.register_script("return {KEYS[1], ARGV[1]}").wraps()
|
|
177
|
-
async def echo_key_value(key: KeyT, value:
|
|
177
|
+
async def echo_key_value(key: KeyT, value: RedisValueT) -> List[RedisValueT]: ...
|
|
178
178
|
|
|
179
179
|
k, v = await echo_key_value("co", "redis")
|
|
180
180
|
# (b"co", b"redis")
|
|
@@ -260,13 +260,13 @@ class Script(Generic[AnyStr]):
|
|
|
260
260
|
bound_arguments: inspect.BoundArguments,
|
|
261
261
|
) -> tuple[
|
|
262
262
|
Parameters[KeyT],
|
|
263
|
-
Parameters[
|
|
263
|
+
Parameters[RedisValueT],
|
|
264
264
|
coredis.client.Client[AnyStr] | None,
|
|
265
265
|
]:
|
|
266
266
|
bound_arguments.apply_defaults()
|
|
267
267
|
arguments = bound_arguments.arguments
|
|
268
268
|
keys: list[KeyT] = []
|
|
269
|
-
args: list[
|
|
269
|
+
args: list[RedisValueT] = []
|
|
270
270
|
for name in sig.parameters:
|
|
271
271
|
if name not in arg_fetch:
|
|
272
272
|
continue
|