coredis 5.5.0__cp313-cp313-macosx_11_0_arm64.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.
- 22fe76227e35f92ab5c3__mypyc.cpython-313-darwin.so +0 -0
- coredis/__init__.py +42 -0
- coredis/_enum.py +42 -0
- coredis/_json.py +11 -0
- coredis/_packer.cpython-313-darwin.so +0 -0
- coredis/_packer.py +71 -0
- coredis/_protocols.py +50 -0
- coredis/_py_311_typing.py +20 -0
- coredis/_py_312_typing.py +17 -0
- coredis/_sidecar.py +114 -0
- coredis/_utils.cpython-313-darwin.so +0 -0
- coredis/_utils.py +440 -0
- coredis/_version.py +34 -0
- coredis/_version.pyi +1 -0
- coredis/cache.py +801 -0
- coredis/client/__init__.py +6 -0
- coredis/client/basic.py +1240 -0
- coredis/client/cluster.py +1265 -0
- coredis/commands/__init__.py +64 -0
- coredis/commands/_key_spec.py +517 -0
- coredis/commands/_utils.py +108 -0
- coredis/commands/_validators.py +159 -0
- coredis/commands/_wrappers.py +175 -0
- coredis/commands/bitfield.py +110 -0
- coredis/commands/constants.py +662 -0
- coredis/commands/core.py +8484 -0
- coredis/commands/function.py +408 -0
- coredis/commands/monitor.py +168 -0
- coredis/commands/pubsub.py +905 -0
- coredis/commands/request.py +108 -0
- coredis/commands/script.py +296 -0
- coredis/commands/sentinel.py +246 -0
- coredis/config.py +50 -0
- coredis/connection.py +906 -0
- coredis/constants.cpython-313-darwin.so +0 -0
- coredis/constants.py +37 -0
- coredis/credentials.py +45 -0
- coredis/exceptions.py +360 -0
- coredis/experimental/__init__.py +1 -0
- coredis/globals.py +23 -0
- coredis/modules/__init__.py +121 -0
- coredis/modules/autocomplete.py +138 -0
- coredis/modules/base.py +262 -0
- coredis/modules/filters.py +1319 -0
- coredis/modules/graph.py +362 -0
- coredis/modules/json.py +691 -0
- coredis/modules/response/__init__.py +0 -0
- coredis/modules/response/_callbacks/__init__.py +0 -0
- coredis/modules/response/_callbacks/autocomplete.py +42 -0
- coredis/modules/response/_callbacks/graph.py +237 -0
- coredis/modules/response/_callbacks/json.py +21 -0
- coredis/modules/response/_callbacks/search.py +221 -0
- coredis/modules/response/_callbacks/timeseries.py +158 -0
- coredis/modules/response/types.py +179 -0
- coredis/modules/search.py +1089 -0
- coredis/modules/timeseries.py +1139 -0
- coredis/parser.cpython-313-darwin.so +0 -0
- coredis/parser.py +344 -0
- coredis/pipeline.py +1225 -0
- coredis/pool/__init__.py +11 -0
- coredis/pool/basic.py +453 -0
- coredis/pool/cluster.py +517 -0
- coredis/pool/nodemanager.py +340 -0
- coredis/py.typed +0 -0
- coredis/recipes/__init__.py +0 -0
- coredis/recipes/credentials/__init__.py +5 -0
- coredis/recipes/credentials/iam_provider.py +63 -0
- coredis/recipes/locks/__init__.py +5 -0
- coredis/recipes/locks/extend.lua +17 -0
- coredis/recipes/locks/lua_lock.py +281 -0
- coredis/recipes/locks/release.lua +10 -0
- coredis/response/__init__.py +5 -0
- coredis/response/_callbacks/__init__.py +538 -0
- coredis/response/_callbacks/acl.py +32 -0
- coredis/response/_callbacks/cluster.py +183 -0
- coredis/response/_callbacks/command.py +86 -0
- coredis/response/_callbacks/connection.py +31 -0
- coredis/response/_callbacks/geo.py +58 -0
- coredis/response/_callbacks/hash.py +85 -0
- coredis/response/_callbacks/keys.py +59 -0
- coredis/response/_callbacks/module.py +33 -0
- coredis/response/_callbacks/script.py +85 -0
- coredis/response/_callbacks/sentinel.py +179 -0
- coredis/response/_callbacks/server.py +241 -0
- coredis/response/_callbacks/sets.py +44 -0
- coredis/response/_callbacks/sorted_set.py +204 -0
- coredis/response/_callbacks/streams.py +185 -0
- coredis/response/_callbacks/strings.py +70 -0
- coredis/response/_callbacks/vector_sets.py +159 -0
- coredis/response/_utils.py +33 -0
- coredis/response/types.py +416 -0
- coredis/retry.py +233 -0
- coredis/sentinel.py +477 -0
- coredis/stream.py +369 -0
- coredis/tokens.py +2286 -0
- coredis/typing.py +593 -0
- coredis-5.5.0.dist-info/METADATA +211 -0
- coredis-5.5.0.dist-info/RECORD +100 -0
- coredis-5.5.0.dist-info/WHEEL +6 -0
- coredis-5.5.0.dist-info/licenses/LICENSE +23 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
import itertools
|
|
6
|
+
import weakref
|
|
7
|
+
from typing import Any, ClassVar, cast
|
|
8
|
+
|
|
9
|
+
from deprecated.sphinx import versionadded
|
|
10
|
+
|
|
11
|
+
from coredis._utils import EncodingInsensitiveDict, nativestr
|
|
12
|
+
from coredis.commands.request import CommandRequest
|
|
13
|
+
from coredis.exceptions import FunctionError
|
|
14
|
+
from coredis.typing import (
|
|
15
|
+
TYPE_CHECKING,
|
|
16
|
+
AnyStr,
|
|
17
|
+
Awaitable,
|
|
18
|
+
Callable,
|
|
19
|
+
Generator,
|
|
20
|
+
Generic,
|
|
21
|
+
KeyT,
|
|
22
|
+
MutableMapping,
|
|
23
|
+
P,
|
|
24
|
+
Parameters,
|
|
25
|
+
R,
|
|
26
|
+
ResponseType,
|
|
27
|
+
StringT,
|
|
28
|
+
TypeVar,
|
|
29
|
+
ValueT,
|
|
30
|
+
add_runtime_checks,
|
|
31
|
+
safe_beartype,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
import coredis.client
|
|
36
|
+
|
|
37
|
+
LibraryT = TypeVar("LibraryT", bound="Library[Any]")
|
|
38
|
+
LibraryStringT = TypeVar("LibraryStringT", bound="Library[str]")
|
|
39
|
+
LibraryBytesT = TypeVar("LibraryBytesT", bound="Library[bytes]")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Library(Generic[AnyStr]):
|
|
43
|
+
#: Class variable equivalent of the :paramref:`Library.name` argument.
|
|
44
|
+
NAME: ClassVar[StringT | None] = None
|
|
45
|
+
#: Class variable equivalent of the :paramref:`Library.code` argument.
|
|
46
|
+
CODE: ClassVar[StringT | None] = None
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
client: coredis.client.Client[AnyStr],
|
|
51
|
+
name: StringT | None = None,
|
|
52
|
+
code: StringT | None = None,
|
|
53
|
+
replace: bool = False,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Abstraction over a library of redis functions
|
|
57
|
+
|
|
58
|
+
Example::
|
|
59
|
+
|
|
60
|
+
library_code = \"\"\"
|
|
61
|
+
#!lua name=coredis
|
|
62
|
+
redis.register_function('myfunc', function(k, a) return a[1] end)
|
|
63
|
+
\"\"\"
|
|
64
|
+
lib = await Library(client, "mylib", library_code)
|
|
65
|
+
assert "1" == await lib["myfunc"]([], [1])
|
|
66
|
+
|
|
67
|
+
When used as a base class the class variables :data:`NAME` and :data:`CODE`
|
|
68
|
+
can be set on the sub class to avoid having to implement a constructor. Constructor
|
|
69
|
+
parameters will take precedence over the class variables.
|
|
70
|
+
|
|
71
|
+
:param client: The coredis client instance to use when calling the functions
|
|
72
|
+
exposed by the library.
|
|
73
|
+
:param name: The name of the library (should match the name in the Shebang
|
|
74
|
+
in the library source).
|
|
75
|
+
:param code: The lua code representing the library
|
|
76
|
+
:param replace: Whether to replace the library when intializing. If ``False``
|
|
77
|
+
an exception will be raised if the library was already loaded in the target
|
|
78
|
+
redis instance.
|
|
79
|
+
"""
|
|
80
|
+
self._client: weakref.ReferenceType[coredis.client.Client[AnyStr]] = weakref.ref(client)
|
|
81
|
+
self.name = nativestr(name or self.NAME)
|
|
82
|
+
self.code = (code or self.CODE or "").lstrip()
|
|
83
|
+
self._functions: EncodingInsensitiveDict = EncodingInsensitiveDict()
|
|
84
|
+
self.replace = replace
|
|
85
|
+
if self.replace and not self.code:
|
|
86
|
+
raise RuntimeError("library code must be provided when the ``replace`` option is used")
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def client(self) -> coredis.client.Client[AnyStr]:
|
|
90
|
+
c = self._client()
|
|
91
|
+
assert c
|
|
92
|
+
return c
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def functions(self) -> MutableMapping[str, Function[AnyStr]]:
|
|
96
|
+
"""
|
|
97
|
+
mapping of function names to :class:`~coredis.commands.function.Function`
|
|
98
|
+
instances that can be directly called.
|
|
99
|
+
"""
|
|
100
|
+
return self._functions
|
|
101
|
+
|
|
102
|
+
async def update(self, new_code: StringT) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Update the code of a library with :paramref:`new_code`
|
|
105
|
+
"""
|
|
106
|
+
self.code = new_code
|
|
107
|
+
if await self.initialize(replace=True):
|
|
108
|
+
return True
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
async def initialize(self: LibraryT, replace: bool = False) -> LibraryT:
|
|
112
|
+
from coredis.pipeline import ClusterPipeline, Pipeline
|
|
113
|
+
|
|
114
|
+
self._functions.clear()
|
|
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)
|
|
120
|
+
if (not library and self.code) or (replace or self.replace):
|
|
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)
|
|
123
|
+
|
|
124
|
+
if not library:
|
|
125
|
+
raise FunctionError(f"No library found for {self.name}")
|
|
126
|
+
|
|
127
|
+
for name, details in library["functions"].items():
|
|
128
|
+
self._functions[name] = Function[AnyStr](
|
|
129
|
+
self.client,
|
|
130
|
+
self.name,
|
|
131
|
+
name,
|
|
132
|
+
bool({b"no-writes", "no-writes"} & details["flags"]),
|
|
133
|
+
)
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __await__(self: LibraryT) -> Generator[Any, None, LibraryT]:
|
|
137
|
+
return self.initialize().__await__()
|
|
138
|
+
|
|
139
|
+
def __getitem__(self, function: str) -> Function[AnyStr] | None:
|
|
140
|
+
return cast(Function[AnyStr] | None, self._functions.get(function))
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
@versionadded(version="3.5.0")
|
|
144
|
+
def wraps(
|
|
145
|
+
cls,
|
|
146
|
+
function_name: str,
|
|
147
|
+
key_spec: list[KeyT] | None = None,
|
|
148
|
+
param_is_key: Callable[[inspect.Parameter], bool] = lambda p: (
|
|
149
|
+
p.annotation in {"KeyT", KeyT}
|
|
150
|
+
),
|
|
151
|
+
runtime_checks: bool = False,
|
|
152
|
+
readonly: bool | None = None,
|
|
153
|
+
) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, CommandRequest[R]]]:
|
|
154
|
+
"""
|
|
155
|
+
Decorator for wrapping methods of subclasses of :class:`Library`
|
|
156
|
+
as entry points to the functions contained in the library. This allows
|
|
157
|
+
exposing a strict signature instead of that which :meth:`Function.__call__`
|
|
158
|
+
provides. The callable being decorated should **not** have an implementation as
|
|
159
|
+
it will never be called.
|
|
160
|
+
|
|
161
|
+
The main objective of the decorator is to allow you to represent a lua library of
|
|
162
|
+
functions as a python class having strict (and type safe) methods as entry points.
|
|
163
|
+
Internally the decorator separates ``keys`` from ``args`` before calling
|
|
164
|
+
:meth:`coredis.Redis.fcall`.
|
|
165
|
+
|
|
166
|
+
Mapping the decorated method's arguments to key providers is done either by
|
|
167
|
+
using :paramref:`key_spec` or :paramref:`param_is_key`. All other parameters of the
|
|
168
|
+
decorated method are assumed to be ``args`` consumed by the lua function.
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
The following example demonstrates most of the functionality provided by the
|
|
172
|
+
decorator::
|
|
173
|
+
|
|
174
|
+
import coredis
|
|
175
|
+
from coredis.commands import Library
|
|
176
|
+
from coredis.typing import KeyT, RedisValueT
|
|
177
|
+
from typing import List
|
|
178
|
+
|
|
179
|
+
class MyAwesomeLibrary(Library):
|
|
180
|
+
NAME = "mylib"
|
|
181
|
+
CODE = \"\"\"
|
|
182
|
+
#!lua name=mylib
|
|
183
|
+
|
|
184
|
+
redis.register_function('echo', function(k, a)
|
|
185
|
+
return a[1]
|
|
186
|
+
end)
|
|
187
|
+
redis.register_function('ping', function()
|
|
188
|
+
return "PONG"
|
|
189
|
+
end)
|
|
190
|
+
redis.register_function('get', function(k, a)
|
|
191
|
+
return redis.call("GET", k[1])
|
|
192
|
+
end)
|
|
193
|
+
redis.register_function('hmget', function(k, a)
|
|
194
|
+
local values = {}
|
|
195
|
+
local fields = {}
|
|
196
|
+
local response = {}
|
|
197
|
+
local i = 1
|
|
198
|
+
local j = 1
|
|
199
|
+
|
|
200
|
+
while a[i] do
|
|
201
|
+
fields[j] = a[i]
|
|
202
|
+
i = i + 2
|
|
203
|
+
j = j + 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
for idx, key in ipairs(k) do
|
|
207
|
+
values = redis.call("HMGET", key, unpack(fields))
|
|
208
|
+
for idx, value in ipairs(values) do
|
|
209
|
+
if not response[idx] and value then
|
|
210
|
+
response[idx] = value
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
for idx, value in ipairs(fields) do
|
|
215
|
+
if not response[idx] then
|
|
216
|
+
response[idx] = a[idx*2]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
return response
|
|
220
|
+
end)
|
|
221
|
+
\"\"\"
|
|
222
|
+
|
|
223
|
+
@Library.wraps("echo")
|
|
224
|
+
def echo(self, value: ValueT) -> CommandRequest[RedisValueT]: ...
|
|
225
|
+
|
|
226
|
+
@Library.wraps("ping"print(c)
|
|
227
|
+
)
|
|
228
|
+
def ping(self) -> CommandRequest[str]: ...
|
|
229
|
+
|
|
230
|
+
@Library.wraps("get")
|
|
231
|
+
def get(self, key: KeyT) -> CommandRequest[ValueT]: ...
|
|
232
|
+
|
|
233
|
+
@Library.wraps("hmmget")
|
|
234
|
+
def hmmget(self, *keys: KeyT, **fields_with_values: RedisValueT):
|
|
235
|
+
\"\"\"
|
|
236
|
+
Return values of ``fields_with_values`` on a first come first serve
|
|
237
|
+
basis from the hashes at ``keys``. Since ``fields_with_values`` is a mapping
|
|
238
|
+
the keys are mapped to hash fields and the values are used
|
|
239
|
+
as defaults if they are not found in any of the hashes at ``keys``
|
|
240
|
+
\"\"\"
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
client = coredis.Redis()
|
|
244
|
+
lib = await MyAwesomeLibrary(client, replace=True)
|
|
245
|
+
await client.set("hello", "world")
|
|
246
|
+
# True
|
|
247
|
+
await lib.echo("hello world")
|
|
248
|
+
# b"hello world"
|
|
249
|
+
await lib.ping()
|
|
250
|
+
# b"pong"
|
|
251
|
+
await lib.get("hello")
|
|
252
|
+
# b"hello"
|
|
253
|
+
await client.hset("k1", {"c": 3, "d": 4})
|
|
254
|
+
await client.hset("k2", {"a": 1, "b": 2})
|
|
255
|
+
await lib.hmmget("k1", "k2", a=-1, b=-2, c=-3, d=-4, e=-5)
|
|
256
|
+
# [b"1", b"2", b"3", b"4", b"-5"]
|
|
257
|
+
|
|
258
|
+
:param key_spec: list of parameters of the decorated method that will
|
|
259
|
+
be passed as the :paramref:`keys` argument to :meth:`__call__`. If provided
|
|
260
|
+
this parameter takes precedence over using :paramref:`param_is_key` to
|
|
261
|
+
determine if a parameter is a key provider.
|
|
262
|
+
:param param_is_key: a callable that accepts a single argument of type
|
|
263
|
+
:class:`inspect.Parameter` and returns ``True`` if the parameter points to a key
|
|
264
|
+
that should be appended to the :paramref:`__call__.keys` argument of
|
|
265
|
+
:meth:`__call__`. The default implementation marks a parameter as a key
|
|
266
|
+
provider if it is of type :data:`coredis.typing.KeyT` and is only used
|
|
267
|
+
if :paramref:`key_spec` is ``None``.
|
|
268
|
+
:param runtime_checks: Whether to enable runtime type checking of input arguments
|
|
269
|
+
and return values. (requires :pypi:`beartype`). If :data:`False` the function will
|
|
270
|
+
still get runtime type checking if the environment configuration ``COREDIS_RUNTIME_CHECKS``
|
|
271
|
+
is set - for details see :ref:`handbook/typing:runtime type checking`.
|
|
272
|
+
:param readonly: If ``True`` forces this function to use :meth:`coredis.Redis.fcall_ro`
|
|
273
|
+
|
|
274
|
+
:return: A function that has a signature mirroring the decorated function.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, CommandRequest[R]]:
|
|
278
|
+
sig = inspect.signature(func)
|
|
279
|
+
first_arg: str = list(sig.parameters.keys())[0]
|
|
280
|
+
runtime_check_wrapper = add_runtime_checks if not runtime_checks else safe_beartype
|
|
281
|
+
key_params = (
|
|
282
|
+
key_spec if key_spec else [n for n, p in sig.parameters.items() if param_is_key(p)]
|
|
283
|
+
)
|
|
284
|
+
arg_fetch: dict[str, Callable[..., Parameters[Any]]] = {
|
|
285
|
+
n: (
|
|
286
|
+
(lambda v: [v])
|
|
287
|
+
if p.kind
|
|
288
|
+
in {
|
|
289
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
290
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
291
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
292
|
+
}
|
|
293
|
+
else (
|
|
294
|
+
(lambda v: list(itertools.chain.from_iterable(v.items())))
|
|
295
|
+
if p.kind == inspect.Parameter.VAR_KEYWORD
|
|
296
|
+
else lambda v: list(v)
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
for n, p in sig.parameters.items()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
def split_args(
|
|
303
|
+
*a: P.args, **k: P.kwargs
|
|
304
|
+
) -> tuple[Library[AnyStr], Parameters[KeyT], Parameters[ValueT]]:
|
|
305
|
+
bound_arguments = sig.bind(*a, **k)
|
|
306
|
+
bound_arguments.apply_defaults()
|
|
307
|
+
arguments: dict[str, Any] = bound_arguments.arguments
|
|
308
|
+
instance: Library[AnyStr] = arguments.pop(first_arg)
|
|
309
|
+
if not isinstance(instance, Library):
|
|
310
|
+
raise RuntimeError(
|
|
311
|
+
f"{instance.__class__.__name__} is not a subclass of"
|
|
312
|
+
" coredis.commands.function.Library therefore it's methods cannot be bound "
|
|
313
|
+
" to a redis library using ``Library.wrap``."
|
|
314
|
+
" Please refer to the documentation at https://coredis.readthedocs.org/"
|
|
315
|
+
" for instructions on how to bind a class to a redis library."
|
|
316
|
+
)
|
|
317
|
+
keys: list[KeyT] = []
|
|
318
|
+
args: list[ValueT] = []
|
|
319
|
+
for name in sig.parameters:
|
|
320
|
+
if name == first_arg:
|
|
321
|
+
continue
|
|
322
|
+
values = arg_fetch[name](arguments[name])
|
|
323
|
+
if name in key_params:
|
|
324
|
+
keys.extend(values)
|
|
325
|
+
else:
|
|
326
|
+
args.extend(values)
|
|
327
|
+
return instance, keys, args
|
|
328
|
+
|
|
329
|
+
@runtime_check_wrapper
|
|
330
|
+
@functools.wraps(func)
|
|
331
|
+
def _inner(*args: P.args, **kwargs: P.kwargs) -> CommandRequest[R]:
|
|
332
|
+
instance, keys, arguments = split_args(*args, **kwargs)
|
|
333
|
+
if (func := instance.functions.get(function_name, None)) is None:
|
|
334
|
+
raise AttributeError(
|
|
335
|
+
f"Library {instance.name} has no registered function {function_name}"
|
|
336
|
+
)
|
|
337
|
+
return cast(CommandRequest[R], func(keys, arguments, readonly=readonly))
|
|
338
|
+
|
|
339
|
+
return _inner
|
|
340
|
+
|
|
341
|
+
return wrapper
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class Function(Generic[AnyStr]):
|
|
345
|
+
def __init__(
|
|
346
|
+
self,
|
|
347
|
+
client: coredis.client.Client[AnyStr],
|
|
348
|
+
library_name: StringT,
|
|
349
|
+
name: StringT,
|
|
350
|
+
readonly: bool = False,
|
|
351
|
+
):
|
|
352
|
+
"""
|
|
353
|
+
Wrapper to call a redis function that has already been loaded
|
|
354
|
+
|
|
355
|
+
:param library_name: Name of the library under which the function is registered
|
|
356
|
+
:param name: Name of the function this instance represents
|
|
357
|
+
:param readonly: If ``True`` the function will be called with
|
|
358
|
+
:meth:`coredis.Redis.fcall_ro` instead of :meth:`coredis.Redis.fcall`
|
|
359
|
+
|
|
360
|
+
Example::
|
|
361
|
+
|
|
362
|
+
func = await Function(client, "mylib", "myfunc")
|
|
363
|
+
response = await func(keys=["a"], args=[1])
|
|
364
|
+
"""
|
|
365
|
+
self._client: weakref.ReferenceType[coredis.client.Client[AnyStr]] = weakref.ref(client)
|
|
366
|
+
self.library: Library[AnyStr] = Library[AnyStr](client, library_name)
|
|
367
|
+
self.name = name
|
|
368
|
+
self.readonly = readonly
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def client(self) -> coredis.client.Client[AnyStr]:
|
|
372
|
+
c = self._client()
|
|
373
|
+
assert c
|
|
374
|
+
return c
|
|
375
|
+
|
|
376
|
+
async def initialize(self) -> Function[AnyStr]:
|
|
377
|
+
await self.library
|
|
378
|
+
return self
|
|
379
|
+
|
|
380
|
+
def __await__(self) -> Generator[Any, None, Function[AnyStr]]:
|
|
381
|
+
return self.initialize().__await__()
|
|
382
|
+
|
|
383
|
+
def __call__(
|
|
384
|
+
self,
|
|
385
|
+
keys: Parameters[KeyT] | None = None,
|
|
386
|
+
args: Parameters[ValueT] | None = None,
|
|
387
|
+
*,
|
|
388
|
+
client: coredis.client.Client[AnyStr] | None = None,
|
|
389
|
+
readonly: bool | None = None,
|
|
390
|
+
) -> CommandRequest[ResponseType]:
|
|
391
|
+
"""
|
|
392
|
+
Wrapper to call :meth:`~coredis.Redis.fcall` with the
|
|
393
|
+
function named :paramref:`Function.name` registered under
|
|
394
|
+
the library at :paramref:`Function.library`
|
|
395
|
+
|
|
396
|
+
:param keys: The keys this function will reference
|
|
397
|
+
:param args: The arguments expected by the function
|
|
398
|
+
:param readonly: If ``True`` forces the function to use :meth:`coredis.Redis.fcall_ro`
|
|
399
|
+
"""
|
|
400
|
+
if client is None:
|
|
401
|
+
client = self.client
|
|
402
|
+
if readonly is None:
|
|
403
|
+
readonly = self.readonly
|
|
404
|
+
|
|
405
|
+
if readonly:
|
|
406
|
+
return client.fcall_ro(self.name, keys or [], args or [])
|
|
407
|
+
else:
|
|
408
|
+
return client.fcall(self.name, keys or [], args or [])
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from deprecated.sphinx import deprecated
|
|
8
|
+
|
|
9
|
+
from coredis.commands.constants import CommandName
|
|
10
|
+
from coredis.exceptions import ConnectionError, RedisError
|
|
11
|
+
from coredis.response.types import MonitorResult
|
|
12
|
+
from coredis.typing import AnyStr, Callable, Generator, Generic, Self, TypeVar
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import coredis.client
|
|
16
|
+
import coredis.connection
|
|
17
|
+
|
|
18
|
+
MonitorT = TypeVar("MonitorT", bound="Monitor[Any]")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@deprecated("The implementation of a monitor will be removed in 6.0", version="5.2.0")
|
|
22
|
+
class Monitor(Generic[AnyStr]):
|
|
23
|
+
"""
|
|
24
|
+
Monitor is useful for handling the ``MONITOR`` command to the redis server.
|
|
25
|
+
|
|
26
|
+
It can be used as an infinite async iterator::
|
|
27
|
+
|
|
28
|
+
async with client.monitor() as monitor:
|
|
29
|
+
async for command in monitor:
|
|
30
|
+
print(command.time, command.client_type, command.command, command.args)
|
|
31
|
+
|
|
32
|
+
Alternatively, each command can be fetched explicitly::
|
|
33
|
+
|
|
34
|
+
monitor = client.monitor()
|
|
35
|
+
command1 = await monitor.get_command()
|
|
36
|
+
command2 = await monitor.get_command()
|
|
37
|
+
await monitor.aclose()
|
|
38
|
+
|
|
39
|
+
If you are only interested in triggering callbacks when a command is received
|
|
40
|
+
by the monitor::
|
|
41
|
+
def monitor_handler(result: MonitorResult) -> None:
|
|
42
|
+
....
|
|
43
|
+
|
|
44
|
+
monitor = await client.monitor(response_handler=monitor_handler)
|
|
45
|
+
# when done
|
|
46
|
+
await monitor.aclose()
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
client: coredis.client.Client[AnyStr],
|
|
52
|
+
response_handler: Callable[[MonitorResult], None] | None = None,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
:param client: a Redis client
|
|
56
|
+
:param response_handler: optional callback to call whenever a
|
|
57
|
+
command is received by the monitor
|
|
58
|
+
"""
|
|
59
|
+
self.client: coredis.client.Client[AnyStr] = client
|
|
60
|
+
self.encoding = client.encoding
|
|
61
|
+
self.connection: coredis.connection.Connection | None = None
|
|
62
|
+
self.monitoring = False
|
|
63
|
+
self._monitor_results: asyncio.Queue[MonitorResult] = asyncio.Queue()
|
|
64
|
+
self._monitor_task: asyncio.Task[None] | None = None
|
|
65
|
+
self._response_handler = response_handler
|
|
66
|
+
|
|
67
|
+
def __aiter__(self) -> Monitor[AnyStr]:
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
async def __anext__(self) -> MonitorResult:
|
|
71
|
+
"""
|
|
72
|
+
Infinite iterator that streams back the next command processed by the
|
|
73
|
+
monitored server.
|
|
74
|
+
"""
|
|
75
|
+
return await self.get_command()
|
|
76
|
+
|
|
77
|
+
def __await__(self: MonitorT) -> Generator[Any, None, MonitorT]:
|
|
78
|
+
return self.__start_monitor().__await__()
|
|
79
|
+
|
|
80
|
+
async def __aenter__(self) -> Self:
|
|
81
|
+
await self.__start_monitor()
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
async def __aexit__(
|
|
85
|
+
self,
|
|
86
|
+
exc_type: type[BaseException] | None,
|
|
87
|
+
exc_value: BaseException | None,
|
|
88
|
+
traceback: TracebackType | None,
|
|
89
|
+
) -> None:
|
|
90
|
+
await self.aclose()
|
|
91
|
+
|
|
92
|
+
async def get_command(self) -> MonitorResult:
|
|
93
|
+
"""
|
|
94
|
+
Wait for the next command issued and return the details
|
|
95
|
+
"""
|
|
96
|
+
await self.__start_monitor()
|
|
97
|
+
return await self._monitor_results.get()
|
|
98
|
+
|
|
99
|
+
async def aclose(self) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Stop monitoring by issuing a ``RESET`` command
|
|
102
|
+
and release the connection.
|
|
103
|
+
"""
|
|
104
|
+
return await self.__stop_monitoring()
|
|
105
|
+
|
|
106
|
+
@deprecated("Use :meth:`aclose` instead", version="4.21.0")
|
|
107
|
+
async def stop(self) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Stop monitoring by issuing a ``RESET`` command
|
|
110
|
+
and release the connection.
|
|
111
|
+
"""
|
|
112
|
+
return await self.aclose()
|
|
113
|
+
|
|
114
|
+
async def __connect(self) -> None:
|
|
115
|
+
if self.connection is None:
|
|
116
|
+
self.connection = await self.client.connection_pool.get_connection()
|
|
117
|
+
|
|
118
|
+
async def __start_monitor(self: MonitorT) -> MonitorT:
|
|
119
|
+
if self.monitoring:
|
|
120
|
+
return self
|
|
121
|
+
await self.__connect()
|
|
122
|
+
assert self.connection
|
|
123
|
+
request = await self.connection.create_request(CommandName.MONITOR, decode=False)
|
|
124
|
+
response = await request
|
|
125
|
+
if not response == b"OK": # noqa
|
|
126
|
+
raise RedisError(f"Failed to start MONITOR {response!r}")
|
|
127
|
+
if not self._monitor_task or self._monitor_task.done():
|
|
128
|
+
self._monitor_task = asyncio.create_task(self._monitor())
|
|
129
|
+
self.monitoring = True
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
async def __stop_monitoring(self) -> None:
|
|
133
|
+
if self.connection:
|
|
134
|
+
request = await self.connection.create_request(CommandName.RESET, decode=False)
|
|
135
|
+
response = await request
|
|
136
|
+
if not response == CommandName.RESET: # noqa
|
|
137
|
+
raise RedisError("Failed to reset connection")
|
|
138
|
+
self.__reset()
|
|
139
|
+
|
|
140
|
+
def __reset(self) -> None:
|
|
141
|
+
if self.connection:
|
|
142
|
+
self.connection.disconnect()
|
|
143
|
+
self.client.connection_pool.release(self.connection)
|
|
144
|
+
if self._monitor_task and not self._monitor_task.done():
|
|
145
|
+
try:
|
|
146
|
+
self._monitor_task.cancel()
|
|
147
|
+
except RuntimeError: # noqa
|
|
148
|
+
pass
|
|
149
|
+
self.monitoring = False
|
|
150
|
+
self.connection = None
|
|
151
|
+
|
|
152
|
+
async def _monitor(self) -> None:
|
|
153
|
+
while self.connection:
|
|
154
|
+
try:
|
|
155
|
+
response = await self.connection.fetch_push_message(block=True)
|
|
156
|
+
if isinstance(response, bytes):
|
|
157
|
+
response = response.decode(self.encoding)
|
|
158
|
+
assert isinstance(response, str)
|
|
159
|
+
result = MonitorResult.parse_response_string(response)
|
|
160
|
+
if self._response_handler:
|
|
161
|
+
self._response_handler(result)
|
|
162
|
+
else:
|
|
163
|
+
self._monitor_results.put_nowait(result)
|
|
164
|
+
except asyncio.CancelledError:
|
|
165
|
+
break
|
|
166
|
+
except ConnectionError:
|
|
167
|
+
break
|
|
168
|
+
self.__reset()
|