[3] | 1 | """Cache object |
---|
| 2 | |
---|
| 3 | The Cache object is used to manage a set of cache files and their |
---|
| 4 | associated backend. The backends can be rotated on the fly by |
---|
| 5 | specifying an alternate type when used. |
---|
| 6 | |
---|
| 7 | Advanced users can add new backends in beaker.backends |
---|
| 8 | |
---|
| 9 | """ |
---|
| 10 | import pkg_resources |
---|
| 11 | import warnings |
---|
| 12 | |
---|
| 13 | import beaker.container as container |
---|
| 14 | import beaker.util as util |
---|
| 15 | from beaker.exceptions import BeakerException, InvalidCacheBackendError |
---|
| 16 | |
---|
| 17 | # Initialize the basic available backends |
---|
| 18 | clsmap = { |
---|
| 19 | 'memory':container.MemoryNamespaceManager, |
---|
| 20 | 'dbm':container.DBMNamespaceManager, |
---|
| 21 | 'file':container.FileNamespaceManager, |
---|
| 22 | } |
---|
| 23 | |
---|
| 24 | |
---|
| 25 | # Load up the additional entry point defined backends |
---|
| 26 | for entry_point in pkg_resources.iter_entry_points('beaker.backends'): |
---|
| 27 | try: |
---|
| 28 | NamespaceManager = entry_point.load() |
---|
| 29 | name = entry_point.name |
---|
| 30 | if name in clsmap: |
---|
| 31 | raise BeakerException("NamespaceManager name conflict,'%s' " |
---|
| 32 | "already loaded" % name) |
---|
| 33 | clsmap[name] = NamespaceManager |
---|
| 34 | except (InvalidCacheBackendError, SyntaxError): |
---|
| 35 | # Ignore invalid backends |
---|
| 36 | pass |
---|
| 37 | except: |
---|
| 38 | import sys |
---|
| 39 | from pkg_resources import DistributionNotFound |
---|
| 40 | # Warn when there's a problem loading a NamespaceManager |
---|
| 41 | if not isinstance(sys.exc_info()[1], DistributionNotFound): |
---|
| 42 | import traceback |
---|
| 43 | from StringIO import StringIO |
---|
| 44 | tb = StringIO() |
---|
| 45 | traceback.print_exc(file=tb) |
---|
| 46 | warnings.warn("Unable to load NamespaceManager entry point: '%s': " |
---|
| 47 | "%s" % (entry_point, tb.getvalue()), RuntimeWarning, |
---|
| 48 | 2) |
---|
| 49 | |
---|
| 50 | |
---|
| 51 | # Load legacy-style backends |
---|
| 52 | try: |
---|
| 53 | import beaker.ext.memcached as memcached |
---|
| 54 | clsmap['ext:memcached'] = memcached.MemcachedNamespaceManager |
---|
| 55 | except InvalidCacheBackendError, e: |
---|
| 56 | clsmap['ext:memcached'] = e |
---|
| 57 | |
---|
| 58 | try: |
---|
| 59 | import beaker.ext.database as database |
---|
| 60 | clsmap['ext:database'] = database.DatabaseNamespaceManager |
---|
| 61 | except InvalidCacheBackendError, e: |
---|
| 62 | clsmap['ext:database'] = e |
---|
| 63 | |
---|
| 64 | try: |
---|
| 65 | import beaker.ext.sqla as sqla |
---|
| 66 | clsmap['ext:sqla'] = sqla.SqlaNamespaceManager |
---|
| 67 | except InvalidCacheBackendError, e: |
---|
| 68 | clsmap['ext:sqla'] = e |
---|
| 69 | |
---|
| 70 | try: |
---|
| 71 | import beaker.ext.google as google |
---|
| 72 | clsmap['ext:google'] = google.GoogleNamespaceManager |
---|
| 73 | except (InvalidCacheBackendError, SyntaxError), e: |
---|
| 74 | clsmap['ext:google'] = e |
---|
| 75 | |
---|
| 76 | |
---|
| 77 | class Cache(object): |
---|
| 78 | """Front-end to the containment API implementing a data cache. |
---|
| 79 | |
---|
| 80 | ``namespace`` |
---|
| 81 | the namespace of this Cache |
---|
| 82 | |
---|
| 83 | ``type`` |
---|
| 84 | type of cache to use |
---|
| 85 | |
---|
| 86 | ``expire`` |
---|
| 87 | seconds to keep cached data |
---|
| 88 | |
---|
| 89 | ``expiretime`` |
---|
| 90 | seconds to keep cached data (legacy support) |
---|
| 91 | |
---|
| 92 | ``starttime`` |
---|
| 93 | time when cache was cache was |
---|
| 94 | """ |
---|
| 95 | def __init__(self, namespace, type='memory', expiretime=None, |
---|
| 96 | starttime=None, expire=None, **nsargs): |
---|
| 97 | try: |
---|
| 98 | cls = clsmap[type] |
---|
| 99 | if isinstance(cls, InvalidCacheBackendError): |
---|
| 100 | raise cls |
---|
| 101 | except KeyError: |
---|
| 102 | raise TypeError("Unknown cache implementation %r" % type) |
---|
| 103 | |
---|
| 104 | self.namespace = cls(namespace, **nsargs) |
---|
| 105 | self.expiretime = expiretime or expire |
---|
| 106 | self.starttime = starttime |
---|
| 107 | self.nsargs = nsargs |
---|
| 108 | |
---|
| 109 | def put(self, key, value, **kw): |
---|
| 110 | self._get_value(key, **kw).set_value(value) |
---|
| 111 | set_value = put |
---|
| 112 | |
---|
| 113 | def get(self, key, **kw): |
---|
| 114 | """Retrieve a cached value from the container""" |
---|
| 115 | return self._get_value(key, **kw).get_value() |
---|
| 116 | get_value = get |
---|
| 117 | |
---|
| 118 | def remove_value(self, key, **kw): |
---|
| 119 | mycontainer = self._get_value(key, **kw) |
---|
| 120 | if mycontainer.has_current_value(): |
---|
| 121 | mycontainer.clear_value() |
---|
| 122 | remove = remove_value |
---|
| 123 | |
---|
| 124 | def _get_value(self, key, **kw): |
---|
| 125 | if isinstance(key, unicode): |
---|
| 126 | key = key.encode('ascii', 'backslashreplace') |
---|
| 127 | |
---|
| 128 | if 'type' in kw: |
---|
| 129 | return self._legacy_get_value(key, **kw) |
---|
| 130 | |
---|
| 131 | kw.setdefault('expiretime', self.expiretime) |
---|
| 132 | kw.setdefault('starttime', self.starttime) |
---|
| 133 | |
---|
| 134 | return container.Value(key, self.namespace, **kw) |
---|
| 135 | |
---|
| 136 | def _legacy_get_value(self, key, type, **kw): |
---|
| 137 | expiretime = kw.pop('expiretime', self.expiretime) |
---|
| 138 | starttime = kw.pop('starttime', None) |
---|
| 139 | createfunc = kw.pop('createfunc', None) |
---|
| 140 | kwargs = self.nsargs.copy() |
---|
| 141 | kwargs.update(kw) |
---|
| 142 | c = Cache(self.namespace.namespace, type=type, **kwargs) |
---|
| 143 | return c._get_value(key, expiretime=expiretime, createfunc=createfunc, |
---|
| 144 | starttime=starttime) |
---|
| 145 | _legacy_get_value = util.deprecated(_legacy_get_value, "Specifying a " |
---|
| 146 | "'type' and other namespace configuration with cache.get()/put()/etc. " |
---|
| 147 | "is deprecated. Specify 'type' and other namespace configuration to " |
---|
| 148 | "cache_manager.get_cache() and/or the Cache constructor instead.") |
---|
| 149 | |
---|
| 150 | def clear(self): |
---|
| 151 | """Clear all the values from the namespace""" |
---|
| 152 | self.namespace.remove() |
---|
| 153 | |
---|
| 154 | # dict interface |
---|
| 155 | def __getitem__(self, key): |
---|
| 156 | return self.get(key) |
---|
| 157 | |
---|
| 158 | def __contains__(self, key): |
---|
| 159 | return self._get_value(key).has_current_value() |
---|
| 160 | |
---|
| 161 | def has_key(self, key): |
---|
| 162 | return key in self |
---|
| 163 | |
---|
| 164 | def __delitem__(self, key): |
---|
| 165 | self.remove_value(key) |
---|
| 166 | |
---|
| 167 | def __setitem__(self, key, value): |
---|
| 168 | self.put(key, value) |
---|
| 169 | |
---|
| 170 | |
---|
| 171 | class CacheManager(object): |
---|
| 172 | def __init__(self, **kwargs): |
---|
| 173 | """Initialize a CacheManager object with a set of options |
---|
| 174 | |
---|
| 175 | Options should be parsed with the |
---|
| 176 | :func:`~beaker.util.parse_cache_config_options` function to |
---|
| 177 | ensure only valid options are used. |
---|
| 178 | |
---|
| 179 | """ |
---|
| 180 | self.kwargs = kwargs |
---|
| 181 | self.caches = {} |
---|
| 182 | self.regions = kwargs.pop('cache_regions', {}) |
---|
| 183 | |
---|
| 184 | def get_cache(self, name, **kwargs): |
---|
| 185 | kw = self.kwargs.copy() |
---|
| 186 | kw.update(kwargs) |
---|
| 187 | return self.caches.setdefault(name + str(kw), Cache(name, **kw)) |
---|
| 188 | |
---|
| 189 | def get_cache_region(self, name, region): |
---|
| 190 | if region not in self.regions: |
---|
| 191 | raise BeakerException('Cache region not configured: %s' % region) |
---|
| 192 | kw = self.regions[region] |
---|
| 193 | return self.caches.setdefault(name + str(kw), Cache(name, **kw)) |
---|
| 194 | |
---|
| 195 | def region(self, region, *args): |
---|
| 196 | """Decorate a function to cache itself using a cache region |
---|
| 197 | |
---|
| 198 | The region decorator requires arguments if there are more than |
---|
| 199 | 2 of the same named function, in the same module. This is |
---|
| 200 | because the namespace used for the functions cache is based on |
---|
| 201 | the functions name and the module. |
---|
| 202 | |
---|
| 203 | |
---|
| 204 | Example:: |
---|
| 205 | |
---|
| 206 | # Assuming a cache object is available like: |
---|
| 207 | cache = CacheManager(dict_of_config_options) |
---|
| 208 | |
---|
| 209 | |
---|
| 210 | def populate_things(): |
---|
| 211 | |
---|
| 212 | @cache.region('short_term', 'some_data') |
---|
| 213 | def load(search_term, limit, offset): |
---|
| 214 | return load_the_data(search_term, limit, offset) |
---|
| 215 | |
---|
| 216 | return load('rabbits', 20, 0) |
---|
| 217 | |
---|
| 218 | .. note:: |
---|
| 219 | |
---|
| 220 | The function being decorated must only be called with |
---|
| 221 | positional arguments. |
---|
| 222 | |
---|
| 223 | """ |
---|
| 224 | cache = [None] |
---|
| 225 | key = " ".join(str(x) for x in args) |
---|
| 226 | |
---|
| 227 | def decorate(func): |
---|
| 228 | def cached(*args): |
---|
| 229 | reg = self.regions[region] |
---|
| 230 | if not reg.get('enabled', True): |
---|
| 231 | return func(*args) |
---|
| 232 | |
---|
| 233 | if not cache[0]: |
---|
| 234 | namespace = util.func_namespace(func) |
---|
| 235 | cache[0] = self.get_cache_region(namespace, region) |
---|
| 236 | |
---|
| 237 | cache_key = key + " " + " ".join(str(x) for x in args) |
---|
| 238 | def go(): |
---|
| 239 | return func(*args) |
---|
| 240 | |
---|
| 241 | return cache[0].get_value(cache_key, createfunc=go) |
---|
| 242 | return cached |
---|
| 243 | return decorate |
---|
| 244 | |
---|
| 245 | def cache(self, *args, **kwargs): |
---|
| 246 | """Decorate a function to cache itself with supplied parameters |
---|
| 247 | |
---|
| 248 | ``args`` |
---|
| 249 | used to make the key unique for this function, as in region() |
---|
| 250 | above. |
---|
| 251 | |
---|
| 252 | ``kwargs`` |
---|
| 253 | parameters to be passed to get_cache(), will override defaults |
---|
| 254 | |
---|
| 255 | Example:: |
---|
| 256 | |
---|
| 257 | # Assuming a cache object is available like: |
---|
| 258 | cache = CacheManager(dict_of_config_options) |
---|
| 259 | |
---|
| 260 | |
---|
| 261 | def populate_things(): |
---|
| 262 | |
---|
| 263 | @cache.cache('mycache', expire=15) |
---|
| 264 | def load(search_term, limit, offset): |
---|
| 265 | return load_the_data(search_term, limit, offset) |
---|
| 266 | |
---|
| 267 | return load('rabbits', 20, 0) |
---|
| 268 | |
---|
| 269 | .. note:: |
---|
| 270 | |
---|
| 271 | The function being decorated must only be called with |
---|
| 272 | positional arguments. |
---|
| 273 | |
---|
| 274 | """ |
---|
| 275 | cache = [None] |
---|
| 276 | key = " ".join(str(x) for x in args) |
---|
| 277 | |
---|
| 278 | def decorate(func): |
---|
| 279 | def cached(*args): |
---|
| 280 | if not cache[0]: |
---|
| 281 | namespace = util.func_namespace(func) |
---|
| 282 | cache[0] = self.get_cache(namespace, **kwargs) |
---|
| 283 | cache_key = key + " " + " ".join(str(x) for x in args) |
---|
| 284 | def go(): |
---|
| 285 | return func(*args) |
---|
| 286 | |
---|
| 287 | return cache[0].get_value(cache_key, createfunc=go) |
---|
| 288 | return cached |
---|
| 289 | return decorate |
---|