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,11 @@
1
+ from __future__ import annotations
2
+
3
+ from .basic import BlockingConnectionPool, ConnectionPool
4
+ from .cluster import BlockingClusterConnectionPool, ClusterConnectionPool
5
+
6
+ __all__ = [
7
+ "ConnectionPool",
8
+ "BlockingConnectionPool",
9
+ "ClusterConnectionPool",
10
+ "BlockingClusterConnectionPool",
11
+ ]
coredis/pool/basic.py ADDED
@@ -0,0 +1,453 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import threading
6
+ import time
7
+ import warnings
8
+ from itertools import chain
9
+ from ssl import SSLContext, VerifyMode
10
+ from typing import Any, cast
11
+ from urllib.parse import parse_qs, unquote, urlparse
12
+
13
+ import async_timeout
14
+
15
+ from coredis._utils import query_param_to_bool
16
+ from coredis.connection import (
17
+ BaseConnection,
18
+ Connection,
19
+ RedisSSLContext,
20
+ UnixDomainSocketConnection,
21
+ )
22
+ from coredis.exceptions import ConnectionError
23
+ from coredis.typing import Callable, ClassVar, RedisValueT, TypeVar
24
+
25
+ _CPT = TypeVar("_CPT", bound="ConnectionPool")
26
+
27
+
28
+ class ConnectionPool:
29
+ """Generic connection pool"""
30
+
31
+ #: Mapping of querystring arguments to their parser functions
32
+ URL_QUERY_ARGUMENT_PARSERS: ClassVar[
33
+ dict[str, Callable[..., int | float | bool | str | None]]
34
+ ] = {
35
+ "client_name": str,
36
+ "stream_timeout": float,
37
+ "connect_timeout": float,
38
+ "max_connections": int,
39
+ "max_idle_time": int,
40
+ "protocol_version": int,
41
+ "idle_check_interval": int,
42
+ "noreply": bool,
43
+ "noevict": bool,
44
+ "notouch": bool,
45
+ }
46
+
47
+ @classmethod
48
+ def from_url(
49
+ cls: type[_CPT],
50
+ url: str,
51
+ db: int | None = None,
52
+ decode_components: bool = False,
53
+ **kwargs: Any,
54
+ ) -> _CPT:
55
+ """
56
+ Returns a connection pool configured from the given URL.
57
+
58
+ For example:
59
+
60
+ - ``redis://[:password]@localhost:6379/0``
61
+ - ``rediss://[:password]@localhost:6379/0``
62
+ - ``unix://[:password]@/path/to/socket.sock?db=0``
63
+
64
+ Three URL schemes are supported:
65
+
66
+ - `redis:// <http://www.iana.org/assignments/uri-schemes/prov/redis>`__
67
+ creates a normal TCP socket connection
68
+ - `rediss:// <http://www.iana.org/assignments/uri-schemes/prov/rediss>`__
69
+ creates a SSL wrapped TCP socket connection
70
+ - ``unix://`` creates a Unix Domain Socket connection
71
+
72
+ There are several ways to specify a database number. The parse function
73
+ will return the first specified option:
74
+
75
+ - A ``db`` querystring option, e.g. ``redis://localhost?db=0``
76
+ - If using the ``redis://`` scheme, the path argument of the url, e.g.
77
+ ``redis://localhost/0``
78
+ - The ``db`` argument to this function.
79
+
80
+ If none of these options are specified, ``db=0`` is used.
81
+
82
+ The :paramref:`decode_components` argument allows this function to work with
83
+ percent-encoded URLs. If this argument is set to ``True`` all ``%xx``
84
+ escapes will be replaced by their single-character equivalents after
85
+ the URL has been parsed. This only applies to the ``hostname``,
86
+ ``path``, and ``password`` components. See :attr:`URL_QUERY_ARGUMENT_PARSERS`
87
+ for a comprehensive mapping of querystring parameters to how they are parsed.
88
+
89
+ Any additional querystring arguments and keyword arguments will be
90
+ passed along to the class constructor. The querystring
91
+ arguments ``connect_timeout`` and ``stream_timeout`` if supplied
92
+ are parsed as float values.
93
+
94
+ .. note:: In the case of conflicting arguments, querystring arguments always win.
95
+ """
96
+ parsed_url = urlparse(url)
97
+ qs = parsed_url.query
98
+
99
+ url_options: dict[
100
+ str,
101
+ int | float | bool | str | type[BaseConnection] | SSLContext | None,
102
+ ] = {}
103
+ for name, value in iter(parse_qs(qs).items()):
104
+ if value and len(value) > 0:
105
+ parser = cls.URL_QUERY_ARGUMENT_PARSERS.get(name)
106
+
107
+ if parser:
108
+ try:
109
+ url_options[name] = parser(value[0])
110
+ except (TypeError, ValueError):
111
+ warnings.warn(UserWarning(f"Invalid value for `{name}` in connection URL."))
112
+ else:
113
+ url_options[name] = value[0]
114
+
115
+ username: str | None = parsed_url.username
116
+ password: str | None = parsed_url.password
117
+ path: str | None = parsed_url.path
118
+ hostname: str | None = parsed_url.hostname
119
+
120
+ if decode_components:
121
+ username = unquote(username) if username else None
122
+ password = unquote(password) if password else None
123
+ path = unquote(path) if path else None
124
+ hostname = unquote(hostname) if hostname else None
125
+
126
+ # We only support redis:// and unix:// schemes.
127
+
128
+ if parsed_url.scheme == "unix":
129
+ url_options.update(
130
+ {
131
+ "username": username,
132
+ "password": password,
133
+ "path": path,
134
+ "connection_class": UnixDomainSocketConnection,
135
+ }
136
+ )
137
+
138
+ else:
139
+ url_options.update(
140
+ {
141
+ "host": hostname,
142
+ "port": int(parsed_url.port or 6379),
143
+ "username": username,
144
+ "password": password,
145
+ }
146
+ )
147
+
148
+ # If there's a path argument, use it as the db argument if a
149
+ # querystring value wasn't specified
150
+
151
+ if "db" not in url_options and path:
152
+ try:
153
+ url_options["db"] = int(path.replace("/", ""))
154
+ except (AttributeError, ValueError):
155
+ pass
156
+
157
+ if parsed_url.scheme == "rediss":
158
+ if "ssl_context" not in kwargs:
159
+ keyfile = cast(str | None, url_options.pop("ssl_keyfile", None))
160
+ certfile = cast(str | None, url_options.pop("ssl_certfile", None))
161
+ check_hostname = query_param_to_bool(
162
+ url_options.pop("ssl_check_hostname", None)
163
+ )
164
+ cert_reqs = cast(
165
+ str | VerifyMode | None,
166
+ url_options.pop("ssl_cert_reqs", None),
167
+ )
168
+ ca_certs = cast(str | None, url_options.pop("ssl_ca_certs", None))
169
+ url_options["ssl_context"] = RedisSSLContext(
170
+ keyfile, certfile, cert_reqs, ca_certs, check_hostname
171
+ ).get()
172
+
173
+ # last shot at the db value
174
+ _db = url_options.get("db", db or 0)
175
+ assert isinstance(_db, (int, str, bytes))
176
+ url_options["db"] = int(_db)
177
+
178
+ # update the arguments from the URL values
179
+ kwargs.update(url_options)
180
+
181
+ return cls(**kwargs)
182
+
183
+ def __init__(
184
+ self,
185
+ *,
186
+ connection_class: type[Connection] | None = None,
187
+ max_connections: int | None = None,
188
+ max_idle_time: int = 0,
189
+ idle_check_interval: int = 1,
190
+ **connection_kwargs: Any | None,
191
+ ) -> None:
192
+ """
193
+ Creates a connection pool. If :paramref:`max_connections` is set, then this
194
+ object raises :class:`~coredis.ConnectionError` when the pool's limit is reached.
195
+
196
+ By default, TCP connections are created :paramref:`connection_class` is specified.
197
+ Use :class:`~coredis.UnixDomainSocketConnection` for unix sockets.
198
+
199
+ Any additional keyword arguments are passed to the constructor of
200
+ connection_class.
201
+ """
202
+ self.connection_class = connection_class or Connection
203
+ self.connection_kwargs = connection_kwargs
204
+ self.max_connections = max_connections or 2**31
205
+ self.max_idle_time = max_idle_time
206
+ self.idle_check_interval = idle_check_interval
207
+ self.initialized = False
208
+ self.reset()
209
+ self.decode_responses = bool(self.connection_kwargs.get("decode_responses", False))
210
+ self.encoding = str(self.connection_kwargs.get("encoding", "utf-8"))
211
+
212
+ async def initialize(self) -> None:
213
+ self.initialized = True
214
+
215
+ def __repr__(self) -> str:
216
+ return f"{type(self).__name__}<{self.connection_class.describe(self.connection_kwargs)}>"
217
+
218
+ def __del__(self) -> None:
219
+ self.disconnect()
220
+
221
+ async def disconnect_on_idle_time_exceeded(self, connection: Connection) -> None:
222
+ while True:
223
+ if (
224
+ time.time() - connection.last_active_at > self.max_idle_time
225
+ and not connection.requests_pending
226
+ ):
227
+ connection.disconnect()
228
+ if connection in self._available_connections:
229
+ self._available_connections.remove(connection)
230
+ self._created_connections -= 1
231
+ break
232
+ await asyncio.sleep(self.idle_check_interval)
233
+
234
+ def reset(self) -> None:
235
+ self.pid = os.getpid()
236
+ self._created_connections = 0
237
+ self._available_connections: list[Connection] = []
238
+ self._in_use_connections: set[Connection] = set()
239
+ self._check_lock = threading.Lock()
240
+
241
+ def checkpid(self) -> None: # noqa
242
+ if self.pid != os.getpid():
243
+ with self._check_lock:
244
+ # Double check
245
+ if self.pid == os.getpid():
246
+ return
247
+ self.disconnect()
248
+ self.reset()
249
+
250
+ def peek_available(self) -> BaseConnection | None:
251
+ return self._available_connections[0] if self._available_connections else None
252
+
253
+ async def get_connection(
254
+ self,
255
+ command_name: bytes | None = None,
256
+ *args: RedisValueT,
257
+ acquire: bool = True,
258
+ **kwargs: RedisValueT | None,
259
+ ) -> Connection:
260
+ """Gets a connection from the pool"""
261
+ self.checkpid()
262
+ try:
263
+ connection = self._available_connections.pop()
264
+ if connection.is_connected and connection.needs_handshake:
265
+ await connection.perform_handshake()
266
+ except IndexError:
267
+ if self._created_connections >= self.max_connections:
268
+ raise ConnectionError("Too many connections")
269
+ connection = self._make_connection(**kwargs)
270
+
271
+ if acquire:
272
+ self._in_use_connections.add(connection)
273
+ else:
274
+ self._available_connections.append(connection)
275
+
276
+ return connection
277
+
278
+ def release(self, connection: Connection) -> None:
279
+ """
280
+ Releases the :paramref:`connection` back to the pool
281
+ """
282
+ self.checkpid()
283
+
284
+ if connection.pid == self.pid:
285
+ self._in_use_connections.remove(connection)
286
+ self._available_connections.append(connection)
287
+
288
+ def disconnect(self) -> None:
289
+ """Closes all connections in the pool"""
290
+ all_conns = chain(self._available_connections, self._in_use_connections)
291
+
292
+ for connection in all_conns:
293
+ connection.disconnect()
294
+ self._created_connections -= 1
295
+
296
+ def _make_connection(self, **options: RedisValueT | None) -> Connection:
297
+ """
298
+ Creates a new connection
299
+ """
300
+
301
+ self._created_connections += 1
302
+ connection = self.connection_class(
303
+ **self.connection_kwargs, # type: ignore
304
+ )
305
+
306
+ if self.max_idle_time > self.idle_check_interval > 0:
307
+ # do not await the future
308
+ asyncio.ensure_future(self.disconnect_on_idle_time_exceeded(connection))
309
+
310
+ return connection
311
+
312
+
313
+ class BlockingConnectionPool(ConnectionPool):
314
+ """
315
+ Blocking connection pool::
316
+
317
+ >>> from coredis import Redis
318
+ >>> client = Redis(connection_pool=BlockingConnectionPool())
319
+
320
+ It performs the same function as the default
321
+ :class:`~coredis.ConnectionPool`, in that, it maintains a pool of reusable
322
+ connections that can be shared by multiple redis clients.
323
+
324
+ The difference is that, in the event that a client tries to get a
325
+ connection from the pool when all of the connections are in use, rather than
326
+ raising a :exc:`~coredis.ConnectionError` (as the default
327
+ :class:`~coredis.ConnectionPool` implementation does), it
328
+ makes the client blocks for a specified number of seconds until
329
+ a connection becomes available.
330
+
331
+ Use :paramref:`max_connections` to increase / decrease the pool size::
332
+
333
+ >>> pool = BlockingConnectionPool(max_connections=10)
334
+
335
+ Use :paramref:`timeout` to tell it either how many seconds to wait for a
336
+ connection to become available, or to block forever::
337
+
338
+ >>> # Block forever.
339
+ >>> pool = BlockingConnectionPool(timeout=None)
340
+ >>> # Raise a ``ConnectionError`` after five seconds if a connection is
341
+ >>> # not available.
342
+ >>> pool = BlockingConnectionPool(timeout=5)
343
+ """
344
+
345
+ def __init__(
346
+ self,
347
+ connection_class: type[Connection] | None = None,
348
+ queue_class: type[asyncio.Queue[Connection | None]] = asyncio.LifoQueue,
349
+ max_connections: int | None = None,
350
+ timeout: int = 20,
351
+ max_idle_time: int = 0,
352
+ idle_check_interval: int = 1,
353
+ **connection_kwargs: RedisValueT | None,
354
+ ):
355
+ self.timeout = timeout
356
+ self.queue_class = queue_class
357
+ self.total_wait = 0
358
+ self.total_allocated = 0
359
+ max_connections = max_connections or 50
360
+
361
+ super().__init__(
362
+ connection_class=connection_class or Connection,
363
+ max_connections=max_connections,
364
+ max_idle_time=max_idle_time,
365
+ idle_check_interval=idle_check_interval,
366
+ **connection_kwargs,
367
+ )
368
+
369
+ async def disconnect_on_idle_time_exceeded(self, connection: Connection) -> None:
370
+ while True:
371
+ if time.time() - connection.last_active_at > self.max_idle_time:
372
+ # Unlike the non blocking pool, we don't free the connection object,
373
+ # but always reuse it
374
+ connection.disconnect()
375
+
376
+ break
377
+ await asyncio.sleep(self.idle_check_interval)
378
+
379
+ def reset(self) -> None:
380
+ self._pool: asyncio.Queue[Connection | None] = self.queue_class(self.max_connections)
381
+
382
+ while True:
383
+ try:
384
+ self._pool.put_nowait(None)
385
+ except asyncio.QueueFull:
386
+ break
387
+
388
+ super().reset()
389
+
390
+ def peek_available(self) -> BaseConnection | None:
391
+ return (
392
+ self._pool._queue[-1] # type: ignore
393
+ if (self._pool and not self._pool.empty())
394
+ else None
395
+ )
396
+
397
+ async def get_connection(
398
+ self,
399
+ command_name: bytes | None = None,
400
+ *args: RedisValueT,
401
+ acquire: bool = True,
402
+ **kwargs: RedisValueT | None,
403
+ ) -> Connection:
404
+ """Gets a connection from the pool"""
405
+ self.checkpid()
406
+
407
+ try:
408
+ async with async_timeout.timeout(self.timeout):
409
+ connection = await self._pool.get()
410
+ if connection and connection.is_connected and connection.needs_handshake:
411
+ await connection.perform_handshake()
412
+ except asyncio.TimeoutError:
413
+ raise ConnectionError("No connection available.")
414
+ if connection is None:
415
+ connection = self._make_connection()
416
+
417
+ if acquire:
418
+ self._in_use_connections.add(connection)
419
+ else:
420
+ self._pool.put_nowait(connection)
421
+
422
+ return connection
423
+
424
+ def release(self, connection: Connection) -> None:
425
+ """Releases the connection back to the pool"""
426
+ _connection: Connection | None = connection
427
+
428
+ self.checkpid()
429
+
430
+ if _connection and _connection.pid == self.pid:
431
+ self._in_use_connections.remove(_connection)
432
+ try:
433
+ self._pool.put_nowait(_connection)
434
+ except asyncio.QueueFull:
435
+ _connection.disconnect()
436
+
437
+ def disconnect(self) -> None:
438
+ """Closes all connections in the pool"""
439
+ pooled_connections: list[Connection | None] = []
440
+
441
+ while True:
442
+ try:
443
+ pooled_connections.append(self._pool.get_nowait())
444
+ except asyncio.QueueEmpty:
445
+ break
446
+ for conn in pooled_connections:
447
+ self._pool.put_nowait(conn)
448
+
449
+ all_conns = chain(pooled_connections, self._in_use_connections)
450
+
451
+ for connection in all_conns:
452
+ if connection is not None:
453
+ connection.disconnect()