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.

Files changed (100) hide show
  1. 22fe76227e35f92ab5c3__mypyc.cpython-314t-darwin.so +0 -0
  2. coredis/__init__.py +42 -0
  3. coredis/_enum.py +42 -0
  4. coredis/_json.py +11 -0
  5. coredis/_packer.cpython-314t-darwin.so +0 -0
  6. coredis/_packer.py +71 -0
  7. coredis/_protocols.py +50 -0
  8. coredis/_py_311_typing.py +20 -0
  9. coredis/_py_312_typing.py +17 -0
  10. coredis/_sidecar.py +114 -0
  11. coredis/_utils.cpython-314t-darwin.so +0 -0
  12. coredis/_utils.py +440 -0
  13. coredis/_version.py +34 -0
  14. coredis/_version.pyi +1 -0
  15. coredis/cache.py +801 -0
  16. coredis/client/__init__.py +6 -0
  17. coredis/client/basic.py +1238 -0
  18. coredis/client/cluster.py +1264 -0
  19. coredis/commands/__init__.py +64 -0
  20. coredis/commands/_key_spec.py +517 -0
  21. coredis/commands/_utils.py +108 -0
  22. coredis/commands/_validators.py +159 -0
  23. coredis/commands/_wrappers.py +175 -0
  24. coredis/commands/bitfield.py +110 -0
  25. coredis/commands/constants.py +662 -0
  26. coredis/commands/core.py +8484 -0
  27. coredis/commands/function.py +408 -0
  28. coredis/commands/monitor.py +168 -0
  29. coredis/commands/pubsub.py +905 -0
  30. coredis/commands/request.py +108 -0
  31. coredis/commands/script.py +296 -0
  32. coredis/commands/sentinel.py +246 -0
  33. coredis/config.py +50 -0
  34. coredis/connection.py +906 -0
  35. coredis/constants.cpython-314t-darwin.so +0 -0
  36. coredis/constants.py +37 -0
  37. coredis/credentials.py +45 -0
  38. coredis/exceptions.py +360 -0
  39. coredis/experimental/__init__.py +1 -0
  40. coredis/globals.py +23 -0
  41. coredis/modules/__init__.py +117 -0
  42. coredis/modules/autocomplete.py +138 -0
  43. coredis/modules/base.py +262 -0
  44. coredis/modules/filters.py +1319 -0
  45. coredis/modules/graph.py +362 -0
  46. coredis/modules/json.py +691 -0
  47. coredis/modules/response/__init__.py +0 -0
  48. coredis/modules/response/_callbacks/__init__.py +0 -0
  49. coredis/modules/response/_callbacks/autocomplete.py +42 -0
  50. coredis/modules/response/_callbacks/graph.py +237 -0
  51. coredis/modules/response/_callbacks/json.py +21 -0
  52. coredis/modules/response/_callbacks/search.py +221 -0
  53. coredis/modules/response/_callbacks/timeseries.py +158 -0
  54. coredis/modules/response/types.py +179 -0
  55. coredis/modules/search.py +1089 -0
  56. coredis/modules/timeseries.py +1139 -0
  57. coredis/parser.cpython-314t-darwin.so +0 -0
  58. coredis/parser.py +344 -0
  59. coredis/pipeline.py +1225 -0
  60. coredis/pool/__init__.py +11 -0
  61. coredis/pool/basic.py +453 -0
  62. coredis/pool/cluster.py +517 -0
  63. coredis/pool/nodemanager.py +340 -0
  64. coredis/py.typed +0 -0
  65. coredis/recipes/__init__.py +0 -0
  66. coredis/recipes/credentials/__init__.py +5 -0
  67. coredis/recipes/credentials/iam_provider.py +63 -0
  68. coredis/recipes/locks/__init__.py +5 -0
  69. coredis/recipes/locks/extend.lua +17 -0
  70. coredis/recipes/locks/lua_lock.py +281 -0
  71. coredis/recipes/locks/release.lua +10 -0
  72. coredis/response/__init__.py +5 -0
  73. coredis/response/_callbacks/__init__.py +538 -0
  74. coredis/response/_callbacks/acl.py +32 -0
  75. coredis/response/_callbacks/cluster.py +183 -0
  76. coredis/response/_callbacks/command.py +86 -0
  77. coredis/response/_callbacks/connection.py +31 -0
  78. coredis/response/_callbacks/geo.py +58 -0
  79. coredis/response/_callbacks/hash.py +85 -0
  80. coredis/response/_callbacks/keys.py +59 -0
  81. coredis/response/_callbacks/module.py +33 -0
  82. coredis/response/_callbacks/script.py +85 -0
  83. coredis/response/_callbacks/sentinel.py +179 -0
  84. coredis/response/_callbacks/server.py +241 -0
  85. coredis/response/_callbacks/sets.py +44 -0
  86. coredis/response/_callbacks/sorted_set.py +204 -0
  87. coredis/response/_callbacks/streams.py +185 -0
  88. coredis/response/_callbacks/strings.py +70 -0
  89. coredis/response/_callbacks/vector_sets.py +159 -0
  90. coredis/response/_utils.py +33 -0
  91. coredis/response/types.py +416 -0
  92. coredis/retry.py +233 -0
  93. coredis/sentinel.py +477 -0
  94. coredis/stream.py +369 -0
  95. coredis/tokens.py +2286 -0
  96. coredis/typing.py +580 -0
  97. coredis-5.2.0.dist-info/METADATA +211 -0
  98. coredis-5.2.0.dist-info/RECORD +100 -0
  99. coredis-5.2.0.dist-info/WHEEL +6 -0
  100. coredis-5.2.0.dist-info/licenses/LICENSE +23 -0
@@ -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