trpc-redis-python 1.0.0__py3-none-any.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.
- trpc_redis/__init__.py +30 -0
- trpc_redis/_client.py +265 -0
- trpc_redis/_codec.py +69 -0
- trpc_redis/_constants.py +52 -0
- trpc_redis/_transport.py +296 -0
- trpc_redis/_types.py +65 -0
- trpc_redis/version.py +12 -0
- trpc_redis_python-1.0.0.dist-info/METADATA +637 -0
- trpc_redis_python-1.0.0.dist-info/RECORD +10 -0
- trpc_redis_python-1.0.0.dist-info/WHEEL +4 -0
trpc_redis/_transport.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making tRPC-Python-Database available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# tRPC-Python-Database is licensed under Apache-2.0.
|
|
6
|
+
"""
|
|
7
|
+
Redis transport module for tRPC framework.
|
|
8
|
+
|
|
9
|
+
This module provides a transport implementation for Redis connections, which is used by the tRPC client
|
|
10
|
+
to handle Redis-specific message serialization and deserialization.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
The transport is registered automatically as a plugin and is used internally by the tRPC Redis client.
|
|
14
|
+
|
|
15
|
+
One transport instance serves every thread of a process. Connection pools are
|
|
16
|
+
cached per thread and per event loop, because a ``redis.asyncio`` pool is
|
|
17
|
+
bound to the loop it first runs on. A forked child starts with an empty cache.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import os
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
import weakref
|
|
25
|
+
from typing import Any
|
|
26
|
+
from typing import Dict
|
|
27
|
+
from typing import Optional
|
|
28
|
+
from typing import Tuple
|
|
29
|
+
|
|
30
|
+
import redis.asyncio as redis
|
|
31
|
+
from redis import exceptions as redis_exceptions
|
|
32
|
+
|
|
33
|
+
from trpc_sdk.abc import BaseClientTransport
|
|
34
|
+
from trpc_sdk.abc import PluginInitPos
|
|
35
|
+
from trpc_sdk.abc import PluginType
|
|
36
|
+
from trpc_sdk.abc import TrpcContext
|
|
37
|
+
from trpc_sdk.exceptions import ClientDecodeError
|
|
38
|
+
from trpc_sdk.exceptions import ClientEncodeError
|
|
39
|
+
from trpc_sdk.exceptions import ClientInvokeError
|
|
40
|
+
from trpc_sdk.exceptions import ClientTimeoutError
|
|
41
|
+
from trpc_sdk.plugin import register_plugin
|
|
42
|
+
from trpc_sdk.transport import RoundTripOptions
|
|
43
|
+
|
|
44
|
+
from ._constants import DECODE_RESPONSES
|
|
45
|
+
from ._constants import DECODE_RESPONSES_DEFAULT
|
|
46
|
+
from ._constants import HOST
|
|
47
|
+
from ._constants import IDLE_TIMEOUT_DEFAULT
|
|
48
|
+
from ._constants import MAX_CONNECTIONS
|
|
49
|
+
from ._constants import MAX_CONNECTIONS_DEFAULT
|
|
50
|
+
from ._constants import PORT
|
|
51
|
+
from ._constants import PASSWORD
|
|
52
|
+
from ._constants import REDIS_NAME
|
|
53
|
+
from ._constants import RequestMode
|
|
54
|
+
from ._constants import SOCKET_CONNECT_TIMEOUT
|
|
55
|
+
from ._types import Request
|
|
56
|
+
from ._types import Response
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _LoopPools(threading.local):
|
|
60
|
+
"""Per-thread map from event loop to that loop's connection pools.
|
|
61
|
+
|
|
62
|
+
Loops are keyed by object rather than ``id()`` so a closed loop's id
|
|
63
|
+
cannot be reused to return a pool bound to the dead loop.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
"""Start the calling thread with no pools."""
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.pools: Dict[asyncio.AbstractEventLoop, Dict[str, redis.ConnectionPool]] = {}
|
|
70
|
+
|
|
71
|
+
def for_loop(self, loop: asyncio.AbstractEventLoop) -> Dict[str, redis.ConnectionPool]:
|
|
72
|
+
"""Return the pool dict of ``loop``, creating it on first use.
|
|
73
|
+
|
|
74
|
+
Entries of closed loops are dropped when a new loop is seen.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
loop: The running event loop of the calling thread.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Mutable mapping from pool key to connection pool.
|
|
81
|
+
"""
|
|
82
|
+
pools = self.pools.get(loop, None)
|
|
83
|
+
if pools is None:
|
|
84
|
+
for stale in [old for old in self.pools if old.is_closed()]:
|
|
85
|
+
del self.pools[stale]
|
|
86
|
+
pools = {}
|
|
87
|
+
self.pools[loop] = pools
|
|
88
|
+
return pools
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_TRANSPORTS: "weakref.WeakSet[RedisClientTransport]" = weakref.WeakSet()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _reset_pools_after_fork():
|
|
95
|
+
"""Drop every pool inherited from the parent process."""
|
|
96
|
+
for transport in list(_TRANSPORTS):
|
|
97
|
+
transport.reset_pools()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if hasattr(os, "register_at_fork"):
|
|
101
|
+
os.register_at_fork(after_in_child=_reset_pools_after_fork)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@register_plugin(PluginType.CLIENT_TRANSPORT, REDIS_NAME, init_pos=PluginInitPos.WORKER)
|
|
105
|
+
class RedisClientTransport(BaseClientTransport):
|
|
106
|
+
"""
|
|
107
|
+
Transport implementation for Redis connections in the tRPC framework.
|
|
108
|
+
Handles connection pooling, command, pipeline, and pubsub operations.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self):
|
|
112
|
+
"""
|
|
113
|
+
Initialize the RedisClientTransport with the default idle timeout and an empty pool cache.
|
|
114
|
+
"""
|
|
115
|
+
super().__init__()
|
|
116
|
+
self.idle_timeout = IDLE_TIMEOUT_DEFAULT
|
|
117
|
+
self._loop_pools = _LoopPools()
|
|
118
|
+
_TRANSPORTS.add(self)
|
|
119
|
+
|
|
120
|
+
def reset_pools(self):
|
|
121
|
+
"""
|
|
122
|
+
Forget every cached connection pool, in all threads.
|
|
123
|
+
"""
|
|
124
|
+
self._loop_pools = _LoopPools()
|
|
125
|
+
|
|
126
|
+
async def _process_pipeline(self, ctx: TrpcContext, pool: redis.ConnectionPool):
|
|
127
|
+
"""
|
|
128
|
+
Process pipeline requests and return a pipe object.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
ctx (TrpcContext): The context object.
|
|
132
|
+
pool (redis.ConnectionPool): The connection pool.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
str: An empty string (pipe object is set in response).
|
|
136
|
+
"""
|
|
137
|
+
pipe = await self.get_pipe(pool)
|
|
138
|
+
ctx.msg.set_client_rsp_head(Response(pipe))
|
|
139
|
+
return ""
|
|
140
|
+
|
|
141
|
+
async def _process_command(self, ctx: TrpcContext, pool: redis.ConnectionPool):
|
|
142
|
+
"""
|
|
143
|
+
Process command requests and return a response object.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
ctx (TrpcContext): The context object.
|
|
147
|
+
pool (redis.ConnectionPool): The connection pool.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Any: The reply from Redis.
|
|
151
|
+
"""
|
|
152
|
+
begin = time.time() * 1000
|
|
153
|
+
req: Request = ctx.msg.client_req_head
|
|
154
|
+
timeout = None
|
|
155
|
+
if ctx.deadline > 0:
|
|
156
|
+
remain = (ctx.deadline - begin) / 1000.0
|
|
157
|
+
if remain <= 0:
|
|
158
|
+
raise ClientTimeoutError("reach deadline")
|
|
159
|
+
timeout = remain
|
|
160
|
+
try:
|
|
161
|
+
reply = await asyncio.wait_for(self.get_response(pool, req), timeout)
|
|
162
|
+
ctx.msg.set_client_rsp_head(Response(reply))
|
|
163
|
+
except asyncio.TimeoutError as exc:
|
|
164
|
+
raise ClientTimeoutError(f"redis client timeout {timeout}, command {req.command} args {req.args}") from exc
|
|
165
|
+
except TypeError as exc:
|
|
166
|
+
raise ClientEncodeError(f"redis client args error {exc}, command {req.command} args {req.args}") from exc
|
|
167
|
+
except redis_exceptions.ResponseError as exc:
|
|
168
|
+
raise ClientDecodeError(f"redis client reply error {exc} command {req.command} args {req.args}") from exc
|
|
169
|
+
except redis_exceptions.RedisError as exc:
|
|
170
|
+
raise ClientInvokeError(f"redis client protocol error {exc} command {req.command} args {req.args}") from exc
|
|
171
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
172
|
+
raise ClientInvokeError(f"redis client unknown error {exc} command {req.command} args {req.args}") from exc
|
|
173
|
+
return reply
|
|
174
|
+
|
|
175
|
+
async def _process_pubsub(self, ctx: TrpcContext, pool: redis.ConnectionPool):
|
|
176
|
+
"""
|
|
177
|
+
Process pubsub requests and return a pubsub object.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
ctx (TrpcContext): The context object.
|
|
181
|
+
pool (redis.ConnectionPool): The connection pool.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
str: An empty string (pubsub object is set in response).
|
|
185
|
+
"""
|
|
186
|
+
req: Request = ctx.msg.client_req_head
|
|
187
|
+
async with redis.Redis(connection_pool=pool) as conn:
|
|
188
|
+
pubsub = conn.pubsub()
|
|
189
|
+
await pubsub.subscribe(*req.args, **req.kwargs)
|
|
190
|
+
ctx.msg.set_client_rsp_head(Response(pubsub))
|
|
191
|
+
return ""
|
|
192
|
+
|
|
193
|
+
async def round_trip(self, ctx: TrpcContext, _: bytes, options: RoundTripOptions):
|
|
194
|
+
"""
|
|
195
|
+
Send a request to Redis and return the response or pipe object.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
ctx (TrpcContext): The client context.
|
|
199
|
+
_ (bytes): The request data (unused).
|
|
200
|
+
options (RoundTripOptions): The round trip options.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Any: The response or pipe object, depending on the request mode.
|
|
204
|
+
"""
|
|
205
|
+
msg = ctx.msg
|
|
206
|
+
req: Request = msg.client_req_head
|
|
207
|
+
address = options.address
|
|
208
|
+
redis_config = getattr(options, REDIS_NAME, {})
|
|
209
|
+
pool = await self.get_conn(address, options.password, redis_config)
|
|
210
|
+
if req.mode == RequestMode.PIPELINE:
|
|
211
|
+
return await self._process_pipeline(ctx, pool)
|
|
212
|
+
elif req.mode == RequestMode.COMMAND:
|
|
213
|
+
return await self._process_command(ctx, pool)
|
|
214
|
+
elif req.mode == RequestMode.PUBSUB:
|
|
215
|
+
return await self._process_pubsub(ctx, pool)
|
|
216
|
+
else:
|
|
217
|
+
raise ClientInvokeError(f"redis client unknown mode {req.mode}")
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _set_default(key: str, value: Any, redis_config: Dict, force: bool = False):
|
|
221
|
+
"""
|
|
222
|
+
Set a default value in the redis_config dictionary.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
key (str): The configuration key.
|
|
226
|
+
value (Any): The value to set.
|
|
227
|
+
redis_config (Dict): The configuration dictionary.
|
|
228
|
+
force (bool): If True, overwrite existing value.
|
|
229
|
+
"""
|
|
230
|
+
if force:
|
|
231
|
+
redis_config[key] = value
|
|
232
|
+
elif key not in redis_config:
|
|
233
|
+
redis_config[key] = value
|
|
234
|
+
|
|
235
|
+
async def get_conn(self, address: Tuple[str, int], password: Optional[str],
|
|
236
|
+
redis_config: Dict) -> redis.ConnectionPool:
|
|
237
|
+
"""
|
|
238
|
+
Get or create a Redis connection pool for the running event loop.
|
|
239
|
+
|
|
240
|
+
Lookup and creation do not await, so no lock is needed within one loop;
|
|
241
|
+
other threads use their own cache.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
address (Tuple[str, int]): The address tuple (host, port).
|
|
245
|
+
password (Optional[str]): The password for Redis.
|
|
246
|
+
redis_config (Dict): The Redis configuration dictionary. Not modified.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
redis.ConnectionPool: The connection pool object.
|
|
250
|
+
"""
|
|
251
|
+
redis_pool = self._loop_pools.for_loop(asyncio.get_running_loop())
|
|
252
|
+
key = f"{address[0]}:{address[1]}:{password or 'none'}"
|
|
253
|
+
conn_pool = redis_pool.get(key, None)
|
|
254
|
+
if conn_pool is not None:
|
|
255
|
+
return conn_pool
|
|
256
|
+
pool_config = dict(redis_config or {})
|
|
257
|
+
self._set_default(SOCKET_CONNECT_TIMEOUT, self.idle_timeout, pool_config)
|
|
258
|
+
self._set_default(MAX_CONNECTIONS, MAX_CONNECTIONS_DEFAULT, pool_config)
|
|
259
|
+
self._set_default(DECODE_RESPONSES, DECODE_RESPONSES_DEFAULT, pool_config)
|
|
260
|
+
self._set_default(HOST, address[0], pool_config, force=True)
|
|
261
|
+
self._set_default(PORT, address[1], pool_config, force=True)
|
|
262
|
+
if password is not None:
|
|
263
|
+
self._set_default(PASSWORD, password, pool_config, force=True)
|
|
264
|
+
conn_pool = redis.ConnectionPool(**pool_config)
|
|
265
|
+
redis_pool[key] = conn_pool
|
|
266
|
+
return conn_pool
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
async def get_response(pool: redis.ConnectionPool, req: Request) -> Any:
|
|
270
|
+
"""
|
|
271
|
+
Get the response data from Redis.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
pool (redis.ConnectionPool): The connection pool.
|
|
275
|
+
req (Request): The request object.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Any: The response data.
|
|
279
|
+
"""
|
|
280
|
+
async with redis.Redis(connection_pool=pool) as conn:
|
|
281
|
+
return await conn.execute_command(req.command, *req.args, **req.kwargs)
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
async def get_pipe(pool: redis.ConnectionPool) -> Any:
|
|
285
|
+
"""
|
|
286
|
+
Get a pipeline object from Redis.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
pool (redis.ConnectionPool): The connection pool.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Any: The pipeline object.
|
|
293
|
+
"""
|
|
294
|
+
async with redis.Redis(connection_pool=pool) as conn:
|
|
295
|
+
pipe = conn.pipeline()
|
|
296
|
+
return pipe
|
trpc_redis/_types.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making tRPC-Python-Database available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# tRPC-Python-Database is licensed under Apache-2.0.
|
|
6
|
+
"""
|
|
7
|
+
Redis request and response types for tRPC framework.
|
|
8
|
+
|
|
9
|
+
The client stores a :class:`Request` in ``msg.client_req_head`` and the transport replies with a
|
|
10
|
+
:class:`Response` in ``msg.client_rsp_head``.
|
|
11
|
+
"""
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
from typing import Dict
|
|
15
|
+
from typing import Optional
|
|
16
|
+
from typing import Sequence
|
|
17
|
+
|
|
18
|
+
from ._constants import RequestMode
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Request:
|
|
23
|
+
"""Mode and command payload for one Redis call.
|
|
24
|
+
|
|
25
|
+
``args`` and ``kwargs`` default to an empty tuple and dict. A single list
|
|
26
|
+
argument is unpacked, so ``args=[["a", "b"]]`` becomes ``("a", "b")``.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
mode: Command, pipeline, or pub/sub dispatch key.
|
|
30
|
+
command: Redis command name. Unused for pipeline and pub/sub.
|
|
31
|
+
args: Positional arguments for the command.
|
|
32
|
+
kwargs: Keyword arguments for the command.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
mode: RequestMode
|
|
36
|
+
command: Optional[str] = None
|
|
37
|
+
args: Optional[Sequence] = None
|
|
38
|
+
kwargs: Optional[Dict] = None
|
|
39
|
+
|
|
40
|
+
def __post_init__(self):
|
|
41
|
+
if self.args and len(self.args) == 1 and isinstance(self.args[0], list):
|
|
42
|
+
self.args = tuple(self.args[0])
|
|
43
|
+
self.args = self.args or tuple()
|
|
44
|
+
self.kwargs = self.kwargs or dict()
|
|
45
|
+
|
|
46
|
+
def set_pipeline(self, pipeline: bool):
|
|
47
|
+
"""
|
|
48
|
+
Set the request mode to pipeline or command.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
pipeline (bool): If True, set mode to PIPELINE; else COMMAND.
|
|
52
|
+
"""
|
|
53
|
+
self.mode = RequestMode.PIPELINE if pipeline else RequestMode.COMMAND
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Response:
|
|
58
|
+
"""Transport reply for one Redis call.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
reply: Command reply, or the ``Pipeline`` / ``PubSub`` object for
|
|
62
|
+
pipeline and pub/sub requests.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
reply: Any = None
|
trpc_redis/version.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making tRPC-Python-Database available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# tRPC-Python-Database is licensed under Apache-2.0.
|
|
6
|
+
"""
|
|
7
|
+
TRPC Redis Version Module
|
|
8
|
+
|
|
9
|
+
This module defines the version information for TRPC Redis.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = '1.0.0'
|