Skip to content

Commit

Permalink
Merge pull request #37 from Attumm/v2.4.0-pre-release
Browse files Browse the repository at this point in the history
V2.4.0
  • Loading branch information
Attumm authored Feb 9, 2024
2 parents d7f4dea + 6e157fb commit 51aca54
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 17 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,21 @@ This approach also minimizes the risk of key collisions between different applic
### Expiration

Redis provides a valuable feature that enables keys to expire. RedisDict supports this feature in the following ways:
1. Set a default expiration time when creating a RedisDict instance. In this example, the keys will have a default expiration time of 10 seconds.
1. Set a default expiration time when creating a RedisDict instance. In this example, the keys will have a default expiration time of 10 seconds. Use seconds with an integer or pass a datetime timedelta.

```python
dic = RedisDict(expire=10)
dic['gone'] = 'in ten seconds'
```
2. Temporarily set the default expiration time within the scope using a context manager. In this example, the key 'gone' will expire after 60 seconds. The default expiration time for other keys outside the context manager remains unchanged.
Or, for a more Pythonic approach, use a timedelta.
```python
from datetime import

dic = RedisDict(expire=timedelta(minutes=1))
dic['gone'] = 'in a minute'
```

2. Temporarily set the default expiration time within the scope using a context manager. In this example, the key 'gone' will expire after 60 seconds. The default expiration time for other keys outside the context manager remains unchanged. Either pass an integer or a timedelta.

```python
dic = RedisDict()
Expand Down
12 changes: 6 additions & 6 deletions redis_dict.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from datetime import timedelta
from typing import Any, Callable, Dict, Iterator, Set, List, Tuple, Union, Optional
from redis import StrictRedis

Expand Down Expand Up @@ -120,24 +121,23 @@ class RedisDict:
type(set()).__name__: _pre_transform_set,
}


def __init__(self,
namespace: str = 'main',
expire: Union[int, None] = None,
expire: Union[int, timedelta, None] = None,
preserve_expiration: Optional[bool] = False,
**redis_kwargs: Any) -> None:
"""
Initialize a RedisDict instance.
Args:
namespace (str, optional): A prefix for keys stored in Redis.
expire (int, optional): Expiration time for keys in seconds.
expire (int, timedelta, optional): Expiration time for keys in seconds.
preserve_expiration (bool, optional): Whether or not to preserve the expiration.
**redis_kwargs: Additional keyword arguments passed to StrictRedis.
"""
self.temp_redis: Optional[StrictRedis[Any]] = None
self.namespace: str = namespace
self.expire: Union[int, None] = expire
self.expire: Union[int, timedelta, None] = expire
self.preserve_expiration: Optional[bool] = preserve_expiration
self.redis: StrictRedis[Any] = StrictRedis(decode_responses=True, **redis_kwargs)
self.get_redis: StrictRedis[Any] = self.redis
Expand Down Expand Up @@ -640,12 +640,12 @@ def chain_del(self, iterable: List[str]) -> None:
return self.__delitem__(':'.join(iterable))

@contextmanager
def expire_at(self, sec_epoch: int) -> Iterator[None]:
def expire_at(self, sec_epoch: int | timedelta) -> Iterator[None]:
"""
Context manager to set the expiration time for keys in the RedisDict.
Args:
sec_epoch (int): The expiration time in Unix timestamp format.
sec_epoch (int, timedelta): The expiration duration is set using either an integer or a timedelta.
Returns:
ContextManager: A context manager during which the expiration time is the time set.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
long_description=long_description,
long_description_content_type='text/markdown',

version='2.3.2',
version='2.4.0',
py_modules=['redis_dict'],
install_requires=['redis',],
license='MIT',
Expand Down
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
attrs==22.2.0
cffi==1.15.1
coverage==5.5
cryptography==41.0.6
cryptography==42.0.0
exceptiongroup==1.1.1
future==0.18.3
hypothesis==6.70.1
Expand Down
54 changes: 47 additions & 7 deletions tests.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import time
import unittest
from datetime import timedelta

import redis

from redis_dict import RedisDict
from hypothesis import given, strategies as st


# !! Make sure you don't have keys within redis named like this, they will be deleted.
TEST_NAMESPACE_PREFIX = '__test_prefix_key_meta_8128__'

Expand Down Expand Up @@ -813,14 +813,49 @@ def test_expire_context(self):
actual_ttl = self.redisdb.ttl('{}:foobar'.format(TEST_NAMESPACE_PREFIX))
self.assertAlmostEqual(3600, actual_ttl, delta=2)

def test_expire_context_timedelta(self):
""" Test adding keys with an expire value by using the contextmanager. With timedelta as argument. """
timedelta_one_hour = timedelta(hours=1)
timedelta_one_minute = timedelta(minutes=1)
hour_in_seconds = 60 * 60
minute_in_seconds = 60

with self.r.expire_at(timedelta_one_hour):
self.r['one_hour'] = 'one_hour'
with self.r.expire_at(timedelta_one_minute):
self.r['one_minute'] = 'one_minute'

actual_ttl = self.redisdb.ttl('{}:one_hour'.format(TEST_NAMESPACE_PREFIX))
self.assertAlmostEqual(hour_in_seconds, actual_ttl, delta=2)
actual_ttl = self.redisdb.ttl('{}:one_minute'.format(TEST_NAMESPACE_PREFIX))
self.assertAlmostEqual(minute_in_seconds, actual_ttl, delta=2)

def test_expire_keyword(self):
"""Test ading keys with an expire value by using the expire config keyword."""
"""Test adding keys with an expire value by using the expire config keyword."""
r = self.create_redis_dict(expire=3600)

r['foobar'] = 'barbar'
actual_ttl = self.redisdb.ttl('{}:foobar'.format(TEST_NAMESPACE_PREFIX))
self.assertAlmostEqual(3600, actual_ttl, delta=2)

def test_expire_keyword_timedelta(self):
""" Test adding keys with an expire value by using the expire config keyword. With timedelta as argument."""
timedelta_one_hour = timedelta(hours=1)
timedelta_one_minute = timedelta(minutes=1)
hour_in_seconds = 60 * 60
minute_in_seconds = 60

r_hour = self.create_redis_dict(expire=timedelta_one_hour)
r_minute = self.create_redis_dict(expire=timedelta_one_minute)

r_hour['one_hour'] = 'one_hour'
r_minute['one_minute'] = 'one_minute'

actual_ttl = self.redisdb.ttl('{}:one_hour'.format(TEST_NAMESPACE_PREFIX))
self.assertAlmostEqual(hour_in_seconds, actual_ttl, delta=2)
actual_ttl = self.redisdb.ttl('{}:one_minute'.format(TEST_NAMESPACE_PREFIX))
self.assertAlmostEqual(minute_in_seconds, actual_ttl, delta=2)

def test_iter(self):
"""Tests the __iter__ function."""
key_values = {
Expand Down Expand Up @@ -1182,6 +1217,7 @@ class TestRedisDictComparison(unittest.TestCase):
__gt__(self, other)
__ge__(self, other)
"""

def setUp(self):
self.r1 = RedisDict(namespace="test1")
self.r2 = RedisDict(namespace="test2")
Expand Down Expand Up @@ -1234,11 +1270,11 @@ def test_equal_with_dict(self):
self.assertNotEqual(self.r1, self.d2)

def test_empty_equal(self):
empty_r = RedisDict(namespace="test_empty") # TODO make sure it's deleted
empty_r = RedisDict(namespace="test_empty") # TODO make sure it's deleted
self.assertEqual(empty_r, {})

def test_nested_empty_equal(self):
nested_empty_r = RedisDict(namespace="test_nested_empty") # TODO make sure it's deleted
nested_empty_r = RedisDict(namespace="test_nested_empty") # TODO make sure it's deleted
nested_empty_r.update({"a": {}})
nested_empty_d = {"a": {}}
self.assertEqual(nested_empty_r, nested_empty_d)
Expand Down Expand Up @@ -1397,7 +1433,8 @@ def test_set_get_dictionary_with_integer_values(self, key, value):
self.r[key] = value
self.assertEqual(self.r[key], value)

@given(key=st.text(min_size=1), value=st.dictionaries(st.text(min_size=1), st.floats(allow_nan=False, allow_infinity=False)))
@given(key=st.text(min_size=1),
value=st.dictionaries(st.text(min_size=1), st.floats(allow_nan=False, allow_infinity=False)))
def test_set_get_dictionary_with_float_values(self, key, value):
self.r[key] = value
self.assertEqual(self.r[key], value)
Expand All @@ -1407,7 +1444,8 @@ def test_set_get_dictionary_with_list_values(self, key, value):
self.r[key] = value
self.assertEqual(self.r[key], value)

@given(key=st.text(min_size=1), value=st.dictionaries(st.text(min_size=1), st.dictionaries(st.text(min_size=1), st.text())))
@given(key=st.text(min_size=1),
value=st.dictionaries(st.text(min_size=1), st.dictionaries(st.text(min_size=1), st.text())))
def test_set_get_nested_dictionary(self, key, value):
"""
Test setting and getting a nested dictionary.
Expand All @@ -1423,7 +1461,8 @@ def test_set_get_nested_list(self, key, value):
self.r[key] = value
self.assertEqual(self.r[key], value)

@given(key=st.text(min_size=1), value=st.tuples(st.integers(), st.text(), st.floats(allow_nan=False, allow_infinity=False), st.booleans()))
@given(key=st.text(min_size=1),
value=st.tuples(st.integers(), st.text(), st.floats(allow_nan=False, allow_infinity=False), st.booleans()))
def test_set_get_tuple(self, key, value):
"""
Test setting and getting a tuple.
Expand All @@ -1442,6 +1481,7 @@ def test_set_get_set(self, key, value):

if __name__ == '__main__':
import sys

if sys.version_info[0] == 2:
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
unittest.main()

0 comments on commit 51aca54

Please sign in to comment.