Skip to content

Commit 9921d02

Browse files
committed
✨ feat: add list and hash
1 parent 275f4e2 commit 9921d02

File tree

5 files changed

+338
-0
lines changed

5 files changed

+338
-0
lines changed

src/use_redis/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from .stream import RedisStreamMessage
33
from .stream import RedisStreamStore as useRedisStream
44
from .set import RedisSetStore as useRedisSet
5+
from .list import RedisListStore as useRedisList
6+
from .hash import RedisHashStore as useRedisHash
57

68
useRedisStreamStore = useRedisStream
79
useRedisStore = useRedis
@@ -13,4 +15,6 @@
1315
"useRedisStreamStore",
1416
"RedisStreamMessage",
1517
"useRedisStream",
18+
"useRedisList",
19+
"useRedisHash",
1620
]

src/use_redis/hash.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from .store import RedisStore
2+
3+
4+
class RedisHashStore(RedisStore):
5+
def __init__(self, key, **kwargs):
6+
super().__init__(**kwargs)
7+
self._key = key
8+
9+
@property
10+
def key(self):
11+
return self._key
12+
13+
def set(self, field, value):
14+
"""设置哈希表中指定字段的值"""
15+
return self.connection.hset(self.key, field, value)
16+
17+
def set_multiple(self, mapping):
18+
"""同时将多个 field-value 对设置到哈希表中"""
19+
return self.connection.hmset(self.key, mapping)
20+
21+
def get(self, *fields):
22+
"""获取哈希表中指定字段的值"""
23+
if len(fields) == 1:
24+
return self.connection.hget(self.key, fields[0])
25+
return self.connection.hmget(self.key, fields)
26+
27+
def delete(self, *fields):
28+
"""删除哈希表中的一个或多个字段"""
29+
return self.connection.hdel(self.key, *fields)
30+
31+
def exists(self, field):
32+
"""检查哈希表中是否存在指定的字段"""
33+
return self.connection.hexists(self.key, field)
34+
35+
def length(self):
36+
"""获取哈希表中字段的数量"""
37+
return self.connection.hlen(self.key)
38+
39+
def keys(self):
40+
"""获取哈希表中的所有字段"""
41+
return self.connection.hkeys(self.key)
42+
43+
def values(self):
44+
"""获取哈希表中所有字段的值"""
45+
return self.connection.hvals(self.key)
46+
47+
def items(self):
48+
"""获取哈希表中所有的字段和值"""
49+
return self.connection.hgetall(self.key)
50+
51+
def increment(self, field, amount=1):
52+
"""
53+
将哈希表中指定字段的值增加给定的增量
54+
55+
:param field: 要增加的字段
56+
:param amount: 增加的数量,可以是整数或浮点数
57+
:return: 增加后的值
58+
"""
59+
if isinstance(amount, int):
60+
return self.connection.hincrby(self.key, field, amount)
61+
else:
62+
return self.connection.hincrbyfloat(self.key, field, amount)
63+
64+
65+
def scan(self, cursor=0, match=None, count=None):
66+
"""迭代哈希表中的键值对"""
67+
return self.connection.hscan(self.key, cursor, match, count)
68+
69+
def __getattr__(self, name):
70+
"""动态处理未实现的方法"""
71+
def method(*args, **kwargs):
72+
redis_method = getattr(self.connection, name)
73+
return redis_method(self.key, *args, **kwargs)
74+
return method

src/use_redis/list.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from .store import RedisStore
2+
3+
4+
class RedisListStore(RedisStore):
5+
def __init__(self, key, **kwargs):
6+
super().__init__(**kwargs)
7+
self._key = key
8+
9+
@property
10+
def key(self):
11+
return self._key
12+
13+
def lpush(self, *values):
14+
"""将一个或多个值插入到列表头部"""
15+
return self.connection.lpush(self.key, *values)
16+
17+
def rpush(self, *values):
18+
"""将一个或多个值插入到列表尾部"""
19+
return self.connection.rpush(self.key, *values)
20+
21+
def lpop(self):
22+
"""移除并返回列表的第一个元素"""
23+
return self.connection.lpop(self.key)
24+
25+
def rpop(self):
26+
"""移除并返回列表的最后一个元素"""
27+
return self.connection.rpop(self.key)
28+
29+
def length(self):
30+
"""返回列表的长度"""
31+
return self.connection.llen(self.key)
32+
33+
def range(self, start, end):
34+
"""获取列表指定范围内的元素"""
35+
return self.connection.lrange(self.key, start, end)
36+
37+
def set(self, index, value):
38+
"""通过索引设置列表元素的值"""
39+
return self.connection.lset(self.key, index, value)
40+
41+
def index(self, index):
42+
"""通过索引获取列表中的元素"""
43+
return self.connection.lindex(self.key, index)
44+
45+
def trim(self, start, end):
46+
"""修剪列表,只保留指定区间内的元素"""
47+
return self.connection.ltrim(self.key, start, end)
48+
49+
def remove(self, value, count=0):
50+
"""移除列表中与value相等的元素"""
51+
return self.connection.lrem(self.key, count, value)
52+
53+
def insert_before(self, pivot, value):
54+
"""在列表的元素前插入元素"""
55+
return self.connection.linsert(self.key, 'BEFORE', pivot, value)
56+
57+
def insert_after(self, pivot, value):
58+
"""在列表的元素后插入元素"""
59+
return self.connection.linsert(self.key, 'AFTER', pivot, value)
60+
61+
def __getattr__(self, name):
62+
"""动态处理未实现的方法"""
63+
64+
def method(*args, **kwargs):
65+
redis_method = getattr(self.connection, name)
66+
return redis_method(self.key, *args, **kwargs)
67+
68+
return method

tests/test_hash.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import pytest
2+
from use_redis.hash import RedisHashStore
3+
4+
5+
@pytest.fixture
6+
def mock_redis(mocker):
7+
mock = mocker.patch("redis.Redis")
8+
return mock.return_value
9+
10+
11+
@pytest.fixture
12+
def hash_store(mock_redis):
13+
return RedisHashStore("test_hash")
14+
15+
16+
def test_set(hash_store, mock_redis):
17+
hash_store.set("field1", "value1")
18+
mock_redis.hset.assert_called_once_with("test_hash", "field1", "value1")
19+
20+
21+
def test_set_multiple(hash_store, mock_redis):
22+
mapping = {"field1": "value1", "field2": "value2"}
23+
hash_store.set_multiple(mapping)
24+
mock_redis.hmset.assert_called_once_with("test_hash", mapping)
25+
26+
27+
def test_get_single(hash_store, mock_redis):
28+
mock_redis.hget.return_value = "value1"
29+
result = hash_store.get("field1")
30+
assert result == "value1"
31+
mock_redis.hget.assert_called_once_with("test_hash", "field1")
32+
33+
34+
def test_get_multiple(hash_store, mock_redis):
35+
mock_redis.hmget.return_value = ["value1", "value2"]
36+
result = hash_store.get("field1", "field2")
37+
assert result == ["value1", "value2"]
38+
mock_redis.hmget.assert_called_once_with("test_hash", ("field1", "field2"))
39+
40+
41+
def test_delete(hash_store, mock_redis):
42+
hash_store.delete("field1", "field2")
43+
mock_redis.hdel.assert_called_once_with("test_hash", "field1", "field2")
44+
45+
46+
def test_exists(hash_store, mock_redis):
47+
mock_redis.hexists.return_value = True
48+
result = hash_store.exists("field1")
49+
assert result is True
50+
mock_redis.hexists.assert_called_once_with("test_hash", "field1")
51+
52+
53+
def test_length(hash_store, mock_redis):
54+
mock_redis.hlen.return_value = 2
55+
result = hash_store.length()
56+
assert result == 2
57+
mock_redis.hlen.assert_called_once_with("test_hash")
58+
59+
60+
def test_keys(hash_store, mock_redis):
61+
mock_redis.hkeys.return_value = ["field1", "field2"]
62+
result = hash_store.keys()
63+
assert result == ["field1", "field2"]
64+
mock_redis.hkeys.assert_called_once_with("test_hash")
65+
66+
67+
def test_values(hash_store, mock_redis):
68+
mock_redis.hvals.return_value = ["value1", "value2"]
69+
result = hash_store.values()
70+
assert result == ["value1", "value2"]
71+
mock_redis.hvals.assert_called_once_with("test_hash")
72+
73+
74+
def test_items(hash_store, mock_redis):
75+
mock_redis.hgetall.return_value = {"field1": "value1", "field2": "value2"}
76+
result = hash_store.items()
77+
assert result == {"field1": "value1", "field2": "value2"}
78+
mock_redis.hgetall.assert_called_once_with("test_hash")
79+
80+
81+
def test_increment_int(hash_store, mock_redis):
82+
mock_redis.hincrby.return_value = 2
83+
result = hash_store.increment("field1", 1)
84+
assert result == 2
85+
mock_redis.hincrby.assert_called_once_with("test_hash", "field1", 1)
86+
87+
88+
def test_increment_float(hash_store, mock_redis):
89+
mock_redis.hincrbyfloat.return_value = 2.5
90+
result = hash_store.increment("field1", 1.5)
91+
assert result == 2.5
92+
mock_redis.hincrbyfloat.assert_called_once_with("test_hash", "field1", 1.5)
93+
94+
95+
def test_scan(hash_store, mock_redis):
96+
mock_redis.hscan.return_value = (0, {"field1": "value1", "field2": "value2"})
97+
result = hash_store.scan(cursor=0, match="f*", count=2)
98+
assert result == (0, {"field1": "value1", "field2": "value2"})
99+
mock_redis.hscan.assert_called_once_with("test_hash", 0, "f*", 2)
100+
101+
102+
def test_dynamic_method(hash_store, mock_redis):
103+
hash_store.hstrlen("field1")
104+
mock_redis.hstrlen.assert_called_once_with("test_hash", "field1")

tests/test_list.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import pytest
2+
from use_redis.list import RedisListStore
3+
4+
5+
@pytest.fixture
6+
def mock_redis(mocker):
7+
mock = mocker.patch("redis.Redis")
8+
return mock.return_value
9+
10+
11+
@pytest.fixture
12+
def list_store(mock_redis):
13+
return RedisListStore("test_list")
14+
15+
16+
def test_lpush(list_store, mock_redis):
17+
list_store.lpush("value1", "value2")
18+
mock_redis.lpush.assert_called_once_with("test_list", "value1", "value2")
19+
20+
21+
def test_rpush(list_store, mock_redis):
22+
list_store.rpush("value1", "value2")
23+
mock_redis.rpush.assert_called_once_with("test_list", "value1", "value2")
24+
25+
26+
def test_lpop(list_store, mock_redis):
27+
mock_redis.lpop.return_value = "value1"
28+
result = list_store.lpop()
29+
assert result == "value1"
30+
mock_redis.lpop.assert_called_once_with("test_list")
31+
32+
33+
def test_rpop(list_store, mock_redis):
34+
mock_redis.rpop.return_value = "value2"
35+
result = list_store.rpop()
36+
assert result == "value2"
37+
mock_redis.rpop.assert_called_once_with("test_list")
38+
39+
40+
def test_length(list_store, mock_redis):
41+
mock_redis.llen.return_value = 2
42+
result = list_store.length()
43+
assert result == 2
44+
mock_redis.llen.assert_called_once_with("test_list")
45+
46+
47+
def test_range(list_store, mock_redis):
48+
mock_redis.lrange.return_value = ["value1", "value2"]
49+
result = list_store.range(0, -1)
50+
assert result == ["value1", "value2"]
51+
mock_redis.lrange.assert_called_once_with("test_list", 0, -1)
52+
53+
54+
def test_set(list_store, mock_redis):
55+
list_store.set(1, "new_value")
56+
mock_redis.lset.assert_called_once_with("test_list", 1, "new_value")
57+
58+
59+
def test_index(list_store, mock_redis):
60+
mock_redis.lindex.return_value = "value1"
61+
result = list_store.index(0)
62+
assert result == "value1"
63+
mock_redis.lindex.assert_called_once_with("test_list", 0)
64+
65+
66+
def test_trim(list_store, mock_redis):
67+
list_store.trim(0, 1)
68+
mock_redis.ltrim.assert_called_once_with("test_list", 0, 1)
69+
70+
71+
def test_remove(list_store, mock_redis):
72+
list_store.remove("value1", 1)
73+
mock_redis.lrem.assert_called_once_with("test_list", 1, "value1")
74+
75+
76+
def test_insert_before(list_store, mock_redis):
77+
list_store.insert_before("pivot", "new_value")
78+
mock_redis.linsert.assert_called_once_with("test_list", 'BEFORE', "pivot", "new_value")
79+
80+
81+
def test_insert_after(list_store, mock_redis):
82+
list_store.insert_after("pivot", "new_value")
83+
mock_redis.linsert.assert_called_once_with("test_list", 'AFTER', "pivot", "new_value")
84+
85+
86+
def test_dynamic_method(list_store, mock_redis):
87+
list_store.lmove("source", "destination", "LEFT", "RIGHT")
88+
mock_redis.lmove.assert_called_once_with("test_list", "source", "destination", "LEFT", "RIGHT")

0 commit comments

Comments
 (0)