Skip to content

Commit bed0d54

Browse files
CahidArdaCopilot
andauthored
DX-2186: Add Redis stream commands (#66)
* feat: add Redis Stream commands and tests todo: fix skipped tests, improve response formating * fix: fmt * test: update stream tests * fix: update tests for script loading and xautoclaim functionality * fix: remove unsupported block parameter from xread and xreadgroup methods * Update upstash_redis/commands.pyi Co-authored-by: Copilot <[email protected]> * Update upstash_redis/commands.pyi Co-authored-by: Copilot <[email protected]> * Update upstash_redis/commands.pyi Co-authored-by: Copilot <[email protected]> * fix: update return types for xread and xreadgroup methods in Commands and AsyncCommands * fix: update return type for xread and xreadgroup methods in Commands and AsyncCommands * fix: update return type for xread and xreadgroup methods in Commands and AsyncCommands --------- Co-authored-by: Copilot <[email protected]>
1 parent 4e3a254 commit bed0d54

29 files changed

+4639
-3
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
---
2+
applyTo: '**'
3+
---
4+
# Adding New Redis Commands to Upstash Redis Python Package
5+
6+
## Package Structure Overview
7+
8+
The Upstash Redis Python package is organized as follows:
9+
10+
```
11+
upstash_redis/
12+
├── __init__.py # Package exports
13+
├── client.py # Synchronous Redis client
14+
├── commands.py # Command implementations
15+
├── commands.pyi # Type hints for commands
16+
├── errors.py # Custom exceptions
17+
├── format.py # Response formatters
18+
├── http.py # HTTP client for Redis REST API
19+
├── typing.py # Type definitions
20+
├── utils.py # Utility functions
21+
└── asyncio/
22+
├── __init__.py
23+
└── client.py # Asynchronous Redis client
24+
25+
tests/
26+
├── commands/ # Command-specific tests organized by category
27+
│ ├── hash/ # Hash command tests
28+
│ ├── json/ # JSON command tests
29+
│ ├── list/ # List command tests
30+
│ ├── set/ # Set command tests
31+
│ ├── sortedSet/ # Sorted set command tests
32+
│ ├── string/ # String command tests
33+
│ └── asyncio/ # Async versions of tests
34+
```
35+
36+
## Steps to Add a New Redis Command
37+
38+
### 1. Implement the Command in `commands.py`
39+
40+
Add your command method to the `Commands` class:
41+
42+
```python
43+
# filepath: upstash_redis/commands.py
44+
def your_new_command(self, key: str, *args, **kwargs) -> CommandsProtocol:
45+
"""
46+
Description of your command.
47+
48+
Args:
49+
key: The Redis key
50+
*args: Additional arguments
51+
**kwargs: Additional keyword arguments
52+
53+
Returns:
54+
CommandsProtocol: Command object for execution
55+
"""
56+
return self._execute_command("YOUR_REDIS_COMMAND", key, *args, **kwargs)
57+
```
58+
59+
### 2. Add Type Hints in `commands.pyi`
60+
61+
Update the type stub file with your command signature:
62+
63+
```python
64+
# filepath: upstash_redis/commands.pyi
65+
def your_new_command(self, key: str, *args, **kwargs) -> CommandsProtocol: ...
66+
```
67+
68+
### 3. Update Client Classes (if needed)
69+
70+
If your command requires special handling, update both sync and async clients:
71+
72+
```python
73+
# filepath: upstash_redis/client.py
74+
# Add any client-specific logic if needed
75+
76+
# filepath: upstash_redis/asyncio/client.py
77+
# Add async version if special handling is needed
78+
```
79+
80+
### 4. Add Response Formatting (if needed)
81+
82+
If your command returns data that needs special formatting, add a formatter in `format.py`:
83+
84+
```python
85+
# filepath: upstash_redis/format.py
86+
def format_your_command_response(response: Any) -> YourReturnType:
87+
"""Format the response from your Redis command."""
88+
# Implementation here
89+
pass
90+
```
91+
92+
### 5. Write Comprehensive Tests
93+
94+
Create test files in the appropriate category folder:
95+
96+
```python
97+
# filepath: tests/commands/{category}/test_your_new_command.py
98+
import pytest
99+
from upstash_redis import Redis
100+
101+
def test_your_new_command_basic():
102+
"""Test basic functionality of your new command."""
103+
redis = Redis.from_env()
104+
result = redis.your_new_command("test_key", "arg1", "arg2")
105+
# Add assertions
106+
107+
def test_your_new_command_edge_cases():
108+
"""Test edge cases and error conditions."""
109+
# Add edge case tests
110+
111+
# If async support is needed:
112+
# filepath: tests/commands/asyncio/test_your_new_command.py
113+
import pytest
114+
from upstash_redis.asyncio import Redis as AsyncRedis
115+
116+
@pytest.mark.asyncio
117+
async def test_your_new_command_async():
118+
"""Test async version of your new command."""
119+
redis = AsyncRedis.from_env()
120+
result = await redis.your_new_command("test_key", "arg1", "arg2")
121+
# Add assertions
122+
```
123+
124+
### 6. Update Package Exports (if needed)
125+
126+
If you're adding a new public class or function, update `__init__.py`:
127+
128+
```python
129+
# filepath: upstash_redis/__init__.py
130+
from upstash_redis.your_new_module import YourNewClass
131+
132+
__all__ = ["AsyncRedis", "Redis", "YourNewClass"]
133+
```
134+
135+
## Command Categories and Organization
136+
137+
Commands are typically organized into these categories:
138+
139+
- **String**: Basic key-value operations (`GET`, `SET`, etc.)
140+
- **Hash**: Hash field operations (`HGET`, `HSET`, etc.)
141+
- **List**: List operations (`LPUSH`, `RPOP`, etc.)
142+
- **Set**: Set operations (`SADD`, `SREM`, etc.)
143+
- **Sorted Set**: Sorted set operations (`ZADD`, `ZREM`, etc.)
144+
- **JSON**: JSON operations (`JSON.GET`, `JSON.SET`, etc.)
145+
- **Generic**: Key management (`DEL`, `EXISTS`, etc.)
146+
- **Server**: Server management commands
147+
148+
## Testing Guidelines
149+
150+
1. **Test file naming**: `test_{command_name}.py`
151+
2. **Test function naming**: `test_{command_name}_{scenario}`
152+
3. **Include both positive and negative test cases**
153+
4. **Test with different data types and edge cases**
154+
5. **Add async tests if the command supports async operations**
155+
6. **Use appropriate fixtures from `conftest.py`**
156+
157+
## Example: Adding a New Hash Command
158+
159+
Here's a complete example of adding a hypothetical `HMERGE` command:
160+
161+
```python
162+
# filepath: upstash_redis/commands.py
163+
def hmerge(self, key: str, source_key: str) -> CommandsProtocol:
164+
"""
165+
Merge hash from source_key into key.
166+
167+
Args:
168+
key: Destination hash key
169+
source_key: Source hash key to merge from
170+
171+
Returns:
172+
CommandsProtocol: Command for execution
173+
"""
174+
return self._execute_command("HMERGE", key, source_key)
175+
```
176+
177+
```python
178+
# filepath: tests/commands/hash/test_hmerge.py
179+
import pytest
180+
from upstash_redis import Redis
181+
182+
def test_hmerge_basic(redis_client):
183+
"""Test basic HMERGE functionality."""
184+
redis = redis_client
185+
186+
# Setup
187+
redis.hset("hash1", {"field1": "value1", "field2": "value2"})
188+
redis.hset("hash2", {"field3": "value3", "field2": "overwrite"})
189+
190+
# Execute
191+
result = redis.hmerge("hash1", "hash2")
192+
193+
# Verify
194+
merged_hash = redis.hgetall("hash1")
195+
assert merged_hash["field1"] == "value1"
196+
assert merged_hash["field2"] == "overwrite" # Should be overwritten
197+
assert merged_hash["field3"] == "value3" # Should be added
198+
```
199+
200+
## Running Tests
201+
202+
To run tests for your new command:
203+
204+
```bash
205+
# Run specific test file
206+
pytest tests/commands/hash/test_your_command.py -v
207+
208+
# Run all tests in a category
209+
pytest tests/commands/hash/ -v
210+
211+
# Run all tests
212+
pytest tests/ -v
213+
```
214+
215+
Follow this structure and you'll have a well-integrated Redis command that follows the package's conventions and patterns.

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,58 @@ redis.bitfield_ro("test_key_2") \
112112
.execute()
113113
```
114114

115+
### Redis Streams
116+
117+
Redis Streams provide a powerful data structure for handling real-time data. The SDK supports all stream commands:
118+
119+
```python
120+
from upstash_redis import Redis
121+
122+
redis = Redis.from_env()
123+
124+
# Add entries to a stream
125+
entry_id = redis.xadd("mystream", "*", {"field1": "value1", "field2": "value2"})
126+
print(f"Added entry: {entry_id}")
127+
128+
# Read from stream
129+
messages = redis.xread({"mystream": "0-0"})
130+
print(f"Messages: {messages}")
131+
132+
# Create consumer group
133+
redis.xgroup_create("mystream", "mygroup", "$")
134+
135+
# Read as part of consumer group
136+
messages = redis.xreadgroup("mygroup", "consumer1", {"mystream": ">"})
137+
138+
# Acknowledge processed messages
139+
if messages:
140+
message_ids = [msg[0] for msg in messages[0][1]]
141+
redis.xack("mystream", "mygroup", *message_ids)
142+
143+
# Get stream length
144+
length = redis.xlen("mystream")
145+
print(f"Stream length: {length}")
146+
```
147+
148+
For async usage:
149+
150+
```python
151+
from upstash_redis.asyncio import Redis
152+
153+
redis = Redis.from_env()
154+
155+
async def stream_example():
156+
# Add entries to a stream
157+
entry_id = await redis.xadd("mystream", "*", {"user": "alice", "action": "login"})
158+
159+
# Read from stream
160+
messages = await redis.xread({"mystream": "0-0"})
161+
162+
# Consumer group operations
163+
await redis.xgroup_create("mystream", "processors", "$")
164+
messages = await redis.xreadgroup("processors", "worker1", {"mystream": ">"})
165+
```
166+
115167
### Custom commands
116168
If you want to run a command that hasn't been implemented, you can use the `execute` function of your client instance
117169
and pass the command as a `list`.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "upstash-redis"
3-
version = "1.4.0"
3+
version = "1.5.0"
44
description = "Serverless Redis SDK from Upstash"
55
license = "MIT"
66
authors = ["Upstash <[email protected]>", "Zgîmbău Tudor <[email protected]>"]

tests/commands/asyncio/scripting/test_evalsha_ro.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ async def test_with_arguments(async_redis: Redis) -> None:
3232

3333
@mark.asyncio
3434
async def test_with_keys_and_arguments(async_redis: Redis) -> None:
35-
sha1_digest = await execute_on_http("SCRIPT", "LOAD", "return {ARGV[1], KEYS[1]}")
35+
# Load the script using the same Redis client instance
36+
sha1_digest = await async_redis.script_load("return {ARGV[1], KEYS[1]}")
3637

3738
assert isinstance(sha1_digest, str)
3839
assert await async_redis.evalsha_ro(sha1_digest, keys=["a"], args=["b"]) == [

tests/commands/asyncio/stream/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)