coredis 5.5.0__cp314-cp314-macosx_10_13_x86_64.whl

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