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 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
@@ -0,0 +1,5 @@
1
+ <configure xmlns="http://namespaces.zope.org/zope">
2
+
3
+ <!-- DEPRECATED - REMOVED IN FUTURE -->
4
+
5
+ </configure>
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)
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any