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
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: trpc-redis-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Redis client for the tRPC-Python framework
|
|
5
|
+
Author: tRPC Group
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Keywords: python,redis,trpc
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: redis>=6.2.0
|
|
18
|
+
Requires-Dist: trpc-python; sys_platform != 'win32'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# tRPC Redis Plugin
|
|
22
|
+
|
|
23
|
+
[中文](README.zh_CN.md)
|
|
24
|
+
|
|
25
|
+
A Redis client implementation for the tRPC Python framework, providing seamless Redis operations including commands, pipelines, and pub/sub functionality.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Command Mode**: Execute Redis commands directly
|
|
30
|
+
- **Pipeline Mode**: Batch multiple Redis operations
|
|
31
|
+
- **Pub/Sub Mode**: Subscribe to Redis channels and handle messages
|
|
32
|
+
- **Connection Pooling**: Efficient connection management
|
|
33
|
+
- **Configuration Support**: Flexible configuration via tRPC config or direct options
|
|
34
|
+
- **Async/Await Support**: Full async support for high-performance applications
|
|
35
|
+
- **Distributed Locks**: Built-in support for distributed locking
|
|
36
|
+
- **Transactions**: ACID-compliant transaction support
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Requirements:
|
|
41
|
+
- trpc-python (the `trpc_sdk` package)
|
|
42
|
+
- redis>=6.2.0
|
|
43
|
+
- Python >=3.9 (required by redis, recommended 3.10)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install trpc-redis>=0.1.0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For development with [uv](https://docs.astral.sh/uv/), run this in the package
|
|
50
|
+
directory. `trpc-python` must be resolvable from your configured package index:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv sync
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
### Basic Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from trpc_sdk.abc import TrpcContext
|
|
63
|
+
from trpc_sdk.client import Options
|
|
64
|
+
from trpc_redis import Client as RedisClient
|
|
65
|
+
|
|
66
|
+
# Initialize client
|
|
67
|
+
target = "ip://127.0.0.1:6379"
|
|
68
|
+
opts = [Options().set_target(target).set_password("xxx")]
|
|
69
|
+
redis_client = RedisClient("redis", opts)
|
|
70
|
+
|
|
71
|
+
async def main():
|
|
72
|
+
ctx = TrpcContext()
|
|
73
|
+
|
|
74
|
+
# Set a key
|
|
75
|
+
await redis_client.set(ctx, "key1", "value1")
|
|
76
|
+
|
|
77
|
+
# Get a key
|
|
78
|
+
value = await redis_client.get(ctx, "key1")
|
|
79
|
+
print(f"Value: {value}")
|
|
80
|
+
|
|
81
|
+
# Delete a key
|
|
82
|
+
result = await redis_client.delete(ctx, "key1")
|
|
83
|
+
print(f"Deleted: {result}")
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
asyncio.run(main())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Pipeline Operations
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
async def test_pipeline():
|
|
93
|
+
ctx = TrpcContext()
|
|
94
|
+
pipe = await redis_client.create_pipeline(ctx)
|
|
95
|
+
|
|
96
|
+
# Add multiple operations to pipeline
|
|
97
|
+
pipe.set(name="key1", value="value1")
|
|
98
|
+
pipe.set(name="key2", value="value2")
|
|
99
|
+
pipe.get("key1")
|
|
100
|
+
|
|
101
|
+
# Execute all operations
|
|
102
|
+
results = await pipe.execute()
|
|
103
|
+
print(f"Pipeline results: {results}")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Pub/Sub Operations
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
async def test_pubsub():
|
|
110
|
+
ctx = TrpcContext()
|
|
111
|
+
|
|
112
|
+
# Subscribe to channels
|
|
113
|
+
pubsub = await redis_client.create_pubsub(ctx, ["test_channel"])
|
|
114
|
+
|
|
115
|
+
# Listen for messages
|
|
116
|
+
async for message in pubsub.listen():
|
|
117
|
+
if message["type"] == "message":
|
|
118
|
+
print(f"Received: {message['data']}")
|
|
119
|
+
break # Exit after first message
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Dynamic Command Methods
|
|
123
|
+
|
|
124
|
+
The client supports dynamic method calls for any Redis command:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
async def test_dynamic_commands():
|
|
128
|
+
ctx = TrpcContext()
|
|
129
|
+
|
|
130
|
+
# These methods are dynamically created
|
|
131
|
+
await redis_client.hset(ctx, "hash_key", "field", "value")
|
|
132
|
+
await redis_client.lpush(ctx, "list_key", "item1", "item2")
|
|
133
|
+
await redis_client.zadd(ctx, "sorted_set", 1, "member1")
|
|
134
|
+
|
|
135
|
+
# Get results
|
|
136
|
+
hash_value = await redis_client.hget(ctx, "hash_key", "field")
|
|
137
|
+
list_length = await redis_client.llen(ctx, "list_key")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Advanced Operations
|
|
141
|
+
|
|
142
|
+
### Distributed Locks
|
|
143
|
+
|
|
144
|
+
#### Simple Lock Implementation
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
import time
|
|
148
|
+
import uuid
|
|
149
|
+
from trpc_sdk.exceptions import ClientInvokeError
|
|
150
|
+
|
|
151
|
+
class DistributedLock:
|
|
152
|
+
def __init__(self, redis_client, lock_name, timeout=10):
|
|
153
|
+
self.redis_client = redis_client
|
|
154
|
+
self.lock_name = f"lock:{lock_name}"
|
|
155
|
+
self.timeout = timeout
|
|
156
|
+
self.lock_value = str(uuid.uuid4())
|
|
157
|
+
|
|
158
|
+
async def acquire(self, ctx, ttl=30):
|
|
159
|
+
"""Acquire a distributed lock"""
|
|
160
|
+
# Use SET with NX and EX for atomic lock acquisition
|
|
161
|
+
result = await self.redis_client.set(
|
|
162
|
+
ctx, self.lock_name, self.lock_value,
|
|
163
|
+
ex=ttl, nx=True
|
|
164
|
+
)
|
|
165
|
+
return result is True
|
|
166
|
+
|
|
167
|
+
async def release(self, ctx):
|
|
168
|
+
"""Release the lock using Lua script for atomicity"""
|
|
169
|
+
lua_script = """
|
|
170
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
171
|
+
return redis.call("del", KEYS[1])
|
|
172
|
+
else
|
|
173
|
+
return 0
|
|
174
|
+
end
|
|
175
|
+
"""
|
|
176
|
+
result = await self.redis_client.eval(
|
|
177
|
+
ctx, lua_script, 1, self.lock_name, self.lock_value
|
|
178
|
+
)
|
|
179
|
+
return result == 1
|
|
180
|
+
|
|
181
|
+
async def __aenter__(self):
|
|
182
|
+
ctx = TrpcContext()
|
|
183
|
+
acquired = await self.acquire(ctx)
|
|
184
|
+
if not acquired:
|
|
185
|
+
raise TimeoutError(f"Failed to acquire lock: {self.lock_name}")
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
189
|
+
ctx = TrpcContext()
|
|
190
|
+
await self.release(ctx)
|
|
191
|
+
|
|
192
|
+
# Usage example
|
|
193
|
+
async def critical_section():
|
|
194
|
+
lock = DistributedLock(redis_client, "my_resource")
|
|
195
|
+
async with lock:
|
|
196
|
+
# This code is protected by the distributed lock
|
|
197
|
+
await redis_client.incr(ctx, "counter")
|
|
198
|
+
await asyncio.sleep(1) # Simulate work
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### Lock with Retry Logic
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
async def acquire_lock_with_retry(redis_client, lock_name, max_retries=5, retry_delay=1):
|
|
205
|
+
"""Acquire lock with retry logic"""
|
|
206
|
+
for attempt in range(max_retries):
|
|
207
|
+
lock = DistributedLock(redis_client, lock_name)
|
|
208
|
+
try:
|
|
209
|
+
ctx = TrpcContext()
|
|
210
|
+
if await lock.acquire(ctx):
|
|
211
|
+
return lock
|
|
212
|
+
except Exception as e:
|
|
213
|
+
print(f"Lock acquisition attempt {attempt + 1} failed: {e}")
|
|
214
|
+
|
|
215
|
+
if attempt < max_retries - 1:
|
|
216
|
+
await asyncio.sleep(retry_delay)
|
|
217
|
+
|
|
218
|
+
raise TimeoutError(f"Failed to acquire lock after {max_retries} attempts")
|
|
219
|
+
|
|
220
|
+
# Usage
|
|
221
|
+
async def safe_operation():
|
|
222
|
+
lock = await acquire_lock_with_retry(redis_client, "shared_resource")
|
|
223
|
+
try:
|
|
224
|
+
# Perform critical operation
|
|
225
|
+
ctx = TrpcContext()
|
|
226
|
+
await redis_client.incr(ctx, "shared_counter")
|
|
227
|
+
finally:
|
|
228
|
+
ctx = TrpcContext()
|
|
229
|
+
await lock.release(ctx)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Transactions
|
|
233
|
+
|
|
234
|
+
#### Basic Transaction
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
async def basic_transaction():
|
|
238
|
+
ctx = TrpcContext()
|
|
239
|
+
|
|
240
|
+
# Start a transaction
|
|
241
|
+
pipe = await redis_client.create_pipeline(ctx)
|
|
242
|
+
|
|
243
|
+
# Watch keys for optimistic locking
|
|
244
|
+
await pipe.watch("account:1", "account:2")
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
# Get current values
|
|
248
|
+
balance1 = await redis_client.get(ctx, "account:1")
|
|
249
|
+
balance2 = await redis_client.get(ctx, "account:2")
|
|
250
|
+
|
|
251
|
+
# Parse balances (assuming they're stored as strings)
|
|
252
|
+
balance1 = int(balance1) if balance1 else 0
|
|
253
|
+
balance2 = int(balance2) if balance2 else 0
|
|
254
|
+
|
|
255
|
+
# Start transaction
|
|
256
|
+
pipe.multi()
|
|
257
|
+
|
|
258
|
+
# Perform operations
|
|
259
|
+
pipe.set("account:1", balance1 - 100)
|
|
260
|
+
pipe.set("account:2", balance2 + 100)
|
|
261
|
+
|
|
262
|
+
# Execute transaction
|
|
263
|
+
results = await pipe.execute()
|
|
264
|
+
print(f"Transaction successful: {results}")
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
# Transaction failed, keys were modified
|
|
268
|
+
print(f"Transaction failed: {e}")
|
|
269
|
+
await pipe.reset()
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Optimistic Locking with Version
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
async def optimistic_transaction():
|
|
276
|
+
ctx = TrpcContext()
|
|
277
|
+
|
|
278
|
+
# Get current data with version
|
|
279
|
+
user_data = await redis_client.hgetall(ctx, "user:123")
|
|
280
|
+
current_version = int(user_data.get("version", "0"))
|
|
281
|
+
|
|
282
|
+
# Start transaction
|
|
283
|
+
pipe = await redis_client.create_pipeline(ctx)
|
|
284
|
+
await pipe.watch("user:123")
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
# Check version hasn't changed
|
|
288
|
+
pipe.multi()
|
|
289
|
+
pipe.hset("user:123", "name", "new_name")
|
|
290
|
+
pipe.hset("user:123", "version", current_version + 1)
|
|
291
|
+
|
|
292
|
+
results = await pipe.execute()
|
|
293
|
+
print(f"Update successful: {results}")
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
print(f"Optimistic lock failed: {e}")
|
|
297
|
+
await pipe.reset()
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Conditional Transaction
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
async def conditional_transaction():
|
|
304
|
+
ctx = TrpcContext()
|
|
305
|
+
|
|
306
|
+
# Check if we can perform the operation
|
|
307
|
+
current_stock = await redis_client.get(ctx, "product:stock")
|
|
308
|
+
current_stock = int(current_stock) if current_stock else 0
|
|
309
|
+
|
|
310
|
+
if current_stock < 1:
|
|
311
|
+
raise ValueError("Insufficient stock")
|
|
312
|
+
|
|
313
|
+
# Start transaction
|
|
314
|
+
pipe = await redis_client.create_pipeline(ctx)
|
|
315
|
+
await pipe.watch("product:stock")
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
pipe.multi()
|
|
319
|
+
pipe.decr("product:stock")
|
|
320
|
+
pipe.incr("product:sold")
|
|
321
|
+
|
|
322
|
+
results = await pipe.execute()
|
|
323
|
+
print(f"Purchase successful: {results}")
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
print(f"Transaction failed: {e}")
|
|
327
|
+
await pipe.reset()
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### Batch Operations with Transaction
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
async def batch_transaction():
|
|
334
|
+
ctx = TrpcContext()
|
|
335
|
+
|
|
336
|
+
# Prepare batch operations
|
|
337
|
+
operations = [
|
|
338
|
+
("set", "key1", "value1"),
|
|
339
|
+
("set", "key2", "value2"),
|
|
340
|
+
("incr", "counter"),
|
|
341
|
+
("lpush", "list", "item1"),
|
|
342
|
+
("lpush", "list", "item2"),
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
# Execute in transaction
|
|
346
|
+
pipe = await redis_client.create_pipeline(ctx)
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
pipe.multi()
|
|
350
|
+
|
|
351
|
+
for op, *args in operations:
|
|
352
|
+
getattr(pipe, op)(*args)
|
|
353
|
+
|
|
354
|
+
results = await pipe.execute()
|
|
355
|
+
print(f"Batch transaction results: {results}")
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
print(f"Batch transaction failed: {e}")
|
|
359
|
+
await pipe.reset()
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Lock and Transaction Combined
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
async def complex_operation():
|
|
366
|
+
"""Combine distributed lock with transaction for complex operations"""
|
|
367
|
+
|
|
368
|
+
# Acquire distributed lock
|
|
369
|
+
lock = DistributedLock(redis_client, "complex_operation")
|
|
370
|
+
ctx = TrpcContext()
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
if not await lock.acquire(ctx):
|
|
374
|
+
raise TimeoutError("Failed to acquire lock")
|
|
375
|
+
|
|
376
|
+
# Start transaction within lock
|
|
377
|
+
pipe = await redis_client.create_pipeline(ctx)
|
|
378
|
+
await pipe.watch("shared_resource")
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
# Get current state
|
|
382
|
+
current_value = await redis_client.get(ctx, "shared_resource")
|
|
383
|
+
current_value = int(current_value) if current_value else 0
|
|
384
|
+
|
|
385
|
+
# Perform complex operation
|
|
386
|
+
pipe.multi()
|
|
387
|
+
pipe.set("shared_resource", current_value + 1)
|
|
388
|
+
pipe.set("last_updated", time.time())
|
|
389
|
+
pipe.incr("operation_count")
|
|
390
|
+
|
|
391
|
+
results = await pipe.execute()
|
|
392
|
+
print(f"Complex operation completed: {results}")
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
print(f"Transaction failed: {e}")
|
|
396
|
+
await pipe.reset()
|
|
397
|
+
|
|
398
|
+
finally:
|
|
399
|
+
# Always release the lock
|
|
400
|
+
await lock.release(ctx)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Configuration
|
|
404
|
+
|
|
405
|
+
### Direct Configuration
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from trpc_sdk.client import Options
|
|
409
|
+
from trpc_redis import Client as RedisClient
|
|
410
|
+
|
|
411
|
+
opts = [
|
|
412
|
+
Options()
|
|
413
|
+
.set_target("ip://127.0.0.1:6379")
|
|
414
|
+
.set_password("your_password")
|
|
415
|
+
.set_timeout(5000) # 5 seconds
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
redis_client = RedisClient("redis", opts)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Configuration File
|
|
422
|
+
|
|
423
|
+
Create a YAML configuration file:
|
|
424
|
+
|
|
425
|
+
```yaml
|
|
426
|
+
client:
|
|
427
|
+
service:
|
|
428
|
+
- name: trpc.redis.test_example
|
|
429
|
+
namespace: ${namespace}
|
|
430
|
+
target: "ip://127.0.0.1:6379"
|
|
431
|
+
password: "test"
|
|
432
|
+
timeout: 5000
|
|
433
|
+
redis:
|
|
434
|
+
max_connections: 1024
|
|
435
|
+
decode_responses: false
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Then initialize the client:
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
from trpc_redis import Client as RedisClient
|
|
442
|
+
|
|
443
|
+
# Client will automatically load configuration
|
|
444
|
+
redis_client = RedisClient("trpc.redis.test_example")
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Advanced Usage
|
|
448
|
+
|
|
449
|
+
### Error Handling
|
|
450
|
+
|
|
451
|
+
```python
|
|
452
|
+
from trpc_sdk.exceptions import ClientTimeoutError, ClientInvokeError
|
|
453
|
+
|
|
454
|
+
async def safe_redis_operation():
|
|
455
|
+
ctx = TrpcContext()
|
|
456
|
+
try:
|
|
457
|
+
result = await redis_client.set(ctx, "key", "value")
|
|
458
|
+
return result
|
|
459
|
+
except ClientTimeoutError:
|
|
460
|
+
print("Operation timed out")
|
|
461
|
+
except ClientInvokeError as e:
|
|
462
|
+
print(f"Redis operation failed: {e}")
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Connection Pool Management
|
|
466
|
+
|
|
467
|
+
Connection pools are keyed by Redis host, port, and password. Clients in the
|
|
468
|
+
same thread and event loop that point at the same server share one pool:
|
|
469
|
+
|
|
470
|
+
```python
|
|
471
|
+
client1 = RedisClient("redis", opts)
|
|
472
|
+
client2 = RedisClient("redis", opts)
|
|
473
|
+
# client1 and client2 share a pool while they run on the same event loop.
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Process and Thread Models
|
|
477
|
+
|
|
478
|
+
A `redis.asyncio` pool is bound to the event loop it first runs on. The
|
|
479
|
+
transport therefore keeps one cache per process, per thread, and per event
|
|
480
|
+
loop, and works in every `global.processmodel` setting of tRPC-Python:
|
|
481
|
+
|
|
482
|
+
| Mode | Where Redis calls run |
|
|
483
|
+
| --- | --- |
|
|
484
|
+
| merge, one loop (default) | The main thread of the single process. |
|
|
485
|
+
| merge, `handle_num > 1` | Each serving thread, on its own loop and pools. |
|
|
486
|
+
| merge, `io_handle_type: separate` | Each handler thread, on its own loop and pools. |
|
|
487
|
+
| `multi_process` + `separate` | Each rpc task process. The I/O process does not call Redis. |
|
|
488
|
+
| `multi_process` + `peer` | Each peer worker process. |
|
|
489
|
+
|
|
490
|
+
Notes:
|
|
491
|
+
|
|
492
|
+
- A forked child starts with an empty cache, so building a `RedisClient` at
|
|
493
|
+
import time, before the server forks, is fine.
|
|
494
|
+
- A `Pipeline` or `PubSub` object belongs to the loop that created it. Use it
|
|
495
|
+
on that loop only; do not hand it to another thread.
|
|
496
|
+
- The YAML `redis:` block is copied for each pool and is never modified.
|
|
497
|
+
- Closing an event loop drops its pools from the cache. Pools are not
|
|
498
|
+
disconnected on shutdown; process exit closes their sockets.
|
|
499
|
+
|
|
500
|
+
`examples/` has one config per mode. With Redis on `127.0.0.1:6379`:
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
cd examples
|
|
504
|
+
./run_examples.sh service trpc_python_peer.yaml # or any config below
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
The same steps by hand, in two terminals:
|
|
508
|
+
|
|
509
|
+
```bash
|
|
510
|
+
cd examples
|
|
511
|
+
python trpc_main.py -c trpc_python_peer.yaml
|
|
512
|
+
python test_service_rpc.py -n 50 # prints the pid/thread spread
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
See [examples/README.md](examples/README.md) for the client script and every config.
|
|
516
|
+
|
|
517
|
+
| Config | `global.processmodel` |
|
|
518
|
+
| --- | --- |
|
|
519
|
+
| `trpc_python.yaml` | none (merge, one loop) |
|
|
520
|
+
| `trpc_python_merge_threads.yaml` | `single_process`, `handle_num: 2` |
|
|
521
|
+
| `trpc_python_separate_thread.yaml` | `single_process`, `io_handle_type: separate`, `io_num: 1`, `handle_num: 2` |
|
|
522
|
+
| `trpc_python_separate.yaml` | `multi_process`, `io_handle_type: separate`, `handle_num: 2` |
|
|
523
|
+
| `trpc_python_peer.yaml` | `multi_process`, `io_handle_type: peer`, `handle_num: 2` |
|
|
524
|
+
|
|
525
|
+
On macOS, `SO_REUSEPORT` does not balance connections, so in peer mode one
|
|
526
|
+
worker may receive all of them.
|
|
527
|
+
|
|
528
|
+
## Project Structure
|
|
529
|
+
|
|
530
|
+
```
|
|
531
|
+
trpc-redis/
|
|
532
|
+
├── trpc_redis/
|
|
533
|
+
│ ├── __init__.py # Public re-exports
|
|
534
|
+
│ ├── version.py # Package version, read by hatch and setup.py
|
|
535
|
+
│ ├── _client.py # Client implementation
|
|
536
|
+
│ ├── _transport.py # Transport for Redis operations
|
|
537
|
+
│ ├── _codec.py # Message encoding/decoding
|
|
538
|
+
│ ├── _constants.py # Configuration constants and RequestMode
|
|
539
|
+
│ └── _types.py # Request / Response types
|
|
540
|
+
├── examples/
|
|
541
|
+
│ ├── README.md # How to run the examples
|
|
542
|
+
│ ├── run_examples.sh # Start the server, run a client, stop the server
|
|
543
|
+
│ ├── redis_client.py # Standalone usage examples
|
|
544
|
+
│ ├── trpc_main.py # Example server whose Greeter calls Redis
|
|
545
|
+
│ ├── greeter.py # Greeter implementation
|
|
546
|
+
│ ├── test_service_rpc.py # Concurrent Greeter client
|
|
547
|
+
│ ├── trpc_python*.yaml # One config per process/thread model
|
|
548
|
+
│ └── stub/ # Generated Greeter stub
|
|
549
|
+
├── tests/
|
|
550
|
+
│ ├── test_codec.py # Codec tests
|
|
551
|
+
│ ├── test_redis_client.py # Client tests
|
|
552
|
+
│ └── test_transport.py # Transport tests
|
|
553
|
+
├── pyproject.toml # Package metadata and uv dev dependencies
|
|
554
|
+
├── setup.py # Legacy setuptools entry point
|
|
555
|
+
├── README.md
|
|
556
|
+
└── README.zh_CN.md
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## API Reference
|
|
560
|
+
|
|
561
|
+
### Client Class
|
|
562
|
+
|
|
563
|
+
#### `__init__(name: str, opts: Optional[List] = None)`
|
|
564
|
+
Initialize a Redis client.
|
|
565
|
+
|
|
566
|
+
- `name`: Service name for the Redis instance
|
|
567
|
+
- `opts`: Optional list of configuration options
|
|
568
|
+
|
|
569
|
+
#### `execute_command(ctx: TrpcContext, cmd: str, *args) -> bytes`
|
|
570
|
+
Execute a Redis command.
|
|
571
|
+
|
|
572
|
+
- `ctx`: tRPC context
|
|
573
|
+
- `cmd`: Redis command name
|
|
574
|
+
- `*args`: Command arguments
|
|
575
|
+
|
|
576
|
+
#### `create_pipeline(ctx: Context) -> Pipeline`
|
|
577
|
+
Create a Redis pipeline for batch operations.
|
|
578
|
+
|
|
579
|
+
#### `create_pubsub(ctx: TrpcContext, *channels) -> PubSub`
|
|
580
|
+
Create a Redis pub/sub subscription.
|
|
581
|
+
|
|
582
|
+
### Request Modes
|
|
583
|
+
|
|
584
|
+
- `RequestMode.COMMAND`: Standard command execution
|
|
585
|
+
- `RequestMode.PIPELINE`: Pipeline operations
|
|
586
|
+
- `RequestMode.PUBSUB`: Publish/subscribe operations
|
|
587
|
+
|
|
588
|
+
## Testing
|
|
589
|
+
|
|
590
|
+
Run the test suite:
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
pytest tests/
|
|
594
|
+
# or, with uv
|
|
595
|
+
uv run pytest tests/
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
For coverage:
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
pytest --cov=trpc_redis tests/
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
## Examples
|
|
605
|
+
|
|
606
|
+
Runnable examples and the one-shot script are described in [examples/README.md](examples/README.md):
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
cd examples
|
|
610
|
+
./run_examples.sh list
|
|
611
|
+
./run_examples.sh client
|
|
612
|
+
./run_examples.sh service
|
|
613
|
+
./run_examples.sh all
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Covered there:
|
|
617
|
+
|
|
618
|
+
- Basic Redis operations
|
|
619
|
+
- Pipeline usage
|
|
620
|
+
- Pub/Sub patterns
|
|
621
|
+
- Error handling
|
|
622
|
+
- Configuration examples
|
|
623
|
+
- Distributed locks
|
|
624
|
+
- Transaction patterns
|
|
625
|
+
- A Greeter server for each process/thread model (see [Process and Thread Models](#process-and-thread-models))
|
|
626
|
+
|
|
627
|
+
## Contributing
|
|
628
|
+
|
|
629
|
+
1. Fork the repository
|
|
630
|
+
2. Create a feature branch
|
|
631
|
+
3. Make your changes
|
|
632
|
+
4. Add tests for new functionality
|
|
633
|
+
5. Submit a pull request
|
|
634
|
+
|
|
635
|
+
## License
|
|
636
|
+
|
|
637
|
+
Copyright @ 2025 Tencent.com
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
trpc_redis/__init__.py,sha256=G0vcdIS-GESfheuBgxUyspq_fdP2huux3bdE4c228Q4,812
|
|
2
|
+
trpc_redis/_client.py,sha256=4StrttbSjlZUmRcYWaeQBaj8dXx5UI9Cimb5HkT9CYA,8451
|
|
3
|
+
trpc_redis/_codec.py,sha256=9bJV3tcw5XiuA_HCxe2QEejCsjCkG_rd4jgd4Yo8W4o,2530
|
|
4
|
+
trpc_redis/_constants.py,sha256=GcdBLm_qMxiXxY15DjpWFqPDiMMAFFm-fIFHVll3jAU,1841
|
|
5
|
+
trpc_redis/_transport.py,sha256=HR1fOYDakuLJNijwDWk_TDs6W27Z3HnH6yQzNPVGeAI,11125
|
|
6
|
+
trpc_redis/_types.py,sha256=FNLpb083-il0EV0u1Yp4FJtqY5MUF_yVxCDo_Z7m1tQ,1970
|
|
7
|
+
trpc_redis/version.py,sha256=JvbsaXm_qRRakTEH_AreiyOzG1Igwhbf-gbDvuDKFv8,326
|
|
8
|
+
trpc_redis_python-1.0.0.dist-info/METADATA,sha256=edzdfipF2oE4JKfVKU2a921d6qHC2VVy5AQ_jHR4oI8,18265
|
|
9
|
+
trpc_redis_python-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
10
|
+
trpc_redis_python-1.0.0.dist-info/RECORD,,
|