"Base Cache class." import warnings from django.core.exceptions import ImproperlyConfigured, DjangoRuntimeWarning from django.utils.encoding import smart_str from django.utils.importlib import import_module class InvalidCacheBackendError(ImproperlyConfigured): pass class CacheKeyWarning(DjangoRuntimeWarning): pass # Memcached does not accept keys longer than this. MEMCACHE_MAX_KEY_LENGTH = 250 def default_key_func(key, key_prefix, version): """ Default function to generate keys. Constructs the key used by all other methods. By default it prepends the `key_prefix'. KEY_FUNCTION can be used to specify an alternate function with custom key making behavior. """ return ':'.join([key_prefix, str(version), smart_str(key)]) def get_key_func(key_func): """ Function to decide which key function to use. Defaults to ``default_key_func``. """ if key_func is not None: if callable(key_func): return key_func else: key_func_module_path, key_func_name = key_func.rsplit('.', 1) key_func_module = import_module(key_func_module_path) return getattr(key_func_module, key_func_name) return default_key_func class BaseCache(object): def __init__(self, params): timeout = params.get('timeout', params.get('TIMEOUT', 300)) try: timeout = int(timeout) except (ValueError, TypeError): timeout = 300 self.default_timeout = timeout options = params.get('OPTIONS', {}) max_entries = params.get('max_entries', options.get('MAX_ENTRIES', 300)) try: self._max_entries = int(max_entries) except (ValueError, TypeError): self._max_entries = 300 cull_frequency = params.get('cull_frequency', options.get('CULL_FREQUENCY', 3)) try: self._cull_frequency = int(cull_frequency) except (ValueError, TypeError): self._cull_frequency = 3 self.key_prefix = smart_str(params.get('KEY_PREFIX', '')) self.version = params.get('VERSION', 1) self.key_func = get_key_func(params.get('KEY_FUNCTION', None)) def make_key(self, key, version=None): """Constructs the key used by all other methods. By default it uses the key_func to generate a key (which, by default, prepends the `key_prefix' and 'version'). An different key function can be provided at the time of cache construction; alternatively, you can subclass the cache backend to provide custom key making behavior. """ if version is None: version = self.version new_key = self.key_func(key, self.key_prefix, version) return new_key def add(self, key, value, timeout=None, version=None): """ Set a value in the cache if the key does not already exist. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. Returns True if the value was stored, False otherwise. """ raise NotImplementedError def get(self, key, default=None, version=None): """ Fetch a given key from the cache. If the key does not exist, return default, which itself defaults to None. """ raise NotImplementedError def set(self, key, value, timeout=None, version=None): """ Set a value in the cache. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. """ raise NotImplementedError def delete(self, key, version=None): """ Delete a key from the cache, failing silently. """ raise NotImplementedError def get_many(self, keys, version=None): """ Fetch a bunch of keys from the cache. For certain backends (memcached, pgsql) this can be *much* faster when fetching multiple values. Returns a dict mapping each key in keys to its value. If the given key is missing, it will be missing from the response dict. """ d = {} for k in keys: val = self.get(k, version=version) if val is not None: d[k] = val return d def has_key(self, key, version=None): """ Returns True if the key is in the cache and has not expired. """ return self.get(key, version=version) is not None def incr(self, key, delta=1, version=None): """ Add delta to value in the cache. If the key does not exist, raise a ValueError exception. """ value = self.get(key, version=version) if value is None: raise ValueError("Key '%s' not found" % key) new_value = value + delta self.set(key, new_value, version=version) return new_value def decr(self, key, delta=1, version=None): """ Subtract delta from value in the cache. If the key does not exist, raise a ValueError exception. """ return self.incr(key, -delta, version=version) def __contains__(self, key): """ Returns True if the key is in the cache and has not expired. """ # This is a separate method, rather than just a copy of has_key(), # so that it always has the same functionality as has_key(), even # if a subclass overrides it. return self.has_key(key) def set_many(self, data, timeout=None, version=None): """ Set a bunch of values in the cache at once from a dict of key/value pairs. For certain backends (memcached), this is much more efficient than calling set() multiple times. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. """ for key, value in data.items(): self.set(key, value, timeout=timeout, version=version) def delete_many(self, keys, version=None): """ Set a bunch of values in the cache at once. For certain backends (memcached), this is much more efficient than calling delete() multiple times. """ for key in keys: self.delete(key, version=version) def clear(self): """Remove *all* values from the cache at once.""" raise NotImplementedError def validate_key(self, key): """ Warn about keys that would not be portable to the memcached backend. This encourages (but does not force) writing backend-portable cache code. """ if len(key) > MEMCACHE_MAX_KEY_LENGTH: warnings.warn('Cache key will cause errors if used with memcached: ' '%s (longer than %s)' % (key, MEMCACHE_MAX_KEY_LENGTH), CacheKeyWarning) for char in key: if ord(char) < 33 or ord(char) == 127: warnings.warn('Cache key contains characters that will cause ' 'errors if used with memcached: %r' % key, CacheKeyWarning) def incr_version(self, key, delta=1, version=None): """Adds delta to the cache version for the supplied key. Returns the new version. """ if version is None: version = self.version value = self.get(key, version=version) if value is None: raise ValueError("Key '%s' not found" % key) self.set(key, value, version=version+delta) self.delete(key, version=version) return version+delta def decr_version(self, key, delta=1, version=None): """Substracts delta from the cache version for the supplied key. Returns the new version. """ return self.incr_version(key, -delta, version)