coredis 5.2.0__cp314-cp314t-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.
Potentially problematic release.
This version of coredis might be problematic. Click here for more details.
- 22fe76227e35f92ab5c3__mypyc.cpython-314t-darwin.so +0 -0
- coredis/__init__.py +42 -0
- coredis/_enum.py +42 -0
- coredis/_json.py +11 -0
- coredis/_packer.cpython-314t-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-314t-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 +1238 -0
- coredis/client/cluster.py +1264 -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-314t-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 +117 -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-314t-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 +580 -0
- coredis-5.2.0.dist-info/METADATA +211 -0
- coredis-5.2.0.dist-info/RECORD +100 -0
- coredis-5.2.0.dist-info/WHEEL +6 -0
- coredis-5.2.0.dist-info/licenses/LICENSE +23 -0
coredis/client/basic.py
ADDED
|
@@ -0,0 +1,1238 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import contextvars
|
|
6
|
+
import functools
|
|
7
|
+
import random
|
|
8
|
+
import warnings
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from ssl import SSLContext
|
|
11
|
+
from typing import TYPE_CHECKING, Any, cast, overload
|
|
12
|
+
|
|
13
|
+
from deprecated.sphinx import deprecated, versionadded
|
|
14
|
+
from packaging import version
|
|
15
|
+
from packaging.version import InvalidVersion, Version
|
|
16
|
+
|
|
17
|
+
from coredis._utils import EncodingInsensitiveDict, nativestr
|
|
18
|
+
from coredis.cache import AbstractCache
|
|
19
|
+
from coredis.commands import CommandRequest
|
|
20
|
+
from coredis.commands._key_spec import KeySpec
|
|
21
|
+
from coredis.commands.constants import CommandFlag, CommandName
|
|
22
|
+
from coredis.commands.core import CoreCommands
|
|
23
|
+
from coredis.commands.function import Library
|
|
24
|
+
from coredis.commands.monitor import Monitor
|
|
25
|
+
from coredis.commands.pubsub import PubSub, SubscriptionCallback
|
|
26
|
+
from coredis.commands.script import Script
|
|
27
|
+
from coredis.commands.sentinel import SentinelCommands
|
|
28
|
+
from coredis.config import Config
|
|
29
|
+
from coredis.connection import (
|
|
30
|
+
BaseConnection,
|
|
31
|
+
RedisSSLContext,
|
|
32
|
+
UnixDomainSocketConnection,
|
|
33
|
+
)
|
|
34
|
+
from coredis.credentials import AbstractCredentialProvider
|
|
35
|
+
from coredis.exceptions import (
|
|
36
|
+
AuthenticationError,
|
|
37
|
+
AuthorizationError,
|
|
38
|
+
ConnectionError,
|
|
39
|
+
PersistenceError,
|
|
40
|
+
RedisError,
|
|
41
|
+
ReplicationError,
|
|
42
|
+
ResponseError,
|
|
43
|
+
TimeoutError,
|
|
44
|
+
UnknownCommandError,
|
|
45
|
+
WatchError,
|
|
46
|
+
)
|
|
47
|
+
from coredis.globals import CACHEABLE_COMMANDS, COMMAND_FLAGS, READONLY_COMMANDS
|
|
48
|
+
from coredis.modules import ModuleMixin
|
|
49
|
+
from coredis.pool import ConnectionPool
|
|
50
|
+
from coredis.response._callbacks import (
|
|
51
|
+
AsyncPreProcessingCallback,
|
|
52
|
+
NoopCallback,
|
|
53
|
+
ResponseCallback,
|
|
54
|
+
)
|
|
55
|
+
from coredis.response.types import MonitorResult, ScoredMember
|
|
56
|
+
from coredis.retry import ConstantRetryPolicy, NoRetryPolicy, RetryPolicy
|
|
57
|
+
from coredis.typing import (
|
|
58
|
+
AnyStr,
|
|
59
|
+
AsyncGenerator,
|
|
60
|
+
AsyncIterator,
|
|
61
|
+
Callable,
|
|
62
|
+
Coroutine,
|
|
63
|
+
ExecutionParameters,
|
|
64
|
+
Generator,
|
|
65
|
+
Generic,
|
|
66
|
+
Iterator,
|
|
67
|
+
KeyT,
|
|
68
|
+
Literal,
|
|
69
|
+
Mapping,
|
|
70
|
+
Parameters,
|
|
71
|
+
ParamSpec,
|
|
72
|
+
RedisCommandP,
|
|
73
|
+
RedisValueT,
|
|
74
|
+
ResponseType,
|
|
75
|
+
StringT,
|
|
76
|
+
T_co,
|
|
77
|
+
TypeAdapter,
|
|
78
|
+
TypeVar,
|
|
79
|
+
Unpack,
|
|
80
|
+
ValueT,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
P = ParamSpec("P")
|
|
84
|
+
R = TypeVar("R")
|
|
85
|
+
|
|
86
|
+
if TYPE_CHECKING:
|
|
87
|
+
import coredis.pipeline
|
|
88
|
+
|
|
89
|
+
ClientT = TypeVar("ClientT", bound="Client[Any]")
|
|
90
|
+
RedisT = TypeVar("RedisT", bound="Redis[Any]")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Client(
|
|
94
|
+
Generic[AnyStr],
|
|
95
|
+
CoreCommands[AnyStr],
|
|
96
|
+
ModuleMixin[AnyStr],
|
|
97
|
+
SentinelCommands[AnyStr],
|
|
98
|
+
):
|
|
99
|
+
cache: AbstractCache | None
|
|
100
|
+
connection_pool: ConnectionPool
|
|
101
|
+
decode_responses: bool
|
|
102
|
+
encoding: str
|
|
103
|
+
protocol_version: Literal[2, 3]
|
|
104
|
+
server_version: Version | None
|
|
105
|
+
callback_storage: dict[type[ResponseCallback[Any, Any, Any]], dict[str, Any]]
|
|
106
|
+
type_adapter: TypeAdapter
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
host: str | None = "localhost",
|
|
111
|
+
port: int | None = 6379,
|
|
112
|
+
db: int = 0,
|
|
113
|
+
username: str | None = None,
|
|
114
|
+
password: str | None = None,
|
|
115
|
+
credential_provider: AbstractCredentialProvider | None = None,
|
|
116
|
+
stream_timeout: float | None = None,
|
|
117
|
+
connect_timeout: float | None = None,
|
|
118
|
+
connection_pool: ConnectionPool | None = None,
|
|
119
|
+
connection_pool_cls: type[ConnectionPool] = ConnectionPool,
|
|
120
|
+
unix_socket_path: str | None = None,
|
|
121
|
+
encoding: str = "utf-8",
|
|
122
|
+
decode_responses: bool = False,
|
|
123
|
+
ssl: bool = False,
|
|
124
|
+
ssl_context: SSLContext | None = None,
|
|
125
|
+
ssl_keyfile: str | None = None,
|
|
126
|
+
ssl_certfile: str | None = None,
|
|
127
|
+
ssl_cert_reqs: Literal["optional", "required", "none"] | None = None,
|
|
128
|
+
ssl_check_hostname: bool | None = None,
|
|
129
|
+
ssl_ca_certs: str | None = None,
|
|
130
|
+
max_connections: int | None = None,
|
|
131
|
+
max_idle_time: float = 0,
|
|
132
|
+
idle_check_interval: float = 1,
|
|
133
|
+
client_name: str | None = None,
|
|
134
|
+
protocol_version: Literal[2, 3] = 3,
|
|
135
|
+
verify_version: bool = True,
|
|
136
|
+
noreply: bool = False,
|
|
137
|
+
retry_policy: RetryPolicy = NoRetryPolicy(),
|
|
138
|
+
noevict: bool = False,
|
|
139
|
+
notouch: bool = False,
|
|
140
|
+
type_adapter: TypeAdapter | None = None,
|
|
141
|
+
**kwargs: Any,
|
|
142
|
+
):
|
|
143
|
+
if not connection_pool:
|
|
144
|
+
kwargs = {
|
|
145
|
+
"db": db,
|
|
146
|
+
"username": username,
|
|
147
|
+
"password": password,
|
|
148
|
+
"credential_provider": credential_provider,
|
|
149
|
+
"encoding": encoding,
|
|
150
|
+
"stream_timeout": stream_timeout,
|
|
151
|
+
"connect_timeout": connect_timeout,
|
|
152
|
+
"max_connections": max_connections,
|
|
153
|
+
"decode_responses": decode_responses,
|
|
154
|
+
"max_idle_time": max_idle_time,
|
|
155
|
+
"idle_check_interval": idle_check_interval,
|
|
156
|
+
"client_name": client_name,
|
|
157
|
+
"protocol_version": protocol_version,
|
|
158
|
+
"noreply": noreply,
|
|
159
|
+
"noevict": noevict,
|
|
160
|
+
"notouch": notouch,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if unix_socket_path is not None:
|
|
164
|
+
kwargs.update(
|
|
165
|
+
{
|
|
166
|
+
"path": unix_socket_path,
|
|
167
|
+
"connection_class": UnixDomainSocketConnection,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
# TCP specific options
|
|
172
|
+
kwargs.update({"host": host, "port": port})
|
|
173
|
+
|
|
174
|
+
if ssl_context is not None:
|
|
175
|
+
kwargs["ssl_context"] = ssl_context
|
|
176
|
+
elif ssl:
|
|
177
|
+
ssl_context = RedisSSLContext(
|
|
178
|
+
ssl_keyfile,
|
|
179
|
+
ssl_certfile,
|
|
180
|
+
ssl_cert_reqs,
|
|
181
|
+
ssl_ca_certs,
|
|
182
|
+
ssl_check_hostname,
|
|
183
|
+
).get()
|
|
184
|
+
kwargs["ssl_context"] = ssl_context
|
|
185
|
+
connection_pool = connection_pool_cls(**kwargs)
|
|
186
|
+
|
|
187
|
+
self.connection_pool = connection_pool
|
|
188
|
+
self.encoding = connection_pool.encoding
|
|
189
|
+
self.decode_responses = connection_pool.decode_responses
|
|
190
|
+
connection_protocol_version = (
|
|
191
|
+
connection_pool.connection_kwargs.get("protocol_version") or protocol_version
|
|
192
|
+
)
|
|
193
|
+
assert connection_protocol_version in {
|
|
194
|
+
2,
|
|
195
|
+
3,
|
|
196
|
+
}, "Protocol version can only be one of {2,3}"
|
|
197
|
+
self.protocol_version = connection_protocol_version
|
|
198
|
+
self.server_version: Version | None = None
|
|
199
|
+
self.verify_version = verify_version
|
|
200
|
+
self.__noreply = noreply
|
|
201
|
+
self._noreplycontext: contextvars.ContextVar[bool | None] = contextvars.ContextVar(
|
|
202
|
+
"noreply", default=None
|
|
203
|
+
)
|
|
204
|
+
self._waitcontext: contextvars.ContextVar[tuple[int, int] | None] = contextvars.ContextVar(
|
|
205
|
+
"wait", default=None
|
|
206
|
+
)
|
|
207
|
+
self._waitaof_context: contextvars.ContextVar[tuple[int, int, int] | None] = (
|
|
208
|
+
contextvars.ContextVar("waitaof", default=None)
|
|
209
|
+
)
|
|
210
|
+
self.retry_policy = retry_policy
|
|
211
|
+
self._module_info: dict[str, version.Version] | None = None
|
|
212
|
+
self.callback_storage = defaultdict(dict)
|
|
213
|
+
self.type_adapter = type_adapter or TypeAdapter()
|
|
214
|
+
|
|
215
|
+
def create_request(
|
|
216
|
+
self,
|
|
217
|
+
name: bytes,
|
|
218
|
+
*arguments: ValueT,
|
|
219
|
+
callback: Callable[..., T_co],
|
|
220
|
+
execution_parameters: ExecutionParameters | None = None,
|
|
221
|
+
) -> CommandRequest[T_co]:
|
|
222
|
+
"""
|
|
223
|
+
Factory method to create a command request awaitable.
|
|
224
|
+
Subclasses of :class:`coredis.client.Client` can override this method
|
|
225
|
+
if custom behavior is required. See :class:`~coredis.commands.CommandRequest`
|
|
226
|
+
for details.
|
|
227
|
+
|
|
228
|
+
:param name: The name of the command
|
|
229
|
+
:param arguments: all arguments sent to the command
|
|
230
|
+
:param callback: a callback that takes the RESP response and converts it
|
|
231
|
+
into a shape to be returned
|
|
232
|
+
:return: An instance of a command request bound to this client.
|
|
233
|
+
"""
|
|
234
|
+
return CommandRequest(
|
|
235
|
+
self, name, *arguments, callback=callback, execution_parameters=execution_parameters
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def noreply(self) -> bool:
|
|
240
|
+
if not hasattr(self, "_noreplycontext"):
|
|
241
|
+
return False
|
|
242
|
+
ctx = self._noreplycontext.get()
|
|
243
|
+
if ctx is not None:
|
|
244
|
+
return ctx
|
|
245
|
+
return self.__noreply
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def requires_wait(self) -> bool:
|
|
249
|
+
if not hasattr(self, "_waitcontext") or not self._waitcontext.get():
|
|
250
|
+
return False
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def requires_waitaof(self) -> bool:
|
|
255
|
+
if not hasattr(self, "_waitaof_context") or not self._waitaof_context.get():
|
|
256
|
+
return False
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
def get_server_module_version(self, module: str) -> version.Version | None:
|
|
260
|
+
return (self._module_info or {}).get(module)
|
|
261
|
+
|
|
262
|
+
def _ensure_server_version(self, version: str | None) -> None:
|
|
263
|
+
if not self.verify_version or Config.optimized:
|
|
264
|
+
return
|
|
265
|
+
if not version:
|
|
266
|
+
return
|
|
267
|
+
if not self.server_version and version:
|
|
268
|
+
try:
|
|
269
|
+
self.server_version = Version(nativestr(version))
|
|
270
|
+
except InvalidVersion:
|
|
271
|
+
warnings.warn(
|
|
272
|
+
(
|
|
273
|
+
f"Server reported an invalid version: {version}."
|
|
274
|
+
"If this is expected you can dismiss this warning by passing "
|
|
275
|
+
"verify_version=False to the client constructor"
|
|
276
|
+
),
|
|
277
|
+
category=UserWarning,
|
|
278
|
+
)
|
|
279
|
+
self.verify_version = False
|
|
280
|
+
self.server_version = None
|
|
281
|
+
|
|
282
|
+
async def _ensure_wait(
|
|
283
|
+
self, command: RedisCommandP, connection: BaseConnection
|
|
284
|
+
) -> asyncio.Future[None]:
|
|
285
|
+
maybe_wait: asyncio.Future[None] = asyncio.get_running_loop().create_future()
|
|
286
|
+
wait = self._waitcontext.get()
|
|
287
|
+
if wait and wait[0] > 0:
|
|
288
|
+
|
|
289
|
+
def check_wait(wait: tuple[int, int], response: asyncio.Future[ResponseType]) -> None:
|
|
290
|
+
exc = response.exception()
|
|
291
|
+
if exc:
|
|
292
|
+
maybe_wait.set_exception(exc)
|
|
293
|
+
elif not cast(int, response.result()) >= wait[0]:
|
|
294
|
+
maybe_wait.set_exception(ReplicationError(command.name, wait[0], wait[1]))
|
|
295
|
+
else:
|
|
296
|
+
maybe_wait.set_result(None)
|
|
297
|
+
|
|
298
|
+
request = await connection.create_request(CommandName.WAIT, *wait, decode=False)
|
|
299
|
+
request.add_done_callback(functools.partial(check_wait, wait))
|
|
300
|
+
else:
|
|
301
|
+
maybe_wait.set_result(None)
|
|
302
|
+
return maybe_wait
|
|
303
|
+
|
|
304
|
+
async def _ensure_persistence(
|
|
305
|
+
self, command: RedisCommandP, connection: BaseConnection
|
|
306
|
+
) -> asyncio.Future[None]:
|
|
307
|
+
maybe_wait: asyncio.Future[None] = asyncio.get_running_loop().create_future()
|
|
308
|
+
waitaof = self._waitaof_context.get()
|
|
309
|
+
if waitaof and waitaof[0] > 0:
|
|
310
|
+
|
|
311
|
+
def check_wait(
|
|
312
|
+
waitaof: tuple[int, int, int], response: asyncio.Future[ResponseType]
|
|
313
|
+
) -> None:
|
|
314
|
+
exc = response.exception()
|
|
315
|
+
if exc:
|
|
316
|
+
maybe_wait.set_exception(exc)
|
|
317
|
+
else:
|
|
318
|
+
res = cast(tuple[int, int], response.result())
|
|
319
|
+
if not (res[0] >= waitaof[0] and res[1] >= waitaof[1]):
|
|
320
|
+
maybe_wait.set_exception(PersistenceError(command.name, *waitaof))
|
|
321
|
+
else:
|
|
322
|
+
maybe_wait.set_result(None)
|
|
323
|
+
|
|
324
|
+
request = await connection.create_request(CommandName.WAITAOF, *waitaof, decode=False)
|
|
325
|
+
request.add_done_callback(functools.partial(check_wait, waitaof))
|
|
326
|
+
else:
|
|
327
|
+
maybe_wait.set_result(None)
|
|
328
|
+
return maybe_wait
|
|
329
|
+
|
|
330
|
+
async def _populate_module_versions(self) -> None:
|
|
331
|
+
if self.noreply or getattr(self, "_module_info", None) is not None:
|
|
332
|
+
return
|
|
333
|
+
try:
|
|
334
|
+
modules = await self.module_list()
|
|
335
|
+
self._module_info = defaultdict(lambda: version.Version("0"))
|
|
336
|
+
for module in modules:
|
|
337
|
+
mod = EncodingInsensitiveDict(module)
|
|
338
|
+
name = nativestr(mod["name"])
|
|
339
|
+
ver = mod["ver"]
|
|
340
|
+
ver, patch = divmod(ver, 100)
|
|
341
|
+
ver, minor = divmod(ver, 100)
|
|
342
|
+
ver, major = divmod(ver, 100)
|
|
343
|
+
self._module_info[name] = version.Version(f"{major}.{minor}.{patch}")
|
|
344
|
+
except (UnknownCommandError, AuthenticationError, AuthorizationError):
|
|
345
|
+
self._module_info = {}
|
|
346
|
+
except ResponseError as err:
|
|
347
|
+
warnings.warn(
|
|
348
|
+
"Unable to determine module support due to response error from "
|
|
349
|
+
f"`MODULE LIST`: {err}."
|
|
350
|
+
)
|
|
351
|
+
self._module_info = {}
|
|
352
|
+
|
|
353
|
+
async def initialize(self: ClientT) -> ClientT:
|
|
354
|
+
await self.connection_pool.initialize()
|
|
355
|
+
await self._populate_module_versions()
|
|
356
|
+
return self
|
|
357
|
+
|
|
358
|
+
def __await__(self: ClientT) -> Generator[Any, None, ClientT]:
|
|
359
|
+
return self.initialize().__await__()
|
|
360
|
+
|
|
361
|
+
def __repr__(self) -> str:
|
|
362
|
+
return f"{type(self).__name__}<{repr(self.connection_pool)}>"
|
|
363
|
+
|
|
364
|
+
async def scan_iter(
|
|
365
|
+
self,
|
|
366
|
+
match: StringT | None = None,
|
|
367
|
+
count: int | None = None,
|
|
368
|
+
type_: StringT | None = None,
|
|
369
|
+
) -> AsyncIterator[AnyStr]:
|
|
370
|
+
"""
|
|
371
|
+
Make an iterator using the SCAN command so that the client doesn't
|
|
372
|
+
need to remember the cursor position.
|
|
373
|
+
"""
|
|
374
|
+
cursor = None
|
|
375
|
+
|
|
376
|
+
while cursor != 0:
|
|
377
|
+
cursor, data = await self.scan(cursor=cursor, match=match, count=count, type_=type_)
|
|
378
|
+
|
|
379
|
+
for item in data:
|
|
380
|
+
yield item
|
|
381
|
+
|
|
382
|
+
async def sscan_iter(
|
|
383
|
+
self,
|
|
384
|
+
key: KeyT,
|
|
385
|
+
match: StringT | None = None,
|
|
386
|
+
count: int | None = None,
|
|
387
|
+
) -> AsyncIterator[AnyStr]:
|
|
388
|
+
"""
|
|
389
|
+
Make an iterator using the SSCAN command so that the client doesn't
|
|
390
|
+
need to remember the cursor position.
|
|
391
|
+
"""
|
|
392
|
+
cursor = None
|
|
393
|
+
|
|
394
|
+
while cursor != 0:
|
|
395
|
+
cursor, data = await self.sscan(key, cursor=cursor, match=match, count=count)
|
|
396
|
+
|
|
397
|
+
for item in data:
|
|
398
|
+
yield item
|
|
399
|
+
|
|
400
|
+
@overload
|
|
401
|
+
def hscan_iter(
|
|
402
|
+
self,
|
|
403
|
+
key: KeyT,
|
|
404
|
+
match: StringT | None = ...,
|
|
405
|
+
count: int | None = ...,
|
|
406
|
+
) -> AsyncGenerator[tuple[AnyStr, AnyStr], None]: ...
|
|
407
|
+
@overload
|
|
408
|
+
def hscan_iter(
|
|
409
|
+
self,
|
|
410
|
+
key: KeyT,
|
|
411
|
+
match: StringT | None = ...,
|
|
412
|
+
count: int | None = ...,
|
|
413
|
+
*,
|
|
414
|
+
novalues: Literal[True],
|
|
415
|
+
) -> AsyncGenerator[AnyStr, None]: ...
|
|
416
|
+
|
|
417
|
+
async def hscan_iter(
|
|
418
|
+
self,
|
|
419
|
+
key: KeyT,
|
|
420
|
+
match: StringT | None = None,
|
|
421
|
+
count: int | None = None,
|
|
422
|
+
novalues: Literal[True] | None = None,
|
|
423
|
+
) -> AsyncGenerator[tuple[AnyStr, AnyStr], None] | AsyncGenerator[AnyStr, None]:
|
|
424
|
+
"""
|
|
425
|
+
Make an iterator using the HSCAN command so that the client doesn't
|
|
426
|
+
need to remember the cursor position.
|
|
427
|
+
"""
|
|
428
|
+
cursor: int | None = None
|
|
429
|
+
while cursor != 0:
|
|
430
|
+
# TODO: find a better way to narrow the return type from hscan
|
|
431
|
+
if novalues:
|
|
432
|
+
cursor, fields = await self.hscan(
|
|
433
|
+
key, cursor=cursor, match=match, count=count, novalues=novalues
|
|
434
|
+
)
|
|
435
|
+
for item in fields:
|
|
436
|
+
yield item
|
|
437
|
+
else:
|
|
438
|
+
cursor, data = await self.hscan(key, cursor=cursor, match=match, count=count)
|
|
439
|
+
|
|
440
|
+
for pair in data.items():
|
|
441
|
+
yield pair
|
|
442
|
+
|
|
443
|
+
async def zscan_iter(
|
|
444
|
+
self,
|
|
445
|
+
key: KeyT,
|
|
446
|
+
match: StringT | None = None,
|
|
447
|
+
count: int | None = None,
|
|
448
|
+
) -> AsyncIterator[ScoredMember]:
|
|
449
|
+
"""
|
|
450
|
+
Make an iterator using the ZSCAN command so that the client doesn't
|
|
451
|
+
need to remember the cursor position.
|
|
452
|
+
"""
|
|
453
|
+
cursor = None
|
|
454
|
+
|
|
455
|
+
while cursor != 0:
|
|
456
|
+
cursor, data = await self.zscan(
|
|
457
|
+
key,
|
|
458
|
+
cursor=cursor,
|
|
459
|
+
match=match,
|
|
460
|
+
count=count,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
for item in data:
|
|
464
|
+
yield item
|
|
465
|
+
|
|
466
|
+
def register_script(self, script: RedisValueT) -> Script[AnyStr]:
|
|
467
|
+
"""
|
|
468
|
+
Registers a Lua :paramref:`script`
|
|
469
|
+
|
|
470
|
+
:return: A :class:`coredis.commands.script.Script` instance that is
|
|
471
|
+
callable and hides the complexity of dealing with scripts, keys, and
|
|
472
|
+
shas.
|
|
473
|
+
"""
|
|
474
|
+
return Script[AnyStr](self, script) # type: ignore
|
|
475
|
+
|
|
476
|
+
@versionadded(version="3.1.0")
|
|
477
|
+
async def register_library(
|
|
478
|
+
self, name: StringT, code: StringT, replace: bool = False
|
|
479
|
+
) -> Library[AnyStr]:
|
|
480
|
+
"""
|
|
481
|
+
Register a new library
|
|
482
|
+
|
|
483
|
+
:param name: name of the library
|
|
484
|
+
:param code: raw code for the library
|
|
485
|
+
:param replace: Whether to replace the library when intializing. If ``False``
|
|
486
|
+
an exception will be raised if the library was already loaded in the target
|
|
487
|
+
redis instance.
|
|
488
|
+
"""
|
|
489
|
+
return await Library[AnyStr](self, name=name, code=code, replace=replace)
|
|
490
|
+
|
|
491
|
+
@versionadded(version="3.1.0")
|
|
492
|
+
async def get_library(self, name: StringT) -> Library[AnyStr]:
|
|
493
|
+
"""
|
|
494
|
+
Fetch a pre registered library
|
|
495
|
+
|
|
496
|
+
:param name: name of the library
|
|
497
|
+
"""
|
|
498
|
+
return await Library[AnyStr](self, name)
|
|
499
|
+
|
|
500
|
+
@contextlib.contextmanager
|
|
501
|
+
def ignore_replies(self: ClientT) -> Iterator[ClientT]:
|
|
502
|
+
"""
|
|
503
|
+
Context manager to run commands without waiting for a reply.
|
|
504
|
+
|
|
505
|
+
Example::
|
|
506
|
+
|
|
507
|
+
client = coredis.Redis()
|
|
508
|
+
with client.ignore_replies():
|
|
509
|
+
assert None == await client.set("fubar", 1), "noreply"
|
|
510
|
+
assert True == await client.set("fubar", 1), "reply"
|
|
511
|
+
"""
|
|
512
|
+
self._noreplycontext.set(True)
|
|
513
|
+
try:
|
|
514
|
+
yield self
|
|
515
|
+
finally:
|
|
516
|
+
self._noreplycontext.set(None)
|
|
517
|
+
|
|
518
|
+
@contextlib.contextmanager
|
|
519
|
+
def ensure_replication(
|
|
520
|
+
self: ClientT, replicas: int = 1, timeout_ms: int = 100
|
|
521
|
+
) -> Iterator[ClientT]:
|
|
522
|
+
"""
|
|
523
|
+
Context manager to ensure that commands executed within the context
|
|
524
|
+
are replicated to atleast :paramref:`replicas` within
|
|
525
|
+
:paramref:`timeout_ms` milliseconds.
|
|
526
|
+
|
|
527
|
+
Internally this uses `WAIT <https://redis.io/commands/wait>`_ after
|
|
528
|
+
each command executed within the context
|
|
529
|
+
|
|
530
|
+
:raises: :exc:`coredis.exceptions.ReplicationError`
|
|
531
|
+
|
|
532
|
+
Example::
|
|
533
|
+
|
|
534
|
+
client = coredis.RedisCluster("localhost", 7000)
|
|
535
|
+
with client.ensure_replication(1, 20):
|
|
536
|
+
await client.set("fubar", 1)
|
|
537
|
+
|
|
538
|
+
"""
|
|
539
|
+
self._waitcontext.set((replicas, timeout_ms))
|
|
540
|
+
try:
|
|
541
|
+
yield self
|
|
542
|
+
finally:
|
|
543
|
+
self._waitcontext.set(None)
|
|
544
|
+
|
|
545
|
+
@versionadded(version="4.12.0")
|
|
546
|
+
@contextlib.contextmanager
|
|
547
|
+
def ensure_persistence(
|
|
548
|
+
self: ClientT,
|
|
549
|
+
local: Literal[0, 1] = 0,
|
|
550
|
+
replicas: int = 0,
|
|
551
|
+
timeout_ms: int = 100,
|
|
552
|
+
) -> Iterator[ClientT]:
|
|
553
|
+
"""
|
|
554
|
+
Context manager to ensure that commands executed within the context
|
|
555
|
+
are synced to the AOF of a :paramref:`local` host and/or :paramref:`replicas`
|
|
556
|
+
within :paramref:`timeout_ms` milliseconds.
|
|
557
|
+
|
|
558
|
+
Internally this uses `WAITAOF <https://redis.io/commands/waitaof>`_ after
|
|
559
|
+
each command executed within the context
|
|
560
|
+
|
|
561
|
+
:raises: :exc:`coredis.exceptions.PersistenceError`
|
|
562
|
+
|
|
563
|
+
Example for standalone client::
|
|
564
|
+
|
|
565
|
+
client = coredis.Redis()
|
|
566
|
+
with client.ensure_persistence(1, 0, 20):
|
|
567
|
+
await client.set("fubar", 1)
|
|
568
|
+
|
|
569
|
+
Example for cluster::
|
|
570
|
+
|
|
571
|
+
client = coredis.RedisCluster("localhost", 7000)
|
|
572
|
+
with client.ensure_persistence(1, 1, 20):
|
|
573
|
+
await client.set("fubar", 1)
|
|
574
|
+
|
|
575
|
+
"""
|
|
576
|
+
self._waitaof_context.set((local, replicas, timeout_ms))
|
|
577
|
+
try:
|
|
578
|
+
yield self
|
|
579
|
+
finally:
|
|
580
|
+
self._waitaof_context.set(None)
|
|
581
|
+
|
|
582
|
+
def should_quick_release(self, command: RedisCommandP) -> bool:
|
|
583
|
+
return CommandFlag.BLOCKING not in COMMAND_FLAGS[command.name]
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class Redis(Client[AnyStr]):
|
|
587
|
+
connection_pool: ConnectionPool
|
|
588
|
+
|
|
589
|
+
@overload
|
|
590
|
+
def __init__(
|
|
591
|
+
self: Redis[bytes],
|
|
592
|
+
host: str | None = ...,
|
|
593
|
+
port: int | None = ...,
|
|
594
|
+
db: int = ...,
|
|
595
|
+
*,
|
|
596
|
+
username: str | None = ...,
|
|
597
|
+
password: str | None = ...,
|
|
598
|
+
credential_provider: AbstractCredentialProvider | None = ...,
|
|
599
|
+
stream_timeout: float | None = ...,
|
|
600
|
+
connect_timeout: float | None = ...,
|
|
601
|
+
connection_pool: ConnectionPool | None = ...,
|
|
602
|
+
connection_pool_cls: type[ConnectionPool] = ...,
|
|
603
|
+
unix_socket_path: str | None = ...,
|
|
604
|
+
encoding: str = ...,
|
|
605
|
+
decode_responses: Literal[False] = ...,
|
|
606
|
+
ssl: bool = ...,
|
|
607
|
+
ssl_context: SSLContext | None = ...,
|
|
608
|
+
ssl_keyfile: str | None = ...,
|
|
609
|
+
ssl_certfile: str | None = ...,
|
|
610
|
+
ssl_cert_reqs: Literal["optional", "required", "none"] | None = ...,
|
|
611
|
+
ssl_check_hostname: bool | None = ...,
|
|
612
|
+
ssl_ca_certs: str | None = ...,
|
|
613
|
+
max_connections: int | None = ...,
|
|
614
|
+
max_idle_time: float = ...,
|
|
615
|
+
idle_check_interval: float = ...,
|
|
616
|
+
client_name: str | None = ...,
|
|
617
|
+
protocol_version: Literal[2, 3] = ...,
|
|
618
|
+
verify_version: bool = ...,
|
|
619
|
+
cache: AbstractCache | None = ...,
|
|
620
|
+
noreply: bool = ...,
|
|
621
|
+
noevict: bool = ...,
|
|
622
|
+
notouch: bool = ...,
|
|
623
|
+
retry_policy: RetryPolicy = ...,
|
|
624
|
+
type_adapter: TypeAdapter | None = ...,
|
|
625
|
+
**kwargs: Any,
|
|
626
|
+
) -> None: ...
|
|
627
|
+
|
|
628
|
+
@overload
|
|
629
|
+
def __init__(
|
|
630
|
+
self: Redis[str],
|
|
631
|
+
host: str | None = ...,
|
|
632
|
+
port: int | None = ...,
|
|
633
|
+
db: int = ...,
|
|
634
|
+
*,
|
|
635
|
+
username: str | None = ...,
|
|
636
|
+
password: str | None = ...,
|
|
637
|
+
credential_provider: AbstractCredentialProvider | None = ...,
|
|
638
|
+
stream_timeout: float | None = ...,
|
|
639
|
+
connect_timeout: float | None = ...,
|
|
640
|
+
connection_pool: ConnectionPool | None = ...,
|
|
641
|
+
connection_pool_cls: type[ConnectionPool] = ...,
|
|
642
|
+
unix_socket_path: str | None = ...,
|
|
643
|
+
encoding: str = ...,
|
|
644
|
+
decode_responses: Literal[True] = ...,
|
|
645
|
+
ssl: bool = ...,
|
|
646
|
+
ssl_context: SSLContext | None = ...,
|
|
647
|
+
ssl_keyfile: str | None = ...,
|
|
648
|
+
ssl_certfile: str | None = ...,
|
|
649
|
+
ssl_cert_reqs: Literal["optional", "required", "none"] | None = ...,
|
|
650
|
+
ssl_check_hostname: bool | None = ...,
|
|
651
|
+
ssl_ca_certs: str | None = ...,
|
|
652
|
+
max_connections: int | None = ...,
|
|
653
|
+
max_idle_time: float = ...,
|
|
654
|
+
idle_check_interval: float = ...,
|
|
655
|
+
client_name: str | None = ...,
|
|
656
|
+
protocol_version: Literal[2, 3] = ...,
|
|
657
|
+
verify_version: bool = ...,
|
|
658
|
+
cache: AbstractCache | None = ...,
|
|
659
|
+
noreply: bool = ...,
|
|
660
|
+
noevict: bool = ...,
|
|
661
|
+
notouch: bool = ...,
|
|
662
|
+
retry_policy: RetryPolicy = ...,
|
|
663
|
+
type_adapter: TypeAdapter | None = ...,
|
|
664
|
+
**kwargs: Any,
|
|
665
|
+
) -> None: ...
|
|
666
|
+
|
|
667
|
+
def __init__(
|
|
668
|
+
self,
|
|
669
|
+
host: str | None = "localhost",
|
|
670
|
+
port: int | None = 6379,
|
|
671
|
+
db: int = 0,
|
|
672
|
+
*,
|
|
673
|
+
username: str | None = None,
|
|
674
|
+
password: str | None = None,
|
|
675
|
+
credential_provider: AbstractCredentialProvider | None = None,
|
|
676
|
+
stream_timeout: float | None = None,
|
|
677
|
+
connect_timeout: float | None = None,
|
|
678
|
+
connection_pool: ConnectionPool | None = None,
|
|
679
|
+
connection_pool_cls: type[ConnectionPool] = ConnectionPool,
|
|
680
|
+
unix_socket_path: str | None = None,
|
|
681
|
+
encoding: str = "utf-8",
|
|
682
|
+
decode_responses: bool = False,
|
|
683
|
+
ssl: bool = False,
|
|
684
|
+
ssl_context: SSLContext | None = None,
|
|
685
|
+
ssl_keyfile: str | None = None,
|
|
686
|
+
ssl_certfile: str | None = None,
|
|
687
|
+
ssl_cert_reqs: Literal["optional", "required", "none"] | None = None,
|
|
688
|
+
ssl_check_hostname: bool | None = None,
|
|
689
|
+
ssl_ca_certs: str | None = None,
|
|
690
|
+
max_connections: int | None = None,
|
|
691
|
+
max_idle_time: float = 0,
|
|
692
|
+
idle_check_interval: float = 1,
|
|
693
|
+
client_name: str | None = None,
|
|
694
|
+
protocol_version: Literal[2, 3] = 3,
|
|
695
|
+
verify_version: bool = True,
|
|
696
|
+
cache: AbstractCache | None = None,
|
|
697
|
+
noreply: bool = False,
|
|
698
|
+
noevict: bool = False,
|
|
699
|
+
notouch: bool = False,
|
|
700
|
+
retry_policy: RetryPolicy = ConstantRetryPolicy((ConnectionError, TimeoutError), 2, 0.01),
|
|
701
|
+
type_adapter: TypeAdapter | None = None,
|
|
702
|
+
**kwargs: Any,
|
|
703
|
+
) -> None:
|
|
704
|
+
"""
|
|
705
|
+
|
|
706
|
+
Changes
|
|
707
|
+
- .. versionadded:: 4.12.0
|
|
708
|
+
|
|
709
|
+
- :paramref:`retry_policy`
|
|
710
|
+
- :paramref:`noevict`
|
|
711
|
+
- :paramref:`notouch`
|
|
712
|
+
- :meth:`Redis.ensure_persistence` context manager
|
|
713
|
+
- Redis Module support
|
|
714
|
+
|
|
715
|
+
- RedisJSON: :attr:`Redis.json`
|
|
716
|
+
- RedisBloom:
|
|
717
|
+
|
|
718
|
+
- BloomFilter: :attr:`Redis.bf`
|
|
719
|
+
- CuckooFilter: :attr:`Redis.cf`
|
|
720
|
+
- CountMinSketch: :attr:`Redis.cms`
|
|
721
|
+
- TopK: :attr:`Redis.topk`
|
|
722
|
+
- TDigest: :attr:`Redis.tdigest`
|
|
723
|
+
- RedisTimeSeries: :attr:`Redis.timeseries`
|
|
724
|
+
- RedisGraph: :attr:`Redis.graph`
|
|
725
|
+
- RediSearch:
|
|
726
|
+
|
|
727
|
+
- Search & Aggregation: :attr:`Redis.search`
|
|
728
|
+
- Autocomplete: Added :attr:`Redis.autocomplete`
|
|
729
|
+
|
|
730
|
+
- .. versionchanged:: 4.12.0
|
|
731
|
+
|
|
732
|
+
- Removed :paramref:`retry_on_timeout` constructor argument. Use
|
|
733
|
+
:paramref:`retry_policy` instead.
|
|
734
|
+
|
|
735
|
+
- .. versionadded:: 4.3.0
|
|
736
|
+
|
|
737
|
+
- Added :paramref:`connection_pool_cls`
|
|
738
|
+
|
|
739
|
+
- .. versionchanged:: 4.0.0
|
|
740
|
+
|
|
741
|
+
- :paramref:`non_atomic_cross_slot` defaults to ``True``
|
|
742
|
+
- :paramref:`protocol_version`` defaults to ``3``
|
|
743
|
+
|
|
744
|
+
- .. versionadded:: 3.11.0
|
|
745
|
+
|
|
746
|
+
- Added :paramref:`noreply`
|
|
747
|
+
|
|
748
|
+
- .. versionadded:: 3.9.0
|
|
749
|
+
|
|
750
|
+
- If :paramref:`cache` is provided the client will check & populate
|
|
751
|
+
the cache for read only commands and invalidate it for commands
|
|
752
|
+
that could change the key(s) in the request.
|
|
753
|
+
|
|
754
|
+
- .. versionchanged:: 3.5.0
|
|
755
|
+
|
|
756
|
+
- The :paramref:`verify_version` parameter now defaults to ``True``
|
|
757
|
+
|
|
758
|
+
- .. versionadded:: 3.1.0
|
|
759
|
+
|
|
760
|
+
- The :paramref:`protocol_version` and :paramref:`verify_version`
|
|
761
|
+
:parameters were added
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
:param host: The hostname of the redis server
|
|
765
|
+
:param port: The port at which th redis server is listening on
|
|
766
|
+
:param db: database number to switch to upon connection
|
|
767
|
+
:param username: Username for authenticating with the redis server
|
|
768
|
+
:param password: Password for authenticating with the redis server
|
|
769
|
+
:param credential_provider: CredentialProvider to get authentication credentials
|
|
770
|
+
:param stream_timeout: Timeout (seconds) when reading responses from the server
|
|
771
|
+
:param connect_timeout: Timeout (seconds) for establishing a connection to the server
|
|
772
|
+
:param connection_pool: The connection pool instance to use. If not provided
|
|
773
|
+
a new pool will be assigned to this client.
|
|
774
|
+
:param connection_pool_cls: The connection pool class to use when constructing
|
|
775
|
+
a connection pool for this instance.
|
|
776
|
+
:param unix_socket_path: Path to the UDS which the redis server
|
|
777
|
+
is listening at
|
|
778
|
+
:param encoding: The codec to use to encode strings transmitted to redis
|
|
779
|
+
and decode responses with. (See :ref:`handbook/encoding:encoding/decoding`)
|
|
780
|
+
:param decode_responses: If ``True`` string responses from the server
|
|
781
|
+
will be decoded using :paramref:`encoding` before being returned.
|
|
782
|
+
(See :ref:`handbook/encoding:encoding/decoding`)
|
|
783
|
+
:param ssl: Whether to use an SSL connection
|
|
784
|
+
:param ssl_context: If provided the :class:`ssl.SSLContext` will be used when
|
|
785
|
+
establishing the connection. Otherwise either the default context (if no other
|
|
786
|
+
ssl related parameters are provided) or a custom context based on the other
|
|
787
|
+
``ssl_*`` parameters will be used.
|
|
788
|
+
:param ssl_keyfile: Path of the private key to use
|
|
789
|
+
:param ssl_certfile: Path to the certificate corresponding to :paramref:`ssl_keyfile`
|
|
790
|
+
:param ssl_cert_reqs: Whether to try to verify the server's certificates and
|
|
791
|
+
how to behave if verification fails (See :attr:`ssl.SSLContext.verify_mode`).
|
|
792
|
+
:param ssl_check_hostname: Whether to enable hostname checking when establishing
|
|
793
|
+
an ssl connection.
|
|
794
|
+
:param ssl_ca_certs: Path to a concatenated certificate authority file or a directory
|
|
795
|
+
containing several CA certifcates to use for validating the server's certificates
|
|
796
|
+
when :paramref:`ssl_cert_reqs` is not ``"none"``
|
|
797
|
+
(See :meth:`ssl.SSLContext.load_verify_locations`).
|
|
798
|
+
:param max_connections: Maximum capacity of the connection pool (Ignored if
|
|
799
|
+
:paramref:`connection_pool` is not ``None``.
|
|
800
|
+
:param max_idle_time: Maximum number of a seconds an unused connection is cached
|
|
801
|
+
before it is disconnected.
|
|
802
|
+
:param idle_check_interval: Periodicity of idle checks (seconds) to release idle
|
|
803
|
+
connections.
|
|
804
|
+
:param client_name: The client name to identifiy with the redis server
|
|
805
|
+
:param protocol_version: Whether to use the RESP (``2``) or RESP3 (``3``)
|
|
806
|
+
protocol for parsing responses from the server (Default ``3``).
|
|
807
|
+
(See :ref:`handbook/response:redis response`)
|
|
808
|
+
:param verify_version: Validate redis server version against the documented
|
|
809
|
+
version introduced before executing a command and raises a
|
|
810
|
+
:exc:`CommandNotSupportedError` error if the required version is higher than
|
|
811
|
+
the reported server version
|
|
812
|
+
:param cache: If provided the cache will be used to avoid requests for read only
|
|
813
|
+
commands if the client has already requested the data and it hasn't been invalidated.
|
|
814
|
+
The cache is responsible for any mutations to the keys that happen outside of this client
|
|
815
|
+
:param noreply: If ``True`` the client will not request a response for any
|
|
816
|
+
commands sent to the server.
|
|
817
|
+
:param noevict: Ensures that connections from the client will be excluded from the
|
|
818
|
+
client eviction process even if we're above the configured client eviction threshold.
|
|
819
|
+
:param notouch: Ensures that commands sent by the client will not alter the LRU/LFU of
|
|
820
|
+
the keys they access.
|
|
821
|
+
:param retry_policy: The retry policy to use when interacting with the redis server
|
|
822
|
+
:param type_adapter: The adapter to use for serializing / deserializing customs types
|
|
823
|
+
when interacting with redis commands.
|
|
824
|
+
|
|
825
|
+
"""
|
|
826
|
+
super().__init__(
|
|
827
|
+
host=host,
|
|
828
|
+
port=port,
|
|
829
|
+
db=db,
|
|
830
|
+
username=username,
|
|
831
|
+
password=password,
|
|
832
|
+
credential_provider=credential_provider,
|
|
833
|
+
stream_timeout=stream_timeout,
|
|
834
|
+
connect_timeout=connect_timeout,
|
|
835
|
+
connection_pool=connection_pool,
|
|
836
|
+
connection_pool_cls=connection_pool_cls,
|
|
837
|
+
unix_socket_path=unix_socket_path,
|
|
838
|
+
encoding=encoding,
|
|
839
|
+
decode_responses=decode_responses,
|
|
840
|
+
ssl=ssl,
|
|
841
|
+
ssl_context=ssl_context,
|
|
842
|
+
ssl_keyfile=ssl_keyfile,
|
|
843
|
+
ssl_certfile=ssl_certfile,
|
|
844
|
+
ssl_cert_reqs=ssl_cert_reqs,
|
|
845
|
+
ssl_check_hostname=ssl_check_hostname,
|
|
846
|
+
ssl_ca_certs=ssl_ca_certs,
|
|
847
|
+
max_connections=max_connections,
|
|
848
|
+
max_idle_time=max_idle_time,
|
|
849
|
+
idle_check_interval=idle_check_interval,
|
|
850
|
+
client_name=client_name,
|
|
851
|
+
protocol_version=protocol_version,
|
|
852
|
+
verify_version=verify_version,
|
|
853
|
+
noreply=noreply,
|
|
854
|
+
noevict=noevict,
|
|
855
|
+
notouch=notouch,
|
|
856
|
+
retry_policy=retry_policy,
|
|
857
|
+
type_adapter=type_adapter,
|
|
858
|
+
**kwargs,
|
|
859
|
+
)
|
|
860
|
+
self.cache = cache
|
|
861
|
+
self._decodecontext: contextvars.ContextVar[bool | None,] = contextvars.ContextVar(
|
|
862
|
+
"decode", default=None
|
|
863
|
+
)
|
|
864
|
+
self._encodingcontext: contextvars.ContextVar[str | None,] = contextvars.ContextVar(
|
|
865
|
+
"decode", default=None
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
@classmethod
|
|
869
|
+
@overload
|
|
870
|
+
def from_url(
|
|
871
|
+
cls,
|
|
872
|
+
url: str,
|
|
873
|
+
db: int | None = ...,
|
|
874
|
+
*,
|
|
875
|
+
decode_responses: Literal[False] = ...,
|
|
876
|
+
protocol_version: Literal[2, 3] = ...,
|
|
877
|
+
verify_version: bool = ...,
|
|
878
|
+
noreply: bool = ...,
|
|
879
|
+
noevict: bool = ...,
|
|
880
|
+
notouch: bool = ...,
|
|
881
|
+
retry_policy: RetryPolicy = ...,
|
|
882
|
+
cache: AbstractCache | None = ...,
|
|
883
|
+
**kwargs: Any,
|
|
884
|
+
) -> Redis[bytes]: ...
|
|
885
|
+
|
|
886
|
+
@classmethod
|
|
887
|
+
@overload
|
|
888
|
+
def from_url(
|
|
889
|
+
cls,
|
|
890
|
+
url: str,
|
|
891
|
+
db: int | None = ...,
|
|
892
|
+
*,
|
|
893
|
+
decode_responses: Literal[True] = ...,
|
|
894
|
+
protocol_version: Literal[2, 3] = ...,
|
|
895
|
+
verify_version: bool = ...,
|
|
896
|
+
noreply: bool = ...,
|
|
897
|
+
noevict: bool = ...,
|
|
898
|
+
notouch: bool = ...,
|
|
899
|
+
retry_policy: RetryPolicy = ...,
|
|
900
|
+
cache: AbstractCache | None = ...,
|
|
901
|
+
**kwargs: Any,
|
|
902
|
+
) -> Redis[str]: ...
|
|
903
|
+
|
|
904
|
+
@classmethod
|
|
905
|
+
def from_url(
|
|
906
|
+
cls: type[RedisT],
|
|
907
|
+
url: str,
|
|
908
|
+
db: int | None = None,
|
|
909
|
+
*,
|
|
910
|
+
decode_responses: bool = False,
|
|
911
|
+
protocol_version: Literal[2, 3] = 3,
|
|
912
|
+
verify_version: bool = True,
|
|
913
|
+
noreply: bool = False,
|
|
914
|
+
noevict: bool = False,
|
|
915
|
+
notouch: bool = False,
|
|
916
|
+
retry_policy: RetryPolicy = ConstantRetryPolicy((ConnectionError, TimeoutError), 2, 0.01),
|
|
917
|
+
type_adapter: TypeAdapter | None = None,
|
|
918
|
+
cache: AbstractCache | None = None,
|
|
919
|
+
**kwargs: Any,
|
|
920
|
+
) -> RedisT:
|
|
921
|
+
"""
|
|
922
|
+
Return a Redis client object configured from the given URL, which must
|
|
923
|
+
use either the `redis:// scheme
|
|
924
|
+
<http://www.iana.org/assignments/uri-schemes/prov/redis>`_ for RESP
|
|
925
|
+
connections or the ``unix://`` scheme for Unix domain sockets.
|
|
926
|
+
|
|
927
|
+
For example:
|
|
928
|
+
|
|
929
|
+
- ``redis://[:password]@localhost:6379/0``
|
|
930
|
+
- ``rediss://[:password]@localhost:6379/0``
|
|
931
|
+
- ``unix://[:password]@/path/to/socket.sock?db=0``
|
|
932
|
+
|
|
933
|
+
:paramref:`url` and :paramref:`kwargs` are passed as is to
|
|
934
|
+
the :func:`coredis.ConnectionPool.from_url`.
|
|
935
|
+
"""
|
|
936
|
+
if decode_responses:
|
|
937
|
+
return cls(
|
|
938
|
+
decode_responses=True,
|
|
939
|
+
protocol_version=protocol_version,
|
|
940
|
+
verify_version=verify_version,
|
|
941
|
+
noreply=noreply,
|
|
942
|
+
retry_policy=retry_policy,
|
|
943
|
+
type_adapter=type_adapter,
|
|
944
|
+
cache=cache,
|
|
945
|
+
connection_pool=ConnectionPool.from_url(
|
|
946
|
+
url,
|
|
947
|
+
db=db,
|
|
948
|
+
decode_responses=decode_responses,
|
|
949
|
+
protocol_version=protocol_version,
|
|
950
|
+
noreply=noreply,
|
|
951
|
+
noevict=noevict,
|
|
952
|
+
notouch=notouch,
|
|
953
|
+
**kwargs,
|
|
954
|
+
),
|
|
955
|
+
)
|
|
956
|
+
else:
|
|
957
|
+
return cls(
|
|
958
|
+
decode_responses=False,
|
|
959
|
+
protocol_version=protocol_version,
|
|
960
|
+
verify_version=verify_version,
|
|
961
|
+
noreply=noreply,
|
|
962
|
+
retry_policy=retry_policy,
|
|
963
|
+
type_adapter=type_adapter,
|
|
964
|
+
cache=cache,
|
|
965
|
+
connection_pool=ConnectionPool.from_url(
|
|
966
|
+
url,
|
|
967
|
+
db=db,
|
|
968
|
+
decode_responses=decode_responses,
|
|
969
|
+
protocol_version=protocol_version,
|
|
970
|
+
noreply=noreply,
|
|
971
|
+
noevict=noevict,
|
|
972
|
+
notouch=notouch,
|
|
973
|
+
**kwargs,
|
|
974
|
+
),
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
async def initialize(self) -> Redis[AnyStr]:
|
|
978
|
+
if not self.connection_pool.initialized:
|
|
979
|
+
await super().initialize()
|
|
980
|
+
if self.cache:
|
|
981
|
+
self.cache = await self.cache.initialize(self)
|
|
982
|
+
return self
|
|
983
|
+
|
|
984
|
+
async def execute_command(
|
|
985
|
+
self,
|
|
986
|
+
command: RedisCommandP,
|
|
987
|
+
callback: Callable[..., R] = NoopCallback(),
|
|
988
|
+
**options: Unpack[ExecutionParameters],
|
|
989
|
+
) -> R:
|
|
990
|
+
"""
|
|
991
|
+
Executes a command with configured retries and returns
|
|
992
|
+
the parsed response
|
|
993
|
+
"""
|
|
994
|
+
return await self.retry_policy.call_with_retries(
|
|
995
|
+
lambda: self._execute_command(command, callback=callback, **options),
|
|
996
|
+
before_hook=self.initialize,
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
async def _execute_command(
|
|
1000
|
+
self,
|
|
1001
|
+
command: RedisCommandP,
|
|
1002
|
+
callback: Callable[..., R] = NoopCallback(),
|
|
1003
|
+
**options: Unpack[ExecutionParameters],
|
|
1004
|
+
) -> R:
|
|
1005
|
+
pool = self.connection_pool
|
|
1006
|
+
quick_release = self.should_quick_release(command)
|
|
1007
|
+
connection = await pool.get_connection(
|
|
1008
|
+
command.name,
|
|
1009
|
+
*command.arguments,
|
|
1010
|
+
acquire=not quick_release or self.requires_wait or self.requires_waitaof,
|
|
1011
|
+
)
|
|
1012
|
+
try:
|
|
1013
|
+
keys = KeySpec.extract_keys(command.name, *command.arguments)
|
|
1014
|
+
cacheable = (
|
|
1015
|
+
command.name in CACHEABLE_COMMANDS
|
|
1016
|
+
and len(keys) == 1
|
|
1017
|
+
and not self.noreply
|
|
1018
|
+
and self._decodecontext.get() is None
|
|
1019
|
+
)
|
|
1020
|
+
cached_reply = None
|
|
1021
|
+
cache_hit = False
|
|
1022
|
+
use_cached = False
|
|
1023
|
+
reply = None
|
|
1024
|
+
if self.cache:
|
|
1025
|
+
if connection.tracking_client_id != self.cache.get_client_id(connection):
|
|
1026
|
+
self.cache.reset()
|
|
1027
|
+
await connection.update_tracking_client(
|
|
1028
|
+
True, self.cache.get_client_id(connection)
|
|
1029
|
+
)
|
|
1030
|
+
if command.name not in READONLY_COMMANDS:
|
|
1031
|
+
self.cache.invalidate(*keys)
|
|
1032
|
+
elif cacheable:
|
|
1033
|
+
try:
|
|
1034
|
+
cached_reply = cast(
|
|
1035
|
+
R,
|
|
1036
|
+
self.cache.get(
|
|
1037
|
+
command.name,
|
|
1038
|
+
keys[0],
|
|
1039
|
+
*command.arguments,
|
|
1040
|
+
),
|
|
1041
|
+
)
|
|
1042
|
+
use_cached = random.random() * 100.0 < min(100.0, self.cache.confidence)
|
|
1043
|
+
cache_hit = True
|
|
1044
|
+
except KeyError:
|
|
1045
|
+
pass
|
|
1046
|
+
if not (use_cached and cached_reply):
|
|
1047
|
+
request = await connection.create_request(
|
|
1048
|
+
command.name,
|
|
1049
|
+
*command.arguments,
|
|
1050
|
+
noreply=self.noreply,
|
|
1051
|
+
decode=options.get("decode", self._decodecontext.get()),
|
|
1052
|
+
encoding=self._encodingcontext.get(),
|
|
1053
|
+
)
|
|
1054
|
+
maybe_wait = [
|
|
1055
|
+
await self._ensure_wait(command, connection),
|
|
1056
|
+
await self._ensure_persistence(command, connection),
|
|
1057
|
+
]
|
|
1058
|
+
reply = await request
|
|
1059
|
+
await asyncio.gather(*maybe_wait)
|
|
1060
|
+
if self.noreply:
|
|
1061
|
+
return None # type: ignore
|
|
1062
|
+
if isinstance(callback, AsyncPreProcessingCallback):
|
|
1063
|
+
await callback.pre_process(self, reply)
|
|
1064
|
+
if self.cache and cacheable:
|
|
1065
|
+
if cache_hit and not use_cached:
|
|
1066
|
+
self.cache.feedback(
|
|
1067
|
+
command.name, keys[0], *command.arguments, match=cached_reply == reply
|
|
1068
|
+
)
|
|
1069
|
+
if not cache_hit:
|
|
1070
|
+
self.cache.put(
|
|
1071
|
+
command.name,
|
|
1072
|
+
keys[0],
|
|
1073
|
+
*command.arguments,
|
|
1074
|
+
value=reply,
|
|
1075
|
+
)
|
|
1076
|
+
return callback(cached_reply if cache_hit else reply, version=self.protocol_version)
|
|
1077
|
+
except RedisError:
|
|
1078
|
+
connection.disconnect()
|
|
1079
|
+
raise
|
|
1080
|
+
finally:
|
|
1081
|
+
self._ensure_server_version(connection.server_version)
|
|
1082
|
+
if not quick_release or self.requires_wait or self.requires_waitaof:
|
|
1083
|
+
pool.release(connection)
|
|
1084
|
+
|
|
1085
|
+
@overload
|
|
1086
|
+
def decoding(
|
|
1087
|
+
self, mode: Literal[False], encoding: str | None = None
|
|
1088
|
+
) -> contextlib.AbstractContextManager[Redis[bytes]]: ...
|
|
1089
|
+
|
|
1090
|
+
@overload
|
|
1091
|
+
def decoding(
|
|
1092
|
+
self, mode: Literal[True], encoding: str | None = None
|
|
1093
|
+
) -> contextlib.AbstractContextManager[Redis[str]]: ...
|
|
1094
|
+
|
|
1095
|
+
@contextlib.contextmanager
|
|
1096
|
+
@versionadded(version="4.8.0")
|
|
1097
|
+
def decoding(self, mode: bool, encoding: str | None = None) -> Iterator[Redis[Any]]:
|
|
1098
|
+
"""
|
|
1099
|
+
Context manager to temporarily change the decoding behavior
|
|
1100
|
+
of the client
|
|
1101
|
+
|
|
1102
|
+
:param mode: Whether to decode or not
|
|
1103
|
+
:param encoding: Optional encoding to use if decoding. If not provided
|
|
1104
|
+
the :paramref:`~coredis.Redis.encoding` parameter provided to the client will
|
|
1105
|
+
be used.
|
|
1106
|
+
|
|
1107
|
+
Example::
|
|
1108
|
+
|
|
1109
|
+
client = coredis.Redis(decode_responses=True)
|
|
1110
|
+
await client.set("fubar", "baz")
|
|
1111
|
+
assert await client.get("fubar") == "baz"
|
|
1112
|
+
with client.decoding(False):
|
|
1113
|
+
assert await client.get("fubar") == b"baz"
|
|
1114
|
+
with client.decoding(True):
|
|
1115
|
+
assert await client.get("fubar") == "baz"
|
|
1116
|
+
|
|
1117
|
+
"""
|
|
1118
|
+
prev_decode = self._decodecontext.get()
|
|
1119
|
+
prev_encoding = self._encodingcontext.get()
|
|
1120
|
+
self._decodecontext.set(mode)
|
|
1121
|
+
self._encodingcontext.set(encoding)
|
|
1122
|
+
try:
|
|
1123
|
+
yield self
|
|
1124
|
+
finally:
|
|
1125
|
+
self._decodecontext.set(prev_decode)
|
|
1126
|
+
self._encodingcontext.set(prev_encoding)
|
|
1127
|
+
|
|
1128
|
+
@deprecated("The implementation of a monitor will be removed in 6.0", version="5.2.0")
|
|
1129
|
+
def monitor(
|
|
1130
|
+
self,
|
|
1131
|
+
response_handler: Callable[[MonitorResult], None] | None = None,
|
|
1132
|
+
) -> Monitor[AnyStr]:
|
|
1133
|
+
"""
|
|
1134
|
+
:param response_handler: Optional callback to be triggered whenever
|
|
1135
|
+
a command is received by this monitor.
|
|
1136
|
+
|
|
1137
|
+
Return an instance of a :class:`~coredis.commands.monitor.Monitor`
|
|
1138
|
+
|
|
1139
|
+
The monitor can be used as an async iterator or individual commands
|
|
1140
|
+
can be fetched via :meth:`~coredis.commands.monitor.Monitor.get_command`.
|
|
1141
|
+
When a :paramref:`response_handler` is provided it will simply by called
|
|
1142
|
+
for every command received.
|
|
1143
|
+
|
|
1144
|
+
"""
|
|
1145
|
+
return Monitor[AnyStr](self, response_handler)
|
|
1146
|
+
|
|
1147
|
+
def pubsub(
|
|
1148
|
+
self,
|
|
1149
|
+
ignore_subscribe_messages: bool = False,
|
|
1150
|
+
retry_policy: RetryPolicy | None = None,
|
|
1151
|
+
channels: Parameters[StringT] | None = None,
|
|
1152
|
+
channel_handlers: Mapping[StringT, SubscriptionCallback] | None = None,
|
|
1153
|
+
patterns: Parameters[StringT] | None = None,
|
|
1154
|
+
pattern_handlers: Mapping[StringT, SubscriptionCallback] | None = None,
|
|
1155
|
+
**kwargs: Any,
|
|
1156
|
+
) -> PubSub[AnyStr]:
|
|
1157
|
+
"""
|
|
1158
|
+
Return a Pub/Sub instance that can be used to subscribe to channels
|
|
1159
|
+
and patterns and receive messages that get published to them.
|
|
1160
|
+
|
|
1161
|
+
:param ignore_subscribe_messages: Whether to skip subscription
|
|
1162
|
+
acknowledgement messages
|
|
1163
|
+
:param retry_policy: An explicit retry policy to use in the subscriber.
|
|
1164
|
+
:param channels: channels that the constructed Pubsub instance should
|
|
1165
|
+
automatically subscribe to
|
|
1166
|
+
:param channel_handlers: Mapping of channels to automatically subscribe to
|
|
1167
|
+
and the associated handlers that will be invoked when a message is received
|
|
1168
|
+
on the specific channel.
|
|
1169
|
+
:param patterns: patterns that the constructed Pubsub instance should
|
|
1170
|
+
automatically subscribe to
|
|
1171
|
+
:param pattern_handlers: Mapping of patterns to automatically subscribe to
|
|
1172
|
+
and the associated handlers that will be invoked when a message is received
|
|
1173
|
+
on channel matching the pattern.
|
|
1174
|
+
|
|
1175
|
+
"""
|
|
1176
|
+
|
|
1177
|
+
return PubSub[AnyStr](
|
|
1178
|
+
self.connection_pool,
|
|
1179
|
+
ignore_subscribe_messages=ignore_subscribe_messages,
|
|
1180
|
+
retry_policy=retry_policy,
|
|
1181
|
+
channels=channels,
|
|
1182
|
+
channel_handlers=channel_handlers,
|
|
1183
|
+
patterns=patterns,
|
|
1184
|
+
pattern_handlers=pattern_handlers,
|
|
1185
|
+
**kwargs,
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
async def pipeline(
|
|
1189
|
+
self,
|
|
1190
|
+
transaction: bool | None = True,
|
|
1191
|
+
watches: Parameters[KeyT] | None = None,
|
|
1192
|
+
timeout: float | None = None,
|
|
1193
|
+
) -> coredis.pipeline.Pipeline[AnyStr]:
|
|
1194
|
+
"""
|
|
1195
|
+
Returns a new pipeline object that can queue multiple commands for
|
|
1196
|
+
batch execution.
|
|
1197
|
+
|
|
1198
|
+
:param transaction: indicates whether all commands should be executed atomically.
|
|
1199
|
+
:param watches: If :paramref:`transaction` is True these keys are watched for external
|
|
1200
|
+
changes during the transaction.
|
|
1201
|
+
:param timeout: If specified this value will take precedence over
|
|
1202
|
+
:paramref:`Redis.stream_timeout`
|
|
1203
|
+
"""
|
|
1204
|
+
from coredis.pipeline import Pipeline
|
|
1205
|
+
|
|
1206
|
+
return Pipeline[AnyStr](self, transaction, watches, timeout)
|
|
1207
|
+
|
|
1208
|
+
async def transaction(
|
|
1209
|
+
self,
|
|
1210
|
+
func: Callable[[coredis.pipeline.Pipeline[AnyStr]], Coroutine[Any, Any, Any]],
|
|
1211
|
+
*watches: KeyT,
|
|
1212
|
+
value_from_callable: bool = False,
|
|
1213
|
+
watch_delay: float | None = None,
|
|
1214
|
+
**kwargs: Any,
|
|
1215
|
+
) -> Any | None:
|
|
1216
|
+
"""
|
|
1217
|
+
Convenience method for executing the callable :paramref:`func` as a
|
|
1218
|
+
transaction while watching all keys specified in :paramref:`watches`.
|
|
1219
|
+
|
|
1220
|
+
:param func: callable should expect a single argument which is a
|
|
1221
|
+
:class:`coredis.pipeline.Pipeline` object retrieved by calling
|
|
1222
|
+
:meth:`~coredis.Redis.pipeline`.
|
|
1223
|
+
:param watches: The keys to watch during the transaction
|
|
1224
|
+
:param value_from_callable: Whether to return the result of transaction or the value
|
|
1225
|
+
returned from :paramref:`func`
|
|
1226
|
+
"""
|
|
1227
|
+
async with await self.pipeline(True) as pipe:
|
|
1228
|
+
while True:
|
|
1229
|
+
try:
|
|
1230
|
+
if watches:
|
|
1231
|
+
await pipe.watch(*watches)
|
|
1232
|
+
func_value = await func(pipe)
|
|
1233
|
+
exec_value = await pipe.execute()
|
|
1234
|
+
return func_value if value_from_callable else exec_value
|
|
1235
|
+
except WatchError:
|
|
1236
|
+
if watch_delay is not None and watch_delay > 0:
|
|
1237
|
+
await asyncio.sleep(watch_delay)
|
|
1238
|
+
continue
|