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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any