coredis 5.5.0__cp313-cp313-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. 22fe76227e35f92ab5c3__mypyc.cpython-313-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-313-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-313-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-313-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-313-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
coredis/sentinel.py ADDED
@@ -0,0 +1,477 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ import ssl
5
+ import weakref
6
+ from typing import Any, cast, overload
7
+
8
+ from coredis import Redis
9
+ from coredis._utils import nativestr
10
+ from coredis.cache import AbstractCache
11
+ from coredis.connection import Connection
12
+ from coredis.credentials import AbstractCredentialProvider
13
+ from coredis.exceptions import (
14
+ ConnectionError,
15
+ PrimaryNotFoundError,
16
+ ReplicaNotFoundError,
17
+ ResponseError,
18
+ TimeoutError,
19
+ )
20
+ from coredis.pool import ConnectionPool
21
+ from coredis.typing import (
22
+ AnyStr,
23
+ Generic,
24
+ Iterable,
25
+ Literal,
26
+ ResponsePrimitive,
27
+ StringT,
28
+ TypeAdapter,
29
+ )
30
+
31
+
32
+ class SentinelManagedConnection(Connection, Generic[AnyStr]):
33
+ def __init__(
34
+ self,
35
+ connection_pool: SentinelConnectionPool,
36
+ host: str = "127.0.0.1",
37
+ port: int = 6379,
38
+ username: str | None = None,
39
+ password: str | None = None,
40
+ credential_provider: AbstractCredentialProvider | None = None,
41
+ db: int = 0,
42
+ stream_timeout: float | None = None,
43
+ connect_timeout: float | None = None,
44
+ ssl_context: ssl.SSLContext | None = None,
45
+ encoding: str = "utf-8",
46
+ decode_responses: bool = False,
47
+ socket_keepalive: bool | None = None,
48
+ socket_keepalive_options: dict[int, int | bytes] | None = None,
49
+ *,
50
+ client_name: str | None = None,
51
+ protocol_version: Literal[2, 3] = 3,
52
+ ):
53
+ self.connection_pool: SentinelConnectionPool = weakref.proxy(connection_pool)
54
+ super().__init__(
55
+ host=host,
56
+ port=port,
57
+ username=username,
58
+ password=password,
59
+ credential_provider=credential_provider,
60
+ db=db,
61
+ stream_timeout=stream_timeout,
62
+ connect_timeout=connect_timeout,
63
+ ssl_context=ssl_context,
64
+ encoding=encoding,
65
+ decode_responses=decode_responses,
66
+ socket_keepalive=socket_keepalive,
67
+ socket_keepalive_options=socket_keepalive_options,
68
+ client_name=client_name,
69
+ protocol_version=protocol_version,
70
+ )
71
+
72
+ def __repr__(self) -> str:
73
+ pool = self.connection_pool
74
+
75
+ if self.host:
76
+ host_info = f",host={self.host},port={self.port}"
77
+ else:
78
+ host_info = ""
79
+ s = f"{type(self).__name__}<service={pool.service_name}{host_info}>"
80
+
81
+ return s
82
+
83
+ async def connect_to(self, address: tuple[str, int]) -> None:
84
+ self.host, self.port = address
85
+ await super().connect()
86
+
87
+ async def connect(self) -> None:
88
+ if not self.is_connected:
89
+ if self.connection_pool.is_primary:
90
+ await self.connect_to(await self.connection_pool.get_primary_address())
91
+ else:
92
+ for replica in await self.connection_pool.rotate_replicas():
93
+ try:
94
+ return await self.connect_to(replica)
95
+ except ConnectionError:
96
+ continue
97
+ raise ReplicaNotFoundError # Never be here
98
+
99
+ return None
100
+
101
+
102
+ class SentinelConnectionPool(ConnectionPool):
103
+ """
104
+ Sentinel backed connection pool.
105
+ """
106
+
107
+ primary_address: tuple[str, int] | None
108
+ replica_counter: int | None
109
+
110
+ def __init__(
111
+ self,
112
+ service_name: StringT,
113
+ sentinel_manager: Sentinel[Any],
114
+ is_primary: bool = True,
115
+ check_connection: bool = True,
116
+ **kwargs: Any,
117
+ ):
118
+ self.is_primary = is_primary
119
+ kwargs["connection_class"] = cast(
120
+ type[Connection],
121
+ kwargs.get(
122
+ "connection_class",
123
+ SentinelManagedConnection[AnyStr], # type: ignore
124
+ ),
125
+ )
126
+ super().__init__(**kwargs)
127
+ self.connection_kwargs["connection_pool"] = self
128
+ self.service_name = nativestr(service_name)
129
+ self.sentinel_manager = sentinel_manager
130
+ self.check_connection = check_connection
131
+
132
+ def __repr__(self) -> str:
133
+ return (
134
+ f"{type(self).__name__}"
135
+ f"<service={self.service_name}"
136
+ f"({'primary' if self.is_primary else 'replica'})"
137
+ )
138
+
139
+ def reset(self) -> None:
140
+ super().reset()
141
+ self.primary_address = None
142
+ self.replica_counter = None
143
+
144
+ async def get_primary_address(self) -> tuple[str, int]:
145
+ primary_address = await self.sentinel_manager.discover_primary(self.service_name)
146
+
147
+ if self.is_primary:
148
+ if self.primary_address is None:
149
+ self.primary_address = primary_address
150
+ elif primary_address != self.primary_address:
151
+ # Primary address changed, disconnect all clients in this pool
152
+ self.disconnect()
153
+
154
+ return primary_address
155
+
156
+ async def rotate_replicas(self) -> list[tuple[str, int]]:
157
+ """Round-robin replicas balancer"""
158
+ replicas = await self.sentinel_manager.discover_replicas(self.service_name)
159
+ replica_addresses: list[tuple[str, int]] = []
160
+
161
+ if replicas:
162
+ if self.replica_counter is None:
163
+ self.replica_counter = random.randint(0, len(replicas) - 1)
164
+
165
+ for _ in range(len(replicas)):
166
+ self.replica_counter = (self.replica_counter + 1) % len(replicas)
167
+ replica_addresses.append(replicas[self.replica_counter])
168
+
169
+ return replica_addresses
170
+ # Fallback to primary
171
+ try:
172
+ return [await self.get_primary_address()]
173
+ except PrimaryNotFoundError:
174
+ pass
175
+ raise ReplicaNotFoundError(f"No replica found for {self.service_name!r}")
176
+
177
+
178
+ class Sentinel(Generic[AnyStr]):
179
+ """
180
+ Example use::
181
+
182
+ from coredis.sentinel import Sentinel
183
+ sentinel = Sentinel([('localhost', 26379)], stream_timeout=0.1)
184
+ async def test():
185
+ primary = await sentinel.primary_for('my-instance', stream_timeout=0.1)
186
+ await primary.set('foo', 'bar')
187
+ replica = await sentinel.replica_for('my-instance', stream_timeout=0.1)
188
+ await replica.get('foo')
189
+
190
+ """
191
+
192
+ @overload
193
+ def __init__(
194
+ self: Sentinel[bytes],
195
+ sentinels: Iterable[tuple[str, int]],
196
+ min_other_sentinels: int = ...,
197
+ sentinel_kwargs: dict[str, Any] | None = ...,
198
+ decode_responses: Literal[False] = ...,
199
+ cache: AbstractCache | None = None,
200
+ type_adapter: TypeAdapter | None = ...,
201
+ **connection_kwargs: Any,
202
+ ) -> None: ...
203
+
204
+ @overload
205
+ def __init__(
206
+ self: Sentinel[str],
207
+ sentinels: Iterable[tuple[str, int]],
208
+ min_other_sentinels: int = ...,
209
+ sentinel_kwargs: dict[str, Any] | None = ...,
210
+ decode_responses: Literal[True] = ...,
211
+ cache: AbstractCache | None = None,
212
+ type_adapter: TypeAdapter | None = None,
213
+ **connection_kwargs: Any,
214
+ ) -> None: ...
215
+
216
+ def __init__(
217
+ self,
218
+ sentinels: Iterable[tuple[str, int]],
219
+ min_other_sentinels: int = 0,
220
+ sentinel_kwargs: dict[str, Any] | None = None,
221
+ decode_responses: bool = False,
222
+ cache: AbstractCache | None = None,
223
+ type_adapter: TypeAdapter | None = None,
224
+ **connection_kwargs: Any,
225
+ ) -> None:
226
+ """
227
+ Changes
228
+ - .. versionadded:: 3.10.0
229
+ Accept :paramref:`cache` parameter to be used with primaries
230
+ and replicas returned from the sentinel instance.
231
+
232
+ :param sentinels: is a list of sentinel nodes. Each node is represented by
233
+ a pair (hostname, port).
234
+ :param min_other_sentinels: defined a minimum number of peers for a sentinel.
235
+ When querying a sentinel, if it doesn't meet this threshold, responses
236
+ from that sentinel won't be considered valid.
237
+ :param sentinel_kwargs: is a dictionary of connection arguments used when
238
+ connecting to sentinel instances. Any argument that can be passed to
239
+ a normal Redis connection can be specified here. If :paramref:`sentinel_kwargs` is
240
+ not specified, ``stream_timeout``, ``socket_keepalive``, ``decode_responses``
241
+ and ``protocol_version`` options specified in :paramref:`connection_kwargs` will be used.
242
+ :param cache: If provided the cache will be shared between both primaries and replicas
243
+ returned by this sentinel.
244
+ :param type_adapter: The adapter to use for serializing / deserializing customs types
245
+ when interacting with redis commands. If provided this adapter will be used for both
246
+ primaries and replicas returned by this sentinel.
247
+ :param connection_kwargs: are keyword arguments that will be used when
248
+ establishing a connection to a Redis server (i.e. are passed on to the
249
+ constructor of :class:`Redis` for all primary and replicas).
250
+ """
251
+ # if sentinel_kwargs isn't defined, use the socket_* options from
252
+ # connection_kwargs
253
+
254
+ if not sentinel_kwargs:
255
+ sentinel_kwargs = {
256
+ k: v
257
+ for k, v in iter(connection_kwargs.items())
258
+ if k
259
+ in {
260
+ "socket_timeout",
261
+ "socket_keepalive",
262
+ "encoding",
263
+ "protocol_version",
264
+ }
265
+ }
266
+
267
+ self.sentinel_kwargs = sentinel_kwargs
268
+ self.min_other_sentinels = min_other_sentinels
269
+ self.connection_kwargs = connection_kwargs
270
+ self.__cache = cache
271
+ self.__type_adapter = type_adapter
272
+ self.connection_kwargs["decode_responses"] = self.sentinel_kwargs["decode_responses"] = (
273
+ decode_responses
274
+ )
275
+
276
+ self.sentinels = [
277
+ Redis(hostname, port, **self.sentinel_kwargs) for hostname, port in sentinels
278
+ ]
279
+
280
+ def __repr__(self) -> str:
281
+ sentinel_addresses: list[str] = []
282
+
283
+ for sentinel in self.sentinels:
284
+ sentinel_addresses.append(
285
+ "{}:{}".format(
286
+ sentinel.connection_pool.connection_kwargs["host"],
287
+ sentinel.connection_pool.connection_kwargs["port"],
288
+ )
289
+ )
290
+
291
+ return "{}<sentinels=[{}]>".format(type(self).__name__, ",".join(sentinel_addresses))
292
+
293
+ def __check_primary_state(
294
+ self,
295
+ state: dict[str, ResponsePrimitive],
296
+ ) -> bool:
297
+ if not state["is_master"] or state["is_sdown"] or state["is_odown"]:
298
+ return False
299
+
300
+ if int(state["num-other-sentinels"] or 0) < self.min_other_sentinels:
301
+ return False
302
+
303
+ return True
304
+
305
+ def __filter_replicas(
306
+ self, replicas: Iterable[dict[str, ResponsePrimitive]]
307
+ ) -> list[tuple[str, int]]:
308
+ """Removes replicas that are in an ODOWN or SDOWN state"""
309
+ replicas_alive: list[tuple[str, int]] = []
310
+
311
+ for replica in replicas:
312
+ if replica["is_odown"] or replica["is_sdown"]:
313
+ continue
314
+ ip, port = replica["ip"], replica["port"]
315
+ assert ip and port
316
+ replicas_alive.append((nativestr(ip), int(port)))
317
+
318
+ return replicas_alive
319
+
320
+ async def discover_primary(self, service_name: str) -> tuple[str, int]:
321
+ """
322
+ Asks sentinel servers for the Redis primary's address corresponding
323
+ to the service labeled :paramref:`service_name`.
324
+
325
+ :return: A pair (address, port) or raises :exc:`~coredis.exceptions.PrimaryNotFoundError`
326
+ if no primary is found.
327
+ """
328
+
329
+ for sentinel_no, sentinel in enumerate(self.sentinels):
330
+ try:
331
+ primaries = await sentinel.sentinel_masters()
332
+ except (ConnectionError, TimeoutError):
333
+ continue
334
+ state = primaries.get(service_name)
335
+
336
+ if state and self.__check_primary_state(state):
337
+ # Put this sentinel at the top of the list
338
+ self.sentinels[0], self.sentinels[sentinel_no] = (
339
+ sentinel,
340
+ self.sentinels[0],
341
+ )
342
+
343
+ return nativestr(state["ip"]), int(state["port"] or -1)
344
+ raise PrimaryNotFoundError(f"No primary found for {service_name!r}")
345
+
346
+ async def discover_replicas(self, service_name: str) -> list[tuple[str, int]]:
347
+ """Returns a list of alive replicas for service :paramref:`service_name`"""
348
+
349
+ for sentinel in self.sentinels:
350
+ try:
351
+ replicas = await sentinel.sentinel_replicas(service_name)
352
+ except (ConnectionError, ResponseError, TimeoutError):
353
+ continue
354
+ filtered_replicas = self.__filter_replicas(replicas)
355
+
356
+ if filtered_replicas:
357
+ return filtered_replicas
358
+
359
+ return []
360
+
361
+ @overload
362
+ def primary_for(
363
+ self: Sentinel[bytes],
364
+ service_name: str,
365
+ *,
366
+ redis_class: type[Redis[bytes]] = ...,
367
+ connection_pool_class: type[SentinelConnectionPool] = ...,
368
+ **kwargs: Any,
369
+ ) -> Redis[bytes]: ...
370
+
371
+ @overload
372
+ def primary_for(
373
+ self: Sentinel[str],
374
+ service_name: str,
375
+ *,
376
+ redis_class: type[Redis[str]] = ...,
377
+ connection_pool_class: type[SentinelConnectionPool] = ...,
378
+ **kwargs: Any,
379
+ ) -> Redis[str]: ...
380
+
381
+ def primary_for(
382
+ self,
383
+ service_name: str,
384
+ *,
385
+ redis_class: type[Redis[Any]] = Redis[Any],
386
+ connection_pool_class: type[SentinelConnectionPool] = SentinelConnectionPool,
387
+ **kwargs: Any,
388
+ ) -> Redis[bytes] | Redis[str]:
389
+ """
390
+ Returns a redis client instance for the :paramref:`service_name` primary.
391
+
392
+ A :class:`coredis.sentinel.SentinelConnectionPool` class is used to
393
+ retrive the primary's address before establishing a new connection.
394
+
395
+ NOTE: If the primary's address has changed, any cached connections to
396
+ the old primary are closed.
397
+
398
+ By default clients will be a :class:`~coredis.Redis` instances.
399
+ Specify a different class to the :paramref:`redis_class` argument if you desire
400
+ something different.
401
+
402
+ The :paramref:`connection_pool_class` specifies the connection pool to use.
403
+ The :class:`~coredis.sentinel.SentinelConnectionPool` will be used by default.
404
+
405
+ All other keyword arguments are merged with any :paramref:`Sentinel.connection_kwargs`
406
+ passed to this class and passed to the connection pool as keyword
407
+ arguments to be used to initialize Redis connections.
408
+ """
409
+ kwargs["is_primary"] = True
410
+ connection_kwargs = dict(self.connection_kwargs)
411
+ connection_kwargs.update(kwargs)
412
+
413
+ return redis_class(
414
+ connection_pool=connection_pool_class(
415
+ service_name,
416
+ self,
417
+ **connection_kwargs,
418
+ ),
419
+ cache=self.__cache,
420
+ type_adapter=self.__type_adapter,
421
+ )
422
+
423
+ @overload
424
+ def replica_for(
425
+ self: Sentinel[bytes],
426
+ service_name: str,
427
+ redis_class: type[Redis[bytes]] = ...,
428
+ connection_pool_class: type[SentinelConnectionPool] = ...,
429
+ **kwargs: Any,
430
+ ) -> Redis[bytes]: ...
431
+
432
+ @overload
433
+ def replica_for(
434
+ self: Sentinel[str],
435
+ service_name: str,
436
+ redis_class: type[Redis[str]] = ...,
437
+ connection_pool_class: type[SentinelConnectionPool] = ...,
438
+ **kwargs: Any,
439
+ ) -> Redis[str]: ...
440
+
441
+ def replica_for(
442
+ self,
443
+ service_name: str,
444
+ redis_class: type[Redis[Any]] = Redis[Any],
445
+ connection_pool_class: type[SentinelConnectionPool] = SentinelConnectionPool,
446
+ **kwargs: Any,
447
+ ) -> Redis[bytes] | Redis[str]:
448
+ """
449
+ Returns redis client instance for the :paramref:`service_name` replica(s).
450
+
451
+ A SentinelConnectionPool class is used to retrieve the replica's
452
+ address before establishing a new connection.
453
+
454
+ By default clients will be a redis.Redis instance. Specify a
455
+ different class to the :paramref:`redis_class` argument if you desire
456
+ something different.
457
+
458
+ The :paramref:`connection_pool_class` specifies the connection pool to use.
459
+ The SentinelConnectionPool will be used by default.
460
+
461
+ All other keyword arguments are merged with any :paramref:`Sentinel.connection_kwargs`
462
+ passed to this class and passed to the connection pool as keyword
463
+ arguments to be used to initialize Redis connections.
464
+ """
465
+ kwargs["is_primary"] = False
466
+ connection_kwargs = dict(self.connection_kwargs)
467
+ connection_kwargs.update(kwargs)
468
+
469
+ return redis_class(
470
+ connection_pool=connection_pool_class(
471
+ service_name,
472
+ self,
473
+ **connection_kwargs,
474
+ ),
475
+ cache=self.__cache,
476
+ type_adapter=self.__type_adapter,
477
+ )