Skip to content

Commit 32190a9

Browse files
committed
Added incr and decr functionality to cache service and cache backends
1 parent fc7df7a commit 32190a9

File tree

10 files changed

+215
-14
lines changed

10 files changed

+215
-14
lines changed

ellar/cache/backends/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .serializer import AioCacheSerializer, ICacheSerializer, RedisSerializer
2+
3+
__all__ = ["ICacheSerializer", "RedisSerializer", "AioCacheSerializer"]

ellar/cache/backends/aio_cache.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"To use `AioMemCacheBackend`, you have to install 'aiomcache' package e.g. `pip install aiomcache`"
1212
) from e
1313

14-
1514
from ..interface import IBaseCacheBackendAsync
1615
from ..make_key_decorator import make_key_decorator, make_key_decorator_and_validate
1716
from ..model import BaseCacheBackend
17+
from .serializer import AioCacheSerializer, ICacheSerializer
1818

1919

2020
class _AioMemCacheBackendSync(IBaseCacheBackendAsync, ABC):
@@ -48,6 +48,14 @@ def touch(
4848
)
4949
return bool(res)
5050

51+
def incr(self, key: str, delta: int = 1, version: str = None) -> int:
52+
res = self._async_executor(self.incr_async(key, delta=delta, version=version))
53+
return t.cast(int, res)
54+
55+
def decr(self, key: str, delta: int = 1, version: str = None) -> int:
56+
res = self._async_executor(self.decr_async(key, delta=delta, version=version))
57+
return t.cast(int, res)
58+
5159

5260
class AioMemCacheBackend(_AioMemCacheBackendSync, BaseCacheBackend):
5361
"""Memcached-based cache backend."""
@@ -61,17 +69,15 @@ def __init__(
6169
port: int = 11211,
6270
pool_size: int = 2,
6371
pool_minsize: int = None,
64-
serializer: t.Callable = pickle.dumps,
65-
deserializer: t.Callable = pickle.loads,
72+
serializer: ICacheSerializer = None,
6673
**kwargs: t.Any
6774
) -> None:
6875
super().__init__(**kwargs)
6976
self._client: Client = None # type: ignore[assignment]
7077
self._client_options = dict(
7178
host=host, port=port, pool_size=pool_size, pool_minsize=pool_minsize
7279
)
73-
self._serializer = serializer
74-
self._deserializer = deserializer
80+
self._serializer = serializer or AioCacheSerializer()
7581

7682
def get_backend_timeout(self, timeout: t.Union[float, int] = None) -> int:
7783
return int(super().get_backend_timeout(timeout))
@@ -86,7 +92,7 @@ def _cache_client(self) -> Client:
8692
async def get_async(self, key: str, version: str = None) -> t.Optional[t.Any]:
8793
value = await self._cache_client.get(key.encode("utf-8"))
8894
if value:
89-
return self._deserializer(value)
95+
return self._serializer.load(value)
9096
return None # pragma: no cover
9197

9298
@make_key_decorator_and_validate
@@ -99,7 +105,7 @@ async def set_async(
99105
) -> bool:
100106
return await self._cache_client.set(
101107
key.encode("utf-8"),
102-
self._serializer(value, self.pickle_protocol),
108+
self._serializer.dumps(value),
103109
exptime=self.get_backend_timeout(timeout),
104110
)
105111

@@ -114,3 +120,13 @@ async def touch_async(
114120
return await self._cache_client.touch(
115121
key.encode("utf-8"), exptime=self.get_backend_timeout(timeout)
116122
)
123+
124+
@make_key_decorator
125+
async def incr_async(self, key: str, delta: int = 1, version: str = None) -> int:
126+
res = await self._cache_client.incr(key.encode("utf-8"), increment=delta)
127+
return t.cast(int, res)
128+
129+
@make_key_decorator
130+
async def decr_async(self, key: str, delta: int = 1, version: str = None) -> int:
131+
res = await self._cache_client.decr(key.encode("utf-8"), decrement=delta)
132+
return t.cast(int, res)

ellar/cache/backends/base.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ def touch(
4444
result = self._cache_client.touch(key, self.get_backend_timeout(timeout))
4545
return bool(result)
4646

47+
@make_key_decorator
48+
def incr(self, key: str, delta: int = 1, version: str = None) -> int:
49+
result = self._cache_client.incr(key, delta)
50+
return t.cast(int, result)
51+
52+
@make_key_decorator
53+
def decr(self, key: str, delta: int = 1, version: str = None) -> int:
54+
result = self._cache_client.decr(key, delta)
55+
return t.cast(int, result)
56+
4757
def close(self, **kwargs: t.Any) -> None:
4858
# Many clients don't clean up connections properly.
4959
self._cache_client.disconnect_all()
@@ -111,3 +121,11 @@ async def clear_async(self) -> None:
111121
def validate_key(self, key: str) -> None:
112122
super().validate_key(key)
113123
self._memcache_key_warnings(key)
124+
125+
async def incr_async(self, key: str, delta: int = 1, version: str = None) -> int:
126+
res = await self.executor(self.incr, key, delta=delta, version=version)
127+
return t.cast(int, res)
128+
129+
async def decr_async(self, key: str, delta: int = 1, version: str = None) -> int:
130+
res = await self.executor(self.decr, key, delta=delta, version=version)
131+
return t.cast(int, res)

ellar/cache/backends/local_cache.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ def touch(
4444
)
4545
return bool(res)
4646

47+
def incr(self, key: str, delta: int = 1, version: str = None) -> int:
48+
res = self._async_executor(self.incr_async(key, delta=delta, version=version))
49+
return t.cast(int, res)
50+
51+
def decr(self, key: str, delta: int = 1, version: str = None) -> int:
52+
res = self._async_executor(self.decr_async(key, delta=delta, version=version))
53+
return t.cast(int, res)
54+
4755

4856
class LocalMemCacheBackend(_LocalMemCacheBackendSync, BaseCacheBackend):
4957
pickle_protocol = pickle.HIGHEST_PROTOCOL
@@ -116,3 +124,25 @@ async def touch_async(
116124
def has_key(self, key: str, version: str = None) -> bool:
117125
res = self._async_executor(self.has_key_async(key, version=version))
118126
return bool(res)
127+
128+
def _incr_decr_action(self, key: str, delta: int) -> int:
129+
pickled = self._cache[key]
130+
value = t.cast(int, pickle.loads(pickled))
131+
new_value = value + delta
132+
pickled = pickle.dumps(new_value, self.pickle_protocol)
133+
self._cache[key] = pickled
134+
return new_value
135+
136+
async def incr_async(self, key: str, delta: int = 1, version: str = None) -> int:
137+
async with self._lock:
138+
if self._has_expired(key):
139+
await self._delete(key)
140+
raise ValueError("Key '%s' not found" % key)
141+
return self._incr_decr_action(key, delta)
142+
143+
async def decr_async(self, key: str, delta: int = 1, version: str = None) -> int:
144+
async with self._lock:
145+
if self._has_expired(key):
146+
await self._delete(key)
147+
raise ValueError("Key '%s' not found" % key)
148+
return self._incr_decr_action(key, delta * -1)
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
from .backend import RedisCacheBackend
2-
from .serializer import IRedisSerializer
32

4-
__all__ = ["IRedisSerializer", "RedisCacheBackend"]
3+
__all__ = ["RedisCacheBackend"]

ellar/cache/backends/redis/backend.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ...interface import IBaseCacheBackendAsync
1515
from ...make_key_decorator import make_key_decorator, make_key_decorator_and_validate
1616
from ...model import BaseCacheBackend
17-
from .serializer import IRedisSerializer, RedisSerializer
17+
from ..serializer import ICacheSerializer, RedisSerializer
1818

1919

2020
class _RedisCacheBackendSync(IBaseCacheBackendAsync, ABC):
@@ -48,6 +48,18 @@ def touch(
4848
)
4949
return bool(res)
5050

51+
def has_key(self, key: str, version: str = None) -> bool:
52+
res = self._async_executor(self.has_key_async(key, version=version))
53+
return bool(res)
54+
55+
def incr(self, key: str, delta: int = 1, version: str = None) -> int:
56+
res = self._async_executor(self.incr_async(key, delta=delta, version=version))
57+
return t.cast(int, res)
58+
59+
def decr(self, key: str, delta: int = 1, version: str = None) -> int:
60+
res = self._async_executor(self.decr_async(key, delta=delta, version=version))
61+
return t.cast(int, res)
62+
5163

5264
class RedisCacheBackend(_RedisCacheBackendSync, BaseCacheBackend):
5365
MEMCACHE_CLIENT: t.Any = Redis
@@ -68,7 +80,7 @@ def __init__(
6880
self,
6981
servers: t.List[str],
7082
options: t.Dict = None,
71-
serializer: IRedisSerializer = None,
83+
serializer: ICacheSerializer = None,
7284
**kwargs: t.Any
7385
) -> None:
7486
super().__init__(**kwargs)
@@ -152,3 +164,21 @@ async def touch_async(
152164
return bool(res)
153165
res = await client.expire(key, self.get_backend_timeout(timeout))
154166
return bool(res)
167+
168+
@make_key_decorator
169+
async def has_key_async(self, key: str, version: str = None) -> bool:
170+
client = self._get_client()
171+
res = await client.exists(key)
172+
return bool(res)
173+
174+
@make_key_decorator
175+
async def incr_async(self, key: str, delta: int = 1, version: str = None) -> int:
176+
client = self._get_client()
177+
res = await client.incr(key, amount=delta)
178+
return t.cast(int, res)
179+
180+
@make_key_decorator
181+
async def decr_async(self, key: str, delta: int = 1, version: str = None) -> int:
182+
client = self._get_client()
183+
res = await client.decr(key, amount=delta)
184+
return t.cast(int, res)

ellar/cache/backends/redis/serializer.py renamed to ellar/cache/backends/serializer.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABC, abstractmethod
44

55

6-
class IRedisSerializer(ABC):
6+
class ICacheSerializer(ABC):
77
default_protocol: int = pickle.HIGHEST_PROTOCOL
88

99
@abstractmethod
@@ -15,7 +15,7 @@ def dumps(self, data: t.Any) -> t.Any: # pragma: no cover
1515
...
1616

1717

18-
class RedisSerializer(IRedisSerializer):
18+
class RedisSerializer(ICacheSerializer):
1919
def __init__(self, protocol: int = None) -> None:
2020
self._protocol = protocol or self.default_protocol
2121

@@ -31,3 +31,18 @@ def dumps(self, data: t.Any) -> t.Any:
3131
if type(data) is int:
3232
return data
3333
return pickle.dumps(data, self._protocol)
34+
35+
36+
class AioCacheSerializer(RedisSerializer):
37+
def load(self, data: t.Any) -> t.Any:
38+
try:
39+
return int(data.decode("utf-8"))
40+
except ValueError:
41+
return pickle.loads(data)
42+
43+
def dumps(self, data: t.Any) -> t.Any:
44+
# Only skip pickling for integers, an int subclasses as bool should be
45+
# pickled.
46+
if type(data) is int:
47+
return str(data).encode("utf-8")
48+
return pickle.dumps(data, self._protocol)

ellar/cache/interface.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ async def touch_async(
5454
or False if the key does not exist.
5555
"""
5656

57+
async def has_key_async(self, key: str, version: str = None) -> bool:
58+
"""
59+
Return True if the key is in the cache and has not expired.
60+
"""
61+
62+
@abstractmethod
63+
async def incr_async(self, key: str, delta: int = 1, version: str = None) -> int:
64+
"""
65+
Increments the number stored at key by one. If the key does not exist, it is set to 0
66+
"""
67+
68+
@abstractmethod
69+
async def decr_async(self, key: str, delta: int = 1, version: str = None) -> int:
70+
"""
71+
Decrements the number stored at key by one. If the key does not exist, it is set to 0
72+
"""
73+
5774

5875
class IBaseCacheBackendSync(ABC):
5976
@abstractmethod
@@ -104,6 +121,23 @@ def touch(
104121
or False if the key does not exist.
105122
"""
106123

124+
def has_key(self, key: str, version: str = None) -> bool:
125+
"""
126+
Return True if the key is in the cache and has not expired.
127+
"""
128+
129+
@abstractmethod
130+
def incr(self, key: str, delta: int = 1, version: str = None) -> int:
131+
"""
132+
Increments the number stored at key by one. If the key does not exist, it is set to 0
133+
"""
134+
135+
@abstractmethod
136+
def decr(self, key: str, delta: int = 1, version: str = None) -> int:
137+
"""
138+
Decrements the number stored at key by one. If the key does not exist, it is set to 0
139+
"""
140+
107141

108142
class ICacheServiceSync(ABC):
109143
@abstractmethod
@@ -168,6 +202,22 @@ def has_key(self, key: str, version: str = None, backend: str = None) -> bool:
168202
Return True if the key is in the cache and has not expired.
169203
"""
170204

205+
@abstractmethod
206+
def incr(
207+
self, key: str, delta: int = 1, version: str = None, backend: str = None
208+
) -> int:
209+
"""
210+
Increments the number stored at key by one. If the key does not exist, it is set to 0
211+
"""
212+
213+
@abstractmethod
214+
def decr(
215+
self, key: str, delta: int = 1, version: str = None, backend: str = None
216+
) -> int:
217+
"""
218+
Decrements the number stored at key by one. If the key does not exist, it is set to 0
219+
"""
220+
171221

172222
class ICacheServiceAsync(ABC):
173223
@abstractmethod
@@ -238,6 +288,22 @@ async def has_key_async(
238288
Return True if the key is in the cache and has not expired.
239289
"""
240290

291+
@abstractmethod
292+
async def incr_async(
293+
self, key: str, delta: int = 1, version: str = None, backend: str = None
294+
) -> int:
295+
"""
296+
Increments the number stored at key by one. If the key does not exist, it is set to 0
297+
"""
298+
299+
@abstractmethod
300+
async def decr_async(
301+
self, key: str, delta: int = 1, version: str = None, backend: str = None
302+
) -> int:
303+
"""
304+
Decrements the number stored at key by one. If the key does not exist, it is set to 0
305+
"""
306+
241307

242308
class ICacheService(ICacheServiceSync, ICacheServiceAsync, ABC):
243309
"""Cache Service Interface"""

ellar/cache/service.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ class InvalidCacheBackendKeyException(Exception):
1212

1313

1414
class _CacheServiceSync(ICacheServiceSync):
15+
def incr(
16+
self, key: str, delta: int = 1, version: str = None, backend: str = None
17+
) -> int:
18+
_backend = self.get_backend(backend)
19+
return _backend.incr(key, delta=delta, version=version)
20+
21+
def decr(
22+
self, key: str, delta: int = 1, version: str = None, backend: str = None
23+
) -> int:
24+
_backend = self.get_backend(backend)
25+
return _backend.decr(key, delta=delta, version=version)
26+
1527
get_backend: t.Callable[..., BaseCacheBackend]
1628

1729
def get(self, key: str, version: str = None, backend: str = None) -> t.Any:
@@ -110,3 +122,15 @@ async def has_key_async(
110122
) -> bool:
111123
_backend = self.get_backend(backend)
112124
return await _backend.has_key_async(key, version=version)
125+
126+
async def incr_async(
127+
self, key: str, delta: int = 1, version: str = None, backend: str = None
128+
) -> int:
129+
_backend = self.get_backend(backend)
130+
return await _backend.incr_async(key, delta=delta, version=version)
131+
132+
async def decr_async(
133+
self, key: str, delta: int = 1, version: str = None, backend: str = None
134+
) -> int:
135+
_backend = self.get_backend(backend)
136+
return await _backend.decr_async(key, delta=delta, version=version)

ellar/common/decorators/modules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def Module(
6060
template_folder: t.Optional[str] = "templates",
6161
base_directory: t.Optional[t.Union[Path, str]] = None,
6262
static_folder: str = "static",
63-
modules: t.Sequence[t.Type] = tuple(),
63+
modules: t.Sequence[t.Union[t.Type, t.Any]] = tuple(),
6464
commands: t.Sequence[t.Union[t.Callable, "EllarTyper"]] = tuple(),
6565
) -> t.Callable:
6666
"""

0 commit comments

Comments
 (0)