bda.cache 2.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.
- bda/cache/__init__.py +14 -0
- bda/cache/configure.zcml +5 -0
- bda/cache/fscache.py +243 -0
- bda/cache/interfaces.py +101 -0
- bda/cache/memcached.py +131 -0
- bda/cache/nullcache.py +68 -0
- bda/cache/tests/__init__.py +1 -0
- bda/cache/tests/test_fscache.py +236 -0
- bda/cache/tests/test_memcached.py +231 -0
- bda/cache/tests/test_nullcache.py +61 -0
- bda_cache-2.0.0.dist-info/METADATA +197 -0
- bda_cache-2.0.0.dist-info/RECORD +13 -0
- bda_cache-2.0.0.dist-info/WHEEL +4 -0
bda/cache/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright 2009, Blue Dynamics Alliance, Austria - http://bluedynamics.com
|
|
2
|
+
# GNU General Public Licence Version 2 or later
|
|
3
|
+
|
|
4
|
+
from bda.cache.fscache import FSCache # noqa
|
|
5
|
+
from bda.cache.fscache import FSCacheException # noqa
|
|
6
|
+
from bda.cache.fscache import FSCacheManager # noqa
|
|
7
|
+
from bda.cache.interfaces import ICacheManager # noqa
|
|
8
|
+
from bda.cache.interfaces import ICacheProvider # noqa
|
|
9
|
+
from bda.cache.interfaces import ICacheVary # noqa
|
|
10
|
+
from bda.cache.memcached import Memcached # noqa
|
|
11
|
+
from bda.cache.memcached import MemcachedException # noqa
|
|
12
|
+
from bda.cache.memcached import MemcachedManager # noqa
|
|
13
|
+
from bda.cache.nullcache import NullCache # noqa
|
|
14
|
+
from bda.cache.nullcache import NullCacheManager # noqa
|
bda/cache/configure.zcml
ADDED
bda/cache/fscache.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Copyright 2009, Blue Dynamics Alliance, Austria - http://bluedynamics.com
|
|
2
|
+
# GNU General Public Licence Version 2 or later
|
|
3
|
+
"""
|
|
4
|
+
Module fscache.
|
|
5
|
+
|
|
6
|
+
This module provides fuctionallity for caching objects on the filesystem.
|
|
7
|
+
|
|
8
|
+
Therefor it uses pickle.
|
|
9
|
+
|
|
10
|
+
This module was originally designed for very simple caching tasks like caching
|
|
11
|
+
mail headers in a MUA or similar. It is NOT a cache manager.
|
|
12
|
+
|
|
13
|
+
If you find this module useful and you have some improvements or bugfixes,
|
|
14
|
+
please contact the author.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from bda.cache.interfaces import CacheException
|
|
18
|
+
from bda.cache.interfaces import ICacheManager
|
|
19
|
+
from bda.cache.interfaces import IFSCacheProvider
|
|
20
|
+
from zope.component import adapter
|
|
21
|
+
from zope.component import provideAdapter
|
|
22
|
+
from zope.interface import implementer
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import pickle
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger('bda.cache.cachemanager')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FSCacheException(CacheException):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@implementer(IFSCacheProvider)
|
|
37
|
+
class FSCache:
|
|
38
|
+
"""Class FsCache.
|
|
39
|
+
|
|
40
|
+
Simple object caching.
|
|
41
|
+
|
|
42
|
+
This class is accessible like a dictionary. For example:
|
|
43
|
+
|
|
44
|
+
cache = FsCache('var/cache/mycache')
|
|
45
|
+
cache['foo.bar'] = obj
|
|
46
|
+
obj = ['foo.bar']
|
|
47
|
+
obj = cache.get('foo.bar', False)
|
|
48
|
+
del cache['foo.bar']
|
|
49
|
+
|
|
50
|
+
The key for objects must ALWAYS be a string, it results directly in a
|
|
51
|
+
file path.
|
|
52
|
+
|
|
53
|
+
By convention, if the key is seperated by a dot like in the example above,
|
|
54
|
+
the files created on your filesystem are placed in a directory tree, so
|
|
55
|
+
the file for the key given in this example is placed at 'foo/bar.'.
|
|
56
|
+
|
|
57
|
+
Take care of your keys, things like 'foo..bar', 'foo.', '.foo' or using
|
|
58
|
+
path seperators like '/' and '\\' will end up in strange results and cause
|
|
59
|
+
inconsistent data.
|
|
60
|
+
|
|
61
|
+
On the other hand, it is no problem to store an object with key 'foo',
|
|
62
|
+
since every file gets a trailing '.', so key 'foo' ends up in a file with
|
|
63
|
+
the name 'foo.' while 'foo.bar' is sored at 'foo/bar.'.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, cachedir, protocol=2, createDirIfNotExist=False):
|
|
67
|
+
"""Create the cache object.
|
|
68
|
+
|
|
69
|
+
* basedir - the directory you want to store the cached objects.
|
|
70
|
+
* protocol - the protocol you want to use for pickling. defaults to 2.
|
|
71
|
+
|
|
72
|
+
Raise an FSCacheException when given basedir is invalid. Does not check
|
|
73
|
+
if there is read/write access on this directory. Keep this in mind.
|
|
74
|
+
|
|
75
|
+
Read the documentation of the module pickle to know about the
|
|
76
|
+
limitations on object pickling.
|
|
77
|
+
"""
|
|
78
|
+
if not os.path.isabs(cachedir):
|
|
79
|
+
errmsg = "Cannot initialize cache. '%s' is not an absolute path."
|
|
80
|
+
raise FSCacheException(errmsg % cachedir)
|
|
81
|
+
if not os.path.exists(cachedir) and not createDirIfNotExist:
|
|
82
|
+
errmsg = "Cannot initialize cache. Directory '%s' not exists."
|
|
83
|
+
raise FSCacheException(errmsg % cachedir)
|
|
84
|
+
if createDirIfNotExist:
|
|
85
|
+
self._createDirIfNotExist(cachedir)
|
|
86
|
+
if not os.path.isdir(cachedir):
|
|
87
|
+
errmsg = "Cannot initialize cache. '%s' is not a Directory."
|
|
88
|
+
raise FSCacheException(errmsg % cachedir)
|
|
89
|
+
self.cachedir = cachedir
|
|
90
|
+
self.protocol = protocol
|
|
91
|
+
|
|
92
|
+
def reset(self):
|
|
93
|
+
for key in self.keys():
|
|
94
|
+
del self[key]
|
|
95
|
+
|
|
96
|
+
def size(self):
|
|
97
|
+
# XXX
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
def keys(self):
|
|
101
|
+
return self._readkeys([])
|
|
102
|
+
|
|
103
|
+
def values(self):
|
|
104
|
+
values = []
|
|
105
|
+
for key in self.keys():
|
|
106
|
+
values.append(self[key])
|
|
107
|
+
return values
|
|
108
|
+
|
|
109
|
+
def get(self, key, default=None):
|
|
110
|
+
return self._getitem(key, default)
|
|
111
|
+
|
|
112
|
+
def __getitem__(self, key):
|
|
113
|
+
return self._getitem(key)
|
|
114
|
+
|
|
115
|
+
def __setitem__(self, key, object):
|
|
116
|
+
"""Read the class documentation how keys must look like."""
|
|
117
|
+
objpath = key.split('.')
|
|
118
|
+
objlen = len(objpath)
|
|
119
|
+
pt = 1
|
|
120
|
+
path = self.cachedir
|
|
121
|
+
while pt < objlen:
|
|
122
|
+
path = os.path.join(self.cachedir, *[f for f in objpath[:pt]])
|
|
123
|
+
exists = os.path.exists(path)
|
|
124
|
+
if not exists:
|
|
125
|
+
os.mkdir(path)
|
|
126
|
+
pt += 1
|
|
127
|
+
path = os.path.join(path, '%s.' % objpath[-1])
|
|
128
|
+
file = open(path, 'wb')
|
|
129
|
+
pickle.dump(object, file, self.protocol)
|
|
130
|
+
file.close()
|
|
131
|
+
|
|
132
|
+
def __delitem__(self, key):
|
|
133
|
+
path = key.split('.')
|
|
134
|
+
path[-1] = '%s.' % path[-1]
|
|
135
|
+
try:
|
|
136
|
+
os.remove(os.path.join(self.cachedir, *path))
|
|
137
|
+
except OSError:
|
|
138
|
+
return None
|
|
139
|
+
pt = len(path) - 1
|
|
140
|
+
while pt > 0:
|
|
141
|
+
previous = path[:pt]
|
|
142
|
+
curpath = os.path.join(self.cachedir, *previous)
|
|
143
|
+
contents = os.listdir(curpath)
|
|
144
|
+
if not contents:
|
|
145
|
+
os.rmdir(curpath)
|
|
146
|
+
else:
|
|
147
|
+
break
|
|
148
|
+
pt -= 1
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def _getitem(self, key, default=None):
|
|
152
|
+
path = '%s.' % os.path.join(self.cachedir, *key.split('.'))
|
|
153
|
+
try:
|
|
154
|
+
file = open(path, 'rb')
|
|
155
|
+
except OSError:
|
|
156
|
+
return default
|
|
157
|
+
obj = pickle.load(file)
|
|
158
|
+
file.close()
|
|
159
|
+
return obj
|
|
160
|
+
|
|
161
|
+
def _readkeys(self, keys, path=[]):
|
|
162
|
+
entries = os.listdir(os.path.join(self.cachedir, *path))
|
|
163
|
+
for entry in entries:
|
|
164
|
+
if entry[-1] == '.':
|
|
165
|
+
key = ''
|
|
166
|
+
for part in path:
|
|
167
|
+
key = '%s%s.' % (key, part)
|
|
168
|
+
keys.append('%s%s' % (key, entry[: len(entry) - 1]))
|
|
169
|
+
else:
|
|
170
|
+
self._readkeys(keys, path + [entry])
|
|
171
|
+
return keys
|
|
172
|
+
|
|
173
|
+
def _createDirIfNotExist(self, cachedir):
|
|
174
|
+
if os.path.exists(cachedir):
|
|
175
|
+
return
|
|
176
|
+
path = cachedir.split(os.path.sep)
|
|
177
|
+
for i in range(len(path)):
|
|
178
|
+
if not os.path.exists(os.path.sep.join(path[: i + 1])) and path[i]:
|
|
179
|
+
os.mkdir(os.path.sep.join(path[: i + 1]))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@implementer(ICacheManager)
|
|
183
|
+
@adapter(IFSCacheProvider)
|
|
184
|
+
class FSCacheManager:
|
|
185
|
+
def __init__(self, context):
|
|
186
|
+
self.timeout = 300 # defaults to 300 seconds
|
|
187
|
+
self.cache = context
|
|
188
|
+
|
|
189
|
+
def setTimeout(self, timeout):
|
|
190
|
+
self.timeout = timeout
|
|
191
|
+
|
|
192
|
+
def getData(self, func, key, force_reload=False, args=[], kwargs={}):
|
|
193
|
+
# from_cache = True
|
|
194
|
+
ret = self.get(key, force_reload)
|
|
195
|
+
if ret is None:
|
|
196
|
+
# from_cache = False
|
|
197
|
+
ret = func(*args, **kwargs)
|
|
198
|
+
self.set(key, ret)
|
|
199
|
+
return ret
|
|
200
|
+
|
|
201
|
+
def get(self, key, force_reload=False):
|
|
202
|
+
if force_reload or self._isTimedOut(key):
|
|
203
|
+
del self.cache[key]
|
|
204
|
+
creationmap = self.cache.get('creationmap', None)
|
|
205
|
+
if creationmap is not None and key in creationmap:
|
|
206
|
+
del creationmap[key]
|
|
207
|
+
self.cache['creationmap'] = creationmap
|
|
208
|
+
return None
|
|
209
|
+
return self.cache.get(key, None)
|
|
210
|
+
|
|
211
|
+
def set(self, key, item, set_creationtime=True):
|
|
212
|
+
self.cache[key] = item
|
|
213
|
+
if not set_creationtime:
|
|
214
|
+
return
|
|
215
|
+
creationmap = self.cache.get('creationmap', None)
|
|
216
|
+
if not creationmap:
|
|
217
|
+
creationmap = dict()
|
|
218
|
+
creationmap[key] = time.time()
|
|
219
|
+
self.cache['creationmap'] = creationmap
|
|
220
|
+
|
|
221
|
+
def rem(self, key):
|
|
222
|
+
del self.cache[key]
|
|
223
|
+
creationmap = self.cache.get('creationmap', None)
|
|
224
|
+
if creationmap is not None and key in creationmap:
|
|
225
|
+
del creationmap[key]
|
|
226
|
+
self.cache['creationmap'] = creationmap
|
|
227
|
+
|
|
228
|
+
def __delitem__(self, key):
|
|
229
|
+
self.rem(key)
|
|
230
|
+
|
|
231
|
+
def _isTimedOut(self, key):
|
|
232
|
+
"""Return wether the item with key is timed out or not."""
|
|
233
|
+
cur = time.time()
|
|
234
|
+
creationmap = self.cache.get('creationmap', None)
|
|
235
|
+
if not creationmap:
|
|
236
|
+
return True
|
|
237
|
+
creationtime = creationmap.get(key, None)
|
|
238
|
+
if not creationtime or creationtime + self.timeout < cur:
|
|
239
|
+
return True
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
provideAdapter(FSCacheManager)
|
bda/cache/interfaces.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Copyright 2009, Blue Dynamics Alliance, Austria - http://bluedynamics.com
|
|
2
|
+
# GNU General Public Licence Version 2 or later
|
|
3
|
+
|
|
4
|
+
from zope.interface import Interface
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CacheException(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ICacheVary(Interface):
|
|
12
|
+
"""This interface describes an API for manipulating objects inside a cache."""
|
|
13
|
+
|
|
14
|
+
def vary(*args, **kwargs):
|
|
15
|
+
"""Manipulate objects inside the cache."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ICacheManager(Interface):
|
|
19
|
+
"""Interface for read/write operations on ICacheProvider implementing
|
|
20
|
+
objects.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def setTimeout(timeout):
|
|
24
|
+
"""Set cache timeout in seconds."""
|
|
25
|
+
|
|
26
|
+
def getData(func, key, force_reload=False, args=[], kwargs={}):
|
|
27
|
+
"""Convenience to read and cache results at once.
|
|
28
|
+
|
|
29
|
+
* Tries to read result from cache.
|
|
30
|
+
* If no result, call given functions with *args ang **kwargs
|
|
31
|
+
* If function called, store returned value to cache
|
|
32
|
+
* Return result
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def get(key, force_reload=False):
|
|
36
|
+
"""Return item with key or None.
|
|
37
|
+
|
|
38
|
+
If force_reload is True, try to delete object with key from cache and
|
|
39
|
+
return None.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def set(key, item, set_creationtime=True):
|
|
43
|
+
"""Store an item with key to cache.
|
|
44
|
+
|
|
45
|
+
XXX: get rid of ``set_creationtime``
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def rem(key):
|
|
49
|
+
"""Remove item with key from cache if exists.
|
|
50
|
+
|
|
51
|
+
Legacy. Replaced by ``__delitem__``
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __delitem__(key):
|
|
55
|
+
"""Remove item with key from cache if exists."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ICacheProvider(Interface):
|
|
59
|
+
"""This interface describes an API for caching objects."""
|
|
60
|
+
|
|
61
|
+
def reset():
|
|
62
|
+
"""Reset the cache object.
|
|
63
|
+
|
|
64
|
+
Remove all objects from cache.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def size():
|
|
68
|
+
"""Return the current size of the cache in byte."""
|
|
69
|
+
|
|
70
|
+
def keys():
|
|
71
|
+
"""Return the keys of the objects contained in this cache as list."""
|
|
72
|
+
|
|
73
|
+
def values():
|
|
74
|
+
"""Return objects contained in this cache as list."""
|
|
75
|
+
|
|
76
|
+
def get(key, default=None):
|
|
77
|
+
"""Return object by key or default value if not exist."""
|
|
78
|
+
|
|
79
|
+
def __getitem__(key):
|
|
80
|
+
"""Return object by key or None if not exists."""
|
|
81
|
+
|
|
82
|
+
def __setitem__(key, object):
|
|
83
|
+
"""Store object to cache by given key."""
|
|
84
|
+
|
|
85
|
+
def __delitem__(key):
|
|
86
|
+
"""Delete object by key from cache.
|
|
87
|
+
|
|
88
|
+
Always return None.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class INullCacheProvider(ICacheProvider):
|
|
93
|
+
"""Marker"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class IFSCacheProvider(ICacheProvider):
|
|
97
|
+
"""Marker"""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class IMemcachedProvider(ICacheProvider):
|
|
101
|
+
"""Marker"""
|
bda/cache/memcached.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copyright 2009, Blue Dynamics Alliance, Austria - http://bluedynamics.com
|
|
2
|
+
# GNU General Public Licence Version 2 or later
|
|
3
|
+
"""
|
|
4
|
+
Module memcache.
|
|
5
|
+
|
|
6
|
+
This module provides fuctionallity for caching objects in a memcache server.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from bda.cache.interfaces import CacheException
|
|
10
|
+
from bda.cache.interfaces import ICacheManager
|
|
11
|
+
from bda.cache.interfaces import IMemcachedProvider
|
|
12
|
+
from zope.component import adapter
|
|
13
|
+
from zope.component import provideAdapter
|
|
14
|
+
from zope.interface import implementer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import libmc
|
|
19
|
+
|
|
20
|
+
LIBMC = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
LIBMC = False
|
|
23
|
+
try:
|
|
24
|
+
import pylibmc
|
|
25
|
+
import zlib
|
|
26
|
+
|
|
27
|
+
PYLIBMC = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
PYLIBMC = False
|
|
30
|
+
|
|
31
|
+
if LIBMC:
|
|
32
|
+
Client = libmc.Client
|
|
33
|
+
elif PYLIBMC:
|
|
34
|
+
Client = pylibmc.Client
|
|
35
|
+
else:
|
|
36
|
+
from memcache import Client
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MemcachedException(CacheException):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@implementer(IMemcachedProvider)
|
|
44
|
+
class Memcached:
|
|
45
|
+
def __init__(self, servers):
|
|
46
|
+
"""Initialize memcached.
|
|
47
|
+
@param servers: an array of servers. Servers can be passed in two forms:
|
|
48
|
+
1. Strings of the form host:port (implies a default weight of 1).
|
|
49
|
+
2. Tuples of the form (host:port, weight) (weight as integer)
|
|
50
|
+
"""
|
|
51
|
+
if LIBMC:
|
|
52
|
+
self._client = Client(servers, comp_threshold=10240, noreply=True)
|
|
53
|
+
else:
|
|
54
|
+
self._client = Client(servers)
|
|
55
|
+
self.timeout = 0
|
|
56
|
+
|
|
57
|
+
def reset(self):
|
|
58
|
+
self._client.flush_all()
|
|
59
|
+
|
|
60
|
+
def size(self):
|
|
61
|
+
bytes = 0
|
|
62
|
+
stats = self._client.get_stats()
|
|
63
|
+
for name, stat in stats:
|
|
64
|
+
bytes += int(stat['bytes'])
|
|
65
|
+
return bytes
|
|
66
|
+
|
|
67
|
+
def keys(self):
|
|
68
|
+
raise MemcachedException("It's not possible to fetch keys from memcached")
|
|
69
|
+
|
|
70
|
+
def values(self):
|
|
71
|
+
raise MemcachedException("It's not possible to fetch values from memcached")
|
|
72
|
+
|
|
73
|
+
def get(self, key, default=None):
|
|
74
|
+
value = self._client.get(key)
|
|
75
|
+
if value is not None:
|
|
76
|
+
return value
|
|
77
|
+
return default
|
|
78
|
+
|
|
79
|
+
def __getitem__(self, key):
|
|
80
|
+
return self._client.get(key)
|
|
81
|
+
|
|
82
|
+
def __setitem__(self, key, object):
|
|
83
|
+
if PYLIBMC and not LIBMC:
|
|
84
|
+
self._client.set(
|
|
85
|
+
key,
|
|
86
|
+
object,
|
|
87
|
+
time=self.timeout,
|
|
88
|
+
min_compress_len=1024000,
|
|
89
|
+
compress_level=zlib.Z_BEST_SPEED,
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
self._client.set(key, object, time=self.timeout)
|
|
93
|
+
|
|
94
|
+
def __delitem__(self, key):
|
|
95
|
+
self._client.delete(key)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@implementer(ICacheManager)
|
|
99
|
+
@adapter(IMemcachedProvider)
|
|
100
|
+
class MemcachedManager:
|
|
101
|
+
def __init__(self, context):
|
|
102
|
+
self.cache = context
|
|
103
|
+
|
|
104
|
+
def setTimeout(self, timeout):
|
|
105
|
+
self.cache.timeout = timeout
|
|
106
|
+
|
|
107
|
+
def getData(self, func, key, force_reload=False, args=[], kwargs={}):
|
|
108
|
+
ret = self.get(key, force_reload)
|
|
109
|
+
if ret is None:
|
|
110
|
+
ret = func(*args, **kwargs)
|
|
111
|
+
self.set(key, ret)
|
|
112
|
+
return ret
|
|
113
|
+
|
|
114
|
+
def get(self, key, force_reload=False):
|
|
115
|
+
if force_reload:
|
|
116
|
+
del self.cache[key]
|
|
117
|
+
return None
|
|
118
|
+
return self.cache.get(key, None)
|
|
119
|
+
|
|
120
|
+
def set(self, key, item):
|
|
121
|
+
self.cache[key] = item
|
|
122
|
+
|
|
123
|
+
def rem(self, key):
|
|
124
|
+
"""deprecated, use __delitem___"""
|
|
125
|
+
del self.cache[key]
|
|
126
|
+
|
|
127
|
+
def __delitem__(self, key):
|
|
128
|
+
del self.cache[key]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
provideAdapter(MemcachedManager)
|
bda/cache/nullcache.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Copyright 2009, Blue Dynamics Alliance, Austria - http://bluedynamics.com
|
|
2
|
+
# GNU General Public Licence Version 2 or later
|
|
3
|
+
|
|
4
|
+
from bda.cache.interfaces import ICacheManager
|
|
5
|
+
from bda.cache.interfaces import INullCacheProvider
|
|
6
|
+
from zope.component import adapter
|
|
7
|
+
from zope.component import provideAdapter
|
|
8
|
+
from zope.interface import implementer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@implementer(INullCacheProvider)
|
|
12
|
+
class NullCache:
|
|
13
|
+
"""Dummy implementation which does nothing and can be used as fallback."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def reset(self):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def size(self):
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
def keys(self):
|
|
25
|
+
raise NotImplementedError("It's not possible to fetch keys from nothing")
|
|
26
|
+
|
|
27
|
+
def values(self):
|
|
28
|
+
raise NotImplementedError("It's not possible to fetch values from nothing")
|
|
29
|
+
|
|
30
|
+
def get(self, key, default=None):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
def __getitem__(self, key):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def __setitem__(self, key, object):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def __delitem__(self, key):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@implementer(ICacheManager)
|
|
44
|
+
@adapter(INullCacheProvider)
|
|
45
|
+
class NullCacheManager:
|
|
46
|
+
def __init__(self, context):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def setTimeout(self, timeout):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def getData(self, func, key, force_reload=False, args=[], kwargs={}):
|
|
53
|
+
return func(*args, **kwargs)
|
|
54
|
+
|
|
55
|
+
def get(self, key, force_reload=False):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def set(self, key, item):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def rem(self, key):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def __delitem__(self, key):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
provideAdapter(NullCacheManager)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from bda.cache import FSCache
|
|
2
|
+
from bda.cache import FSCacheException
|
|
3
|
+
from bda.cache import FSCacheManager
|
|
4
|
+
from bda.cache import ICacheManager
|
|
5
|
+
from bda.cache.interfaces import IFSCacheProvider
|
|
6
|
+
from unittest import mock
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FSCacheTestCase(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self.tempdir = tempfile.mkdtemp()
|
|
17
|
+
self.addCleanup(shutil.rmtree, self.tempdir)
|
|
18
|
+
self.cachedir = os.path.join(self.tempdir, 'cache')
|
|
19
|
+
os.mkdir(self.cachedir)
|
|
20
|
+
|
|
21
|
+
def listdir(self, *path):
|
|
22
|
+
return sorted(os.listdir(os.path.join(self.cachedir, *path)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestFSCacheInit(FSCacheTestCase):
|
|
26
|
+
def test_relative_path(self):
|
|
27
|
+
with self.assertRaises(FSCacheException) as cm:
|
|
28
|
+
FSCache('notabspath')
|
|
29
|
+
self.assertEqual(
|
|
30
|
+
str(cm.exception),
|
|
31
|
+
"Cannot initialize cache. 'notabspath' is not an absolute path.",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def test_missing_directory(self):
|
|
35
|
+
path = os.path.join(self.tempdir, 'missing')
|
|
36
|
+
with self.assertRaises(FSCacheException) as cm:
|
|
37
|
+
FSCache(path)
|
|
38
|
+
self.assertEqual(
|
|
39
|
+
str(cm.exception), f"Cannot initialize cache. Directory '{path}' not exists."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def test_not_a_directory(self):
|
|
43
|
+
path = os.path.join(self.tempdir, 'file')
|
|
44
|
+
with open(path, 'w') as f:
|
|
45
|
+
f.write('hallo')
|
|
46
|
+
with self.assertRaises(FSCacheException) as cm:
|
|
47
|
+
FSCache(path)
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
str(cm.exception), f"Cannot initialize cache. '{path}' is not a Directory."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def test_create_missing_directory(self):
|
|
53
|
+
path = os.path.join(self.tempdir, 'a', 'b')
|
|
54
|
+
cache = FSCache(path, createDirIfNotExist=True)
|
|
55
|
+
self.assertTrue(os.path.isdir(path))
|
|
56
|
+
self.assertEqual(cache.cachedir, path)
|
|
57
|
+
|
|
58
|
+
def test_create_existing_directory(self):
|
|
59
|
+
cache = FSCache(self.cachedir, createDirIfNotExist=True)
|
|
60
|
+
self.assertEqual(cache.cachedir, self.cachedir)
|
|
61
|
+
|
|
62
|
+
def test_defaults(self):
|
|
63
|
+
cache = FSCache(self.cachedir)
|
|
64
|
+
self.assertTrue(IFSCacheProvider.providedBy(cache))
|
|
65
|
+
self.assertEqual(cache.protocol, 2)
|
|
66
|
+
self.assertEqual(cache.size(), 0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestFSCache(FSCacheTestCase):
|
|
70
|
+
def setUp(self):
|
|
71
|
+
super().setUp()
|
|
72
|
+
self.cache = FSCache(self.cachedir)
|
|
73
|
+
|
|
74
|
+
def test_dotted_key_is_stored_in_directory_tree(self):
|
|
75
|
+
self.cache['user.section.msg'] = 'message'
|
|
76
|
+
self.assertEqual(self.listdir(), ['user'])
|
|
77
|
+
self.assertEqual(self.listdir('user'), ['section'])
|
|
78
|
+
self.assertEqual(self.listdir('user', 'section'), ['msg.'])
|
|
79
|
+
self.assertEqual(self.cache['user.section.msg'], 'message')
|
|
80
|
+
|
|
81
|
+
def test_key_may_be_part_of_another_key(self):
|
|
82
|
+
# Every file gets a trailing dot, thus 'user' is stored at 'user.'
|
|
83
|
+
# next to the directory 'user' containing 'user.section'.
|
|
84
|
+
self.cache['user'] = {'key': 'user'}
|
|
85
|
+
self.cache['user.section'] = {'key': 'user.section'}
|
|
86
|
+
self.assertEqual(self.listdir(), ['user', 'user.'])
|
|
87
|
+
self.assertEqual(self.cache['user'], {'key': 'user'})
|
|
88
|
+
self.assertEqual(self.cache['user.section'], {'key': 'user.section'})
|
|
89
|
+
|
|
90
|
+
def test_missing_key(self):
|
|
91
|
+
self.assertIsNone(self.cache['notavailable'])
|
|
92
|
+
self.assertIsNone(self.cache.get('notavailable'))
|
|
93
|
+
self.assertEqual(self.cache.get('notavailable', 'works'), 'works')
|
|
94
|
+
|
|
95
|
+
def test_overwrite(self):
|
|
96
|
+
self.cache['user.section.msg'] = 'message'
|
|
97
|
+
self.cache['user.section.msg'] = {'anykey': 'anydata'}
|
|
98
|
+
self.assertEqual(self.cache.get('user.section.msg'), {'anykey': 'anydata'})
|
|
99
|
+
|
|
100
|
+
def test_keys_and_values(self):
|
|
101
|
+
self.cache['anythingelse'] = 'anythingelse'
|
|
102
|
+
self.cache['user'] = 'user'
|
|
103
|
+
self.cache['user.section'] = ['user.section']
|
|
104
|
+
self.cache['user.section.msg0'] = {'msg': 0}
|
|
105
|
+
self.cache['user.section.msg1'] = {'msg': 1}
|
|
106
|
+
self.assertEqual(
|
|
107
|
+
sorted(self.cache.keys()),
|
|
108
|
+
[
|
|
109
|
+
'anythingelse',
|
|
110
|
+
'user',
|
|
111
|
+
'user.section',
|
|
112
|
+
'user.section.msg0',
|
|
113
|
+
'user.section.msg1',
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
self.assertCountEqual(
|
|
117
|
+
self.cache.values(),
|
|
118
|
+
['anythingelse', 'user', ['user.section'], {'msg': 0}, {'msg': 1}],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def test_delete_removes_empty_directories(self):
|
|
122
|
+
self.cache['user.section'] = 'section'
|
|
123
|
+
self.cache['user.section.msg0'] = 'msg0'
|
|
124
|
+
self.cache['user.section.msg1'] = 'msg1'
|
|
125
|
+
del self.cache['user.section.msg0']
|
|
126
|
+
self.assertEqual(self.listdir('user', 'section'), ['msg1.'])
|
|
127
|
+
del self.cache['user.section.msg1']
|
|
128
|
+
self.assertEqual(self.listdir('user'), ['section.'])
|
|
129
|
+
del self.cache['user.section']
|
|
130
|
+
self.assertEqual(self.listdir(), [])
|
|
131
|
+
|
|
132
|
+
def test_delete_removes_empty_directories_recursively(self):
|
|
133
|
+
self.cache['1.2.3.4'] = 'value'
|
|
134
|
+
self.assertEqual(self.listdir('1', '2', '3'), ['4.'])
|
|
135
|
+
del self.cache['1.2.3.4']
|
|
136
|
+
self.assertEqual(self.listdir(), [])
|
|
137
|
+
|
|
138
|
+
def test_delete_keeps_non_empty_directories(self):
|
|
139
|
+
self.cache['user.whatever.object'] = 'something'
|
|
140
|
+
self.cache['user.other'] = 'other'
|
|
141
|
+
del self.cache['user.whatever.object']
|
|
142
|
+
self.assertEqual(self.listdir(), ['user'])
|
|
143
|
+
self.assertEqual(self.listdir('user'), ['other.'])
|
|
144
|
+
|
|
145
|
+
def test_delete_missing_key(self):
|
|
146
|
+
del self.cache['notavailable']
|
|
147
|
+
del self.cache['not.available']
|
|
148
|
+
self.assertEqual(self.listdir(), [])
|
|
149
|
+
|
|
150
|
+
def test_reset(self):
|
|
151
|
+
for key in ['1', '2', '1.1', '1.1.1', '1.2.3.1', '1.2.3.2']:
|
|
152
|
+
self.cache[key] = ['something']
|
|
153
|
+
self.assertEqual(
|
|
154
|
+
sorted(self.cache.keys()), ['1', '1.1', '1.1.1', '1.2.3.1', '1.2.3.2', '2']
|
|
155
|
+
)
|
|
156
|
+
self.cache.reset()
|
|
157
|
+
self.assertEqual(self.cache.keys(), [])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestFSCacheManager(FSCacheTestCase):
|
|
161
|
+
def setUp(self):
|
|
162
|
+
super().setUp()
|
|
163
|
+
self.cache = FSCache(self.cachedir)
|
|
164
|
+
self.manager = ICacheManager(self.cache)
|
|
165
|
+
|
|
166
|
+
def test_adapter_registered(self):
|
|
167
|
+
self.assertIsInstance(self.manager, FSCacheManager)
|
|
168
|
+
self.assertIs(self.manager.cache, self.cache)
|
|
169
|
+
self.assertEqual(self.manager.timeout, 300)
|
|
170
|
+
|
|
171
|
+
def test_set_timeout(self):
|
|
172
|
+
self.manager.setTimeout(10)
|
|
173
|
+
self.assertEqual(self.manager.timeout, 10)
|
|
174
|
+
|
|
175
|
+
def test_set_and_get(self):
|
|
176
|
+
self.manager.set('foo', 'bar')
|
|
177
|
+
self.assertEqual(self.manager.get('foo'), 'bar')
|
|
178
|
+
self.assertEqual(list(self.cache['creationmap']), ['foo'])
|
|
179
|
+
|
|
180
|
+
def test_set_without_creationtime(self):
|
|
181
|
+
# Items without creation time are considered as timed out.
|
|
182
|
+
self.manager.set('foo', 'bar', set_creationtime=False)
|
|
183
|
+
self.assertEqual(self.cache['foo'], 'bar')
|
|
184
|
+
self.assertIsNone(self.cache['creationmap'])
|
|
185
|
+
self.assertIsNone(self.manager.get('foo'))
|
|
186
|
+
self.assertIsNone(self.cache['foo'])
|
|
187
|
+
|
|
188
|
+
def test_get_missing_creationtime(self):
|
|
189
|
+
self.manager.set('foo', 'bar')
|
|
190
|
+
self.manager.set('baz', 'qux', set_creationtime=False)
|
|
191
|
+
self.assertIsNone(self.manager.get('baz'))
|
|
192
|
+
self.assertIsNone(self.cache['baz'])
|
|
193
|
+
self.assertEqual(list(self.cache['creationmap']), ['foo'])
|
|
194
|
+
|
|
195
|
+
def test_get_timed_out(self):
|
|
196
|
+
self.manager.set('foo', 'bar')
|
|
197
|
+
with mock.patch('time.time', return_value=time.time() + 301):
|
|
198
|
+
self.assertIsNone(self.manager.get('foo'))
|
|
199
|
+
self.assertIsNone(self.cache['foo'])
|
|
200
|
+
self.assertEqual(self.cache['creationmap'], {})
|
|
201
|
+
|
|
202
|
+
def test_get_force_reload(self):
|
|
203
|
+
self.manager.set('foo', 'bar')
|
|
204
|
+
self.assertIsNone(self.manager.get('foo', force_reload=True))
|
|
205
|
+
self.assertIsNone(self.cache['foo'])
|
|
206
|
+
self.assertEqual(self.cache['creationmap'], {})
|
|
207
|
+
|
|
208
|
+
def test_get_data(self):
|
|
209
|
+
calls = []
|
|
210
|
+
|
|
211
|
+
def square(x):
|
|
212
|
+
calls.append(x)
|
|
213
|
+
return x * x
|
|
214
|
+
|
|
215
|
+
self.assertEqual(self.manager.getData(square, '4', args=[4]), 16)
|
|
216
|
+
self.assertEqual(self.manager.getData(square, '4', args=[4]), 16)
|
|
217
|
+
self.assertEqual(calls, [4])
|
|
218
|
+
self.assertEqual(self.manager.getData(square, '4', True, args=[4]), 16)
|
|
219
|
+
self.assertEqual(self.manager.getData(square, '5', kwargs={'x': 5}), 25)
|
|
220
|
+
self.assertEqual(calls, [4, 4, 5])
|
|
221
|
+
|
|
222
|
+
def test_rem(self):
|
|
223
|
+
self.manager.set('foo', 'bar')
|
|
224
|
+
self.manager.set('baz', 'qux')
|
|
225
|
+
self.manager.rem('foo')
|
|
226
|
+
self.assertIsNone(self.cache['foo'])
|
|
227
|
+
self.assertEqual(list(self.cache['creationmap']), ['baz'])
|
|
228
|
+
del self.manager['baz']
|
|
229
|
+
self.assertIsNone(self.cache['baz'])
|
|
230
|
+
self.assertEqual(self.cache['creationmap'], {})
|
|
231
|
+
|
|
232
|
+
def test_rem_without_creationtime(self):
|
|
233
|
+
self.manager.set('foo', 'bar', set_creationtime=False)
|
|
234
|
+
self.manager.rem('foo')
|
|
235
|
+
self.assertIsNone(self.cache['foo'])
|
|
236
|
+
self.assertIsNone(self.cache['creationmap'])
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from bda.cache import ICacheManager
|
|
2
|
+
from bda.cache import Memcached
|
|
3
|
+
from bda.cache import memcached
|
|
4
|
+
from bda.cache import MemcachedException
|
|
5
|
+
from bda.cache import MemcachedManager
|
|
6
|
+
from bda.cache.interfaces import IMemcachedProvider
|
|
7
|
+
from unittest import mock
|
|
8
|
+
import importlib.util
|
|
9
|
+
import sys
|
|
10
|
+
import types
|
|
11
|
+
import unittest
|
|
12
|
+
import zlib
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SERVERS = ['127.0.0.1:11211']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FakeClient:
|
|
19
|
+
"""In-memory replacement for ``memcache.Client``.
|
|
20
|
+
|
|
21
|
+
Keeps the tests independent of a running memcached server.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
stats = [
|
|
25
|
+
('127.0.0.1:11211 (1)', {'bytes': '40'}),
|
|
26
|
+
('127.0.0.1:11212 (1)', {'bytes': '25'}),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
def __init__(self, servers):
|
|
30
|
+
self.servers = servers
|
|
31
|
+
self.data = {}
|
|
32
|
+
self.expiration_times = {}
|
|
33
|
+
|
|
34
|
+
def get(self, key):
|
|
35
|
+
return self.data.get(key)
|
|
36
|
+
|
|
37
|
+
def set(self, key, value, time=0):
|
|
38
|
+
self.data[key] = value
|
|
39
|
+
self.expiration_times[key] = time
|
|
40
|
+
|
|
41
|
+
def delete(self, key):
|
|
42
|
+
self.data.pop(key, None)
|
|
43
|
+
|
|
44
|
+
def flush_all(self):
|
|
45
|
+
self.data.clear()
|
|
46
|
+
|
|
47
|
+
def get_stats(self):
|
|
48
|
+
return self.stats
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def patch_python_memcached(testcase):
|
|
52
|
+
patcher = mock.patch.multiple(
|
|
53
|
+
memcached, Client=FakeClient, LIBMC=False, PYLIBMC=False
|
|
54
|
+
)
|
|
55
|
+
patcher.start()
|
|
56
|
+
testcase.addCleanup(patcher.stop)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestMemcached(unittest.TestCase):
|
|
60
|
+
def setUp(self):
|
|
61
|
+
patch_python_memcached(self)
|
|
62
|
+
self.provider = Memcached(SERVERS)
|
|
63
|
+
self.client = self.provider._client
|
|
64
|
+
|
|
65
|
+
def test_init(self):
|
|
66
|
+
self.assertTrue(IMemcachedProvider.providedBy(self.provider))
|
|
67
|
+
self.assertIsInstance(self.client, FakeClient)
|
|
68
|
+
self.assertEqual(self.client.servers, SERVERS)
|
|
69
|
+
self.assertEqual(self.provider.timeout, 0)
|
|
70
|
+
|
|
71
|
+
def test_set_and_get(self):
|
|
72
|
+
self.assertIsNone(self.provider['somekey'])
|
|
73
|
+
self.assertEqual(self.provider.get('somekey', 'default'), 'default')
|
|
74
|
+
self.provider['somekey'] = 'somevalue'
|
|
75
|
+
self.assertEqual(self.provider['somekey'], 'somevalue')
|
|
76
|
+
self.assertEqual(self.provider.get('somekey', 'default'), 'somevalue')
|
|
77
|
+
|
|
78
|
+
def test_timeout_is_passed_as_expiration_time(self):
|
|
79
|
+
self.provider['1'] = 1
|
|
80
|
+
self.provider.timeout = 2
|
|
81
|
+
self.provider['2'] = 2
|
|
82
|
+
self.assertEqual(self.client.expiration_times, {'1': 0, '2': 2})
|
|
83
|
+
|
|
84
|
+
def test_delete(self):
|
|
85
|
+
self.provider['somekey'] = 'somevalue'
|
|
86
|
+
del self.provider['somekey']
|
|
87
|
+
self.assertIsNone(self.provider['somekey'])
|
|
88
|
+
|
|
89
|
+
def test_reset(self):
|
|
90
|
+
self.provider['1'] = 'Eins'
|
|
91
|
+
self.provider['2'] = 'Zwei'
|
|
92
|
+
self.provider.reset()
|
|
93
|
+
self.assertEqual((self.provider['1'], self.provider['2']), (None, None))
|
|
94
|
+
|
|
95
|
+
def test_size_sums_up_bytes_of_all_servers(self):
|
|
96
|
+
self.assertEqual(self.provider.size(), 65)
|
|
97
|
+
|
|
98
|
+
def test_keys_not_supported(self):
|
|
99
|
+
with self.assertRaises(MemcachedException) as cm:
|
|
100
|
+
self.provider.keys()
|
|
101
|
+
self.assertEqual(
|
|
102
|
+
str(cm.exception), "It's not possible to fetch keys from memcached"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def test_values_not_supported(self):
|
|
106
|
+
with self.assertRaises(MemcachedException) as cm:
|
|
107
|
+
self.provider.values()
|
|
108
|
+
self.assertEqual(
|
|
109
|
+
str(cm.exception), "It's not possible to fetch values from memcached"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestMemcachedManager(unittest.TestCase):
|
|
114
|
+
def setUp(self):
|
|
115
|
+
patch_python_memcached(self)
|
|
116
|
+
self.provider = Memcached(SERVERS)
|
|
117
|
+
self.manager = ICacheManager(self.provider)
|
|
118
|
+
|
|
119
|
+
def test_adapter_registered(self):
|
|
120
|
+
self.assertIsInstance(self.manager, MemcachedManager)
|
|
121
|
+
self.assertIs(self.manager.cache, self.provider)
|
|
122
|
+
|
|
123
|
+
def test_set_timeout(self):
|
|
124
|
+
self.manager.setTimeout(10)
|
|
125
|
+
self.assertEqual(self.provider.timeout, 10)
|
|
126
|
+
|
|
127
|
+
def test_set_and_get(self):
|
|
128
|
+
self.manager.set('AnotherKey', 'AnotherValue')
|
|
129
|
+
self.assertEqual(self.manager.get('AnotherKey'), 'AnotherValue')
|
|
130
|
+
|
|
131
|
+
def test_get_force_reload(self):
|
|
132
|
+
self.manager.set('AnotherKey', 'AnotherValue')
|
|
133
|
+
self.assertIsNone(self.manager.get('AnotherKey', True))
|
|
134
|
+
self.assertIsNone(self.manager.get('AnotherKey'))
|
|
135
|
+
|
|
136
|
+
def test_get_data(self):
|
|
137
|
+
calls = []
|
|
138
|
+
|
|
139
|
+
def square(x):
|
|
140
|
+
calls.append(x)
|
|
141
|
+
return x * x
|
|
142
|
+
|
|
143
|
+
self.assertEqual(self.manager.getData(square, '4', args=[4]), 16)
|
|
144
|
+
self.assertEqual(self.manager.getData(square, '4', args=[4]), 16)
|
|
145
|
+
self.assertEqual(calls, [4])
|
|
146
|
+
self.assertEqual(self.manager.getData(square, '4', True, args=[4]), 16)
|
|
147
|
+
self.assertEqual(self.manager.getData(square, '5', True, kwargs={'x': 5}), 25)
|
|
148
|
+
self.assertEqual(calls, [4, 4, 5])
|
|
149
|
+
self.assertEqual(self.manager.get('5'), 25)
|
|
150
|
+
|
|
151
|
+
def test_delete(self):
|
|
152
|
+
self.manager.set('5', 25)
|
|
153
|
+
del self.manager['5']
|
|
154
|
+
self.assertIsNone(self.manager.get('5'))
|
|
155
|
+
|
|
156
|
+
def test_rem(self):
|
|
157
|
+
self.manager.set('5', 25)
|
|
158
|
+
self.manager.rem('5')
|
|
159
|
+
self.assertIsNone(self.manager.get('5'))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def fake_module(name):
|
|
163
|
+
module = types.ModuleType(name)
|
|
164
|
+
module.Client = mock.Mock(name=f'{name}.Client')
|
|
165
|
+
return module
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_memcached_module(**modules):
|
|
169
|
+
"""Load a fresh copy of ``bda.cache.memcached``.
|
|
170
|
+
|
|
171
|
+
The client library is selected at import time, the given ``modules``
|
|
172
|
+
replace the installed ones meanwhile, ``None`` means not installed. The
|
|
173
|
+
copy is not added to ``sys.modules`` and does not register its adapter.
|
|
174
|
+
"""
|
|
175
|
+
spec = importlib.util.find_spec('bda.cache.memcached')
|
|
176
|
+
module = importlib.util.module_from_spec(spec)
|
|
177
|
+
with (
|
|
178
|
+
mock.patch.dict(sys.modules, modules),
|
|
179
|
+
mock.patch('zope.component.provideAdapter') as provide_adapter,
|
|
180
|
+
):
|
|
181
|
+
spec.loader.exec_module(module)
|
|
182
|
+
provide_adapter.assert_called_once_with(module.MemcachedManager)
|
|
183
|
+
return module
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestClientLibrarySelection(unittest.TestCase):
|
|
187
|
+
def test_python_memcached(self):
|
|
188
|
+
memcache = fake_module('memcache')
|
|
189
|
+
module = load_memcached_module(libmc=None, pylibmc=None, memcache=memcache)
|
|
190
|
+
self.assertFalse(module.LIBMC)
|
|
191
|
+
self.assertFalse(module.PYLIBMC)
|
|
192
|
+
provider = module.Memcached(SERVERS)
|
|
193
|
+
memcache.Client.assert_called_once_with(SERVERS)
|
|
194
|
+
provider['key'] = 'value'
|
|
195
|
+
provider._client.set.assert_called_once_with('key', 'value', time=0)
|
|
196
|
+
|
|
197
|
+
def test_libmc(self):
|
|
198
|
+
libmc = fake_module('libmc')
|
|
199
|
+
module = load_memcached_module(libmc=libmc, pylibmc=None)
|
|
200
|
+
self.assertTrue(module.LIBMC)
|
|
201
|
+
self.assertFalse(module.PYLIBMC)
|
|
202
|
+
provider = module.Memcached(SERVERS)
|
|
203
|
+
libmc.Client.assert_called_once_with(SERVERS, comp_threshold=10240, noreply=True)
|
|
204
|
+
provider['key'] = 'value'
|
|
205
|
+
provider._client.set.assert_called_once_with('key', 'value', time=0)
|
|
206
|
+
|
|
207
|
+
def test_pylibmc(self):
|
|
208
|
+
pylibmc = fake_module('pylibmc')
|
|
209
|
+
module = load_memcached_module(libmc=None, pylibmc=pylibmc)
|
|
210
|
+
self.assertFalse(module.LIBMC)
|
|
211
|
+
self.assertTrue(module.PYLIBMC)
|
|
212
|
+
provider = module.Memcached(SERVERS)
|
|
213
|
+
pylibmc.Client.assert_called_once_with(SERVERS)
|
|
214
|
+
provider['key'] = 'value'
|
|
215
|
+
provider._client.set.assert_called_once_with(
|
|
216
|
+
'key',
|
|
217
|
+
'value',
|
|
218
|
+
time=0,
|
|
219
|
+
min_compress_len=1024000,
|
|
220
|
+
compress_level=zlib.Z_BEST_SPEED,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def test_libmc_is_preferred_over_pylibmc(self):
|
|
224
|
+
libmc = fake_module('libmc')
|
|
225
|
+
pylibmc = fake_module('pylibmc')
|
|
226
|
+
module = load_memcached_module(libmc=libmc, pylibmc=pylibmc)
|
|
227
|
+
provider = module.Memcached(SERVERS)
|
|
228
|
+
libmc.Client.assert_called_once_with(SERVERS, comp_threshold=10240, noreply=True)
|
|
229
|
+
pylibmc.Client.assert_not_called()
|
|
230
|
+
provider['key'] = 'value'
|
|
231
|
+
provider._client.set.assert_called_once_with('key', 'value', time=0)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from bda.cache import ICacheManager
|
|
2
|
+
from bda.cache import NullCache
|
|
3
|
+
from bda.cache import NullCacheManager
|
|
4
|
+
from bda.cache.interfaces import INullCacheProvider
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestNullCache(unittest.TestCase):
|
|
9
|
+
def test_provides_null_cache_provider(self):
|
|
10
|
+
self.assertTrue(INullCacheProvider.providedBy(NullCache()))
|
|
11
|
+
|
|
12
|
+
def test_is_always_empty(self):
|
|
13
|
+
provider = NullCache()
|
|
14
|
+
provider['foo'] = object()
|
|
15
|
+
self.assertIsNone(provider['foo'])
|
|
16
|
+
self.assertIsNone(provider.get('foo'))
|
|
17
|
+
self.assertIsNone(provider.get('foo', 'default'))
|
|
18
|
+
self.assertEqual(provider.size(), 0)
|
|
19
|
+
provider.reset()
|
|
20
|
+
del provider['foo']
|
|
21
|
+
|
|
22
|
+
def test_keys_not_supported(self):
|
|
23
|
+
with self.assertRaises(NotImplementedError) as cm:
|
|
24
|
+
NullCache().keys()
|
|
25
|
+
self.assertEqual(
|
|
26
|
+
str(cm.exception), "It's not possible to fetch keys from nothing"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def test_values_not_supported(self):
|
|
30
|
+
with self.assertRaises(NotImplementedError) as cm:
|
|
31
|
+
NullCache().values()
|
|
32
|
+
self.assertEqual(
|
|
33
|
+
str(cm.exception), "It's not possible to fetch values from nothing"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestNullCacheManager(unittest.TestCase):
|
|
38
|
+
def test_adapter_registered(self):
|
|
39
|
+
manager = ICacheManager(NullCache())
|
|
40
|
+
self.assertIsInstance(manager, NullCacheManager)
|
|
41
|
+
|
|
42
|
+
def test_never_caches(self):
|
|
43
|
+
manager = ICacheManager(NullCache())
|
|
44
|
+
manager.setTimeout(0)
|
|
45
|
+
manager.set('foo', object())
|
|
46
|
+
self.assertIsNone(manager.get('foo'))
|
|
47
|
+
manager.rem('foo')
|
|
48
|
+
del manager['foo']
|
|
49
|
+
|
|
50
|
+
def test_get_data_always_calls_function(self):
|
|
51
|
+
calls = []
|
|
52
|
+
|
|
53
|
+
def func(*args, **kwargs):
|
|
54
|
+
calls.append((args, kwargs))
|
|
55
|
+
return 'result'
|
|
56
|
+
|
|
57
|
+
manager = ICacheManager(NullCache())
|
|
58
|
+
result = manager.getData(func, 'foo', args=[1], kwargs={'a': 2})
|
|
59
|
+
self.assertEqual(result, 'result')
|
|
60
|
+
manager.getData(func, 'foo', args=[1], kwargs={'a': 2})
|
|
61
|
+
self.assertEqual(calls, [((1,), {'a': 2}), ((1,), {'a': 2})])
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bda.cache
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Simple caching infrastructure.
|
|
5
|
+
Project-URL: Homepage, https://github.com/bluedynamics/bda.cache
|
|
6
|
+
Author-email: Robert Niederreiter <rnix@squarewave.at>
|
|
7
|
+
License: GNU General Public License v2 or later (GPLv2+)
|
|
8
|
+
Keywords: caching,fscache,memcached,zca,zope
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: python-memcached
|
|
22
|
+
Requires-Dist: zope-component
|
|
23
|
+
Provides-Extra: libmc
|
|
24
|
+
Requires-Dist: libmc; extra == 'libmc'
|
|
25
|
+
Provides-Extra: pylibmc
|
|
26
|
+
Requires-Dist: pylibmc; extra == 'pylibmc'
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest; extra == 'test'
|
|
29
|
+
Description-Content-Type: text/x-rst
|
|
30
|
+
|
|
31
|
+
.. image:: https://img.shields.io/pypi/v/bda.cache.svg
|
|
32
|
+
:target: https://pypi.python.org/pypi/bda.cache
|
|
33
|
+
:alt: Latest PyPI version
|
|
34
|
+
|
|
35
|
+
.. image:: https://img.shields.io/pypi/dm/bda.cache.svg
|
|
36
|
+
:target: https://pypi.python.org/pypi/bda.cache
|
|
37
|
+
:alt: Number of PyPI downloads
|
|
38
|
+
|
|
39
|
+
.. image:: https://github.com/bluedynamics/bda.cache/actions/workflows/test.yml/badge.svg
|
|
40
|
+
:target: https://github.com/bluedynamics/bda.cache/actions/workflows/test.yml
|
|
41
|
+
:alt: Test bda.cache
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
General Caching API
|
|
45
|
+
===================
|
|
46
|
+
|
|
47
|
+
This package is designed to be used by applications which require different
|
|
48
|
+
kinds of caching flavour. This is abstracted due to the interfaces
|
|
49
|
+
``ICacheProvider`` and ``ICacheManager``. ICacheProvider takes care of the
|
|
50
|
+
concrete cache implementation, ICacheManager is the read/write interface.
|
|
51
|
+
|
|
52
|
+
The convention is to adapt a concrete ICacheProvider implementation
|
|
53
|
+
|
|
54
|
+
.. code-block:: python
|
|
55
|
+
|
|
56
|
+
from bda.cache import ICacheManager
|
|
57
|
+
from bda.cache import Memcached
|
|
58
|
+
|
|
59
|
+
provider = Memcached(['127.0.0.1:11211'])
|
|
60
|
+
manager = ICacheManager(provider)
|
|
61
|
+
|
|
62
|
+
We can ask the manager for data inside the cache
|
|
63
|
+
|
|
64
|
+
.. code-block:: python
|
|
65
|
+
|
|
66
|
+
data = manager.get('somekey', force_reload=False)
|
|
67
|
+
|
|
68
|
+
If ``force_reload`` is set to True, try to delete data with key from cache and
|
|
69
|
+
return None.
|
|
70
|
+
|
|
71
|
+
We're also able to manipulate the cache's data (some restrictions my result out
|
|
72
|
+
of backend not providing them)
|
|
73
|
+
|
|
74
|
+
.. code-block:: python
|
|
75
|
+
|
|
76
|
+
manager.set('somekey', object())
|
|
77
|
+
|
|
78
|
+
There exist a convenience to use some API's and cache it's results all at once.
|
|
79
|
+
|
|
80
|
+
.. code-block:: python
|
|
81
|
+
|
|
82
|
+
data = manager.getData(func, key, force_reload=False, args=[], kwargs={})
|
|
83
|
+
|
|
84
|
+
First the data for ``key`` is looked up inside the cache provider. If not found
|
|
85
|
+
there or if ``force_reload`` is set to True, call given ``func`` . The return
|
|
86
|
+
value of this function will be stored in the cache provider. ``args`` and
|
|
87
|
+
``kwargs`` are passed as parameters to given function if it's call is required.
|
|
88
|
+
|
|
89
|
+
You might ask why all this is done due to 2 seperate interfaces...
|
|
90
|
+
|
|
91
|
+
Some usecase might require different cache providers i.e. for different payload
|
|
92
|
+
size. This way you can implement any other cache provider usage as well due to
|
|
93
|
+
the cache manager interface.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
Dependencies
|
|
97
|
+
------------
|
|
98
|
+
|
|
99
|
+
- ``zope.interface``
|
|
100
|
+
|
|
101
|
+
- ``zope.component``
|
|
102
|
+
|
|
103
|
+
- ``bda.cache.memcached.Memcached`` requires ``python-memcached``
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
Notes
|
|
107
|
+
-----
|
|
108
|
+
|
|
109
|
+
If you're interessted to contribute; Feel free but keep in mind that this code
|
|
110
|
+
is planned to be released under a BSD like licence in future.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
Changes
|
|
114
|
+
=======
|
|
115
|
+
|
|
116
|
+
2.0.0 (2026-10-31)
|
|
117
|
+
------------------
|
|
118
|
+
|
|
119
|
+
- Fix the client library selection in ``bda.cache.memcached`` if both
|
|
120
|
+
``libmc`` and ``pylibmc`` are installed. ``pylibmc.Client`` was initialized
|
|
121
|
+
with the ``libmc`` arguments. ``libmc`` is preferred now, as intended.
|
|
122
|
+
[rnix]
|
|
123
|
+
|
|
124
|
+
- Drop the Python 2 remnants: the ``six`` imports in ``bda.cache.fscache``
|
|
125
|
+
and ``bda.cache.memcached`` (``six`` never was a declared dependency),
|
|
126
|
+
``class X(object)`` and the ``IOError`` alias. Remove the unreachable code
|
|
127
|
+
in ``FSCache.__delitem__`` and the unused ``ICacheProvider`` import in
|
|
128
|
+
``bda.cache.memcached``. Behaviour unchanged.
|
|
129
|
+
[rnix]
|
|
130
|
+
|
|
131
|
+
- Convert the doctests to unittest tests and use pytest as test runner. The
|
|
132
|
+
memcached tests use a fake client and no longer need a running memcached
|
|
133
|
+
server. Drop the ``interlude`` and ``zope.testing`` test dependencies.
|
|
134
|
+
[rnix]
|
|
135
|
+
|
|
136
|
+
- Sort the imports with ruff (one per line, ``from`` imports first, no
|
|
137
|
+
sections) and format the code with ``ruff format`` (90 characters per line,
|
|
138
|
+
single quotes).
|
|
139
|
+
[rnix]
|
|
140
|
+
|
|
141
|
+
- Add ``Makefile`` (mxmake) with ``qa.ruff``, ``qa.test`` and ``qa.coverage``
|
|
142
|
+
domains, pin the ruff configuration in ``pyproject.toml`` and add a GitHub
|
|
143
|
+
Actions test workflow.
|
|
144
|
+
[rnix]
|
|
145
|
+
|
|
146
|
+
- Refactor package layout to use ``pyproject.toml`` and implicit namespace
|
|
147
|
+
packages.
|
|
148
|
+
[rnix]
|
|
149
|
+
|
|
150
|
+
- Drop Python 2 support. Python >= 3.10 is required.
|
|
151
|
+
[rnix]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
1.3.0 (2019-02-05)
|
|
155
|
+
------------------
|
|
156
|
+
|
|
157
|
+
- Python 3 comaptibility [reinhardt]
|
|
158
|
+
|
|
159
|
+
- Code style (black/isort) [jensens]
|
|
160
|
+
|
|
161
|
+
1.2.0 (2017-10-19)
|
|
162
|
+
------------------
|
|
163
|
+
|
|
164
|
+
- Change to use libmc or pylibmc when available
|
|
165
|
+
|
|
166
|
+
- Add option to require with bda.cache[pylibmc] or bda.cache[libmc]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
1.1.2 (2009-02-10)
|
|
170
|
+
------------------
|
|
171
|
+
|
|
172
|
+
- remove legacy code
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
1.1.1 (2009-02-10)
|
|
176
|
+
------------------
|
|
177
|
+
|
|
178
|
+
- bugfix in zcml configuration.
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
1.1 (2009-02-09)
|
|
182
|
+
----------------
|
|
183
|
+
|
|
184
|
+
- Cache Managers are now adapters.
|
|
185
|
+
|
|
186
|
+
- Implement memcached server support
|
|
187
|
+
|
|
188
|
+
- deprecate the default CacheManager
|
|
189
|
+
|
|
190
|
+
- Fix tests for FSCache
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
1.0
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
- Initial work
|
|
197
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
bda/cache/__init__.py,sha256=6F3RtJXBWpmpqAXJJUd2TtazSn64-IJhWxtLz6p-lO0,715
|
|
2
|
+
bda/cache/configure.zcml,sha256=I1CysdnqB3ak2tfReSKlnQPaEySG8f9deffbFCD0fEo,106
|
|
3
|
+
bda/cache/fscache.py,sha256=GqU3eqCkYlG_mDXhkAXrR9uf5B4-CgdegprLP2aavX8,7869
|
|
4
|
+
bda/cache/interfaces.py,sha256=9KU8Brhy6-8mDgaahuI91pXve7emyhNZhqDgkuJ11jw,2516
|
|
5
|
+
bda/cache/memcached.py,sha256=LkRoRyCWWAKDwwbtUVGTcyXlEeYOUfLProcYKjP2--Q,3351
|
|
6
|
+
bda/cache/nullcache.py,sha256=I-RNavUCQ2kCibdFAklUrTcAScdDYT715X2dWBvvHWQ,1522
|
|
7
|
+
bda/cache/tests/__init__.py,sha256=MsSFjiLMLJZ7QhUPpVBWKiyDnCzryquRyr329NoCACI,2
|
|
8
|
+
bda/cache/tests/test_fscache.py,sha256=BkSkSdPp4YZxBUVLDtBsWLMUKn18U0ap1pwBxWf_-Og,9013
|
|
9
|
+
bda/cache/tests/test_memcached.py,sha256=eWugOcIHSayKx_snQM8nHMjFriK4jLg-3b_tCZ2rKgs,7858
|
|
10
|
+
bda/cache/tests/test_nullcache.py,sha256=bQEEgJqBy2-QMuncrsiFrgDbw6Do1XgbGYD9EyKHnCA,2085
|
|
11
|
+
bda_cache-2.0.0.dist-info/METADATA,sha256=CYGfIWRUk-WUbSbfhnezuePUndGspeTWIhaSNdC4q2E,5747
|
|
12
|
+
bda_cache-2.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
13
|
+
bda_cache-2.0.0.dist-info/RECORD,,
|