| | """Django-compatible disk and file backed cache.""" |
| |
|
| | from functools import wraps |
| |
|
| | from django.core.cache.backends.base import BaseCache |
| |
|
| | try: |
| | from django.core.cache.backends.base import DEFAULT_TIMEOUT |
| | except ImportError: |
| | |
| | DEFAULT_TIMEOUT = 300 |
| |
|
| | from .core import ENOVAL, args_to_key, full_name |
| | from .fanout import FanoutCache |
| |
|
| |
|
| | class DjangoCache(BaseCache): |
| | """Django-compatible disk and file backed cache.""" |
| |
|
| | def __init__(self, directory, params): |
| | """Initialize DjangoCache instance. |
| | |
| | :param str directory: cache directory |
| | :param dict params: cache parameters |
| | |
| | """ |
| | super().__init__(params) |
| | shards = params.get('SHARDS', 8) |
| | timeout = params.get('DATABASE_TIMEOUT', 0.010) |
| | options = params.get('OPTIONS', {}) |
| | self._cache = FanoutCache(directory, shards, timeout, **options) |
| |
|
| | @property |
| | def directory(self): |
| | """Cache directory.""" |
| | return self._cache.directory |
| |
|
| | def cache(self, name): |
| | """Return Cache with given `name` in subdirectory. |
| | |
| | :param str name: subdirectory name for Cache |
| | :return: Cache with given name |
| | |
| | """ |
| | return self._cache.cache(name) |
| |
|
| | def deque(self, name, maxlen=None): |
| | """Return Deque with given `name` in subdirectory. |
| | |
| | :param str name: subdirectory name for Deque |
| | :param maxlen: max length (default None, no max) |
| | :return: Deque with given name |
| | |
| | """ |
| | return self._cache.deque(name, maxlen=maxlen) |
| |
|
| | def index(self, name): |
| | """Return Index with given `name` in subdirectory. |
| | |
| | :param str name: subdirectory name for Index |
| | :return: Index with given name |
| | |
| | """ |
| | return self._cache.index(name) |
| |
|
| | def add( |
| | self, |
| | key, |
| | value, |
| | timeout=DEFAULT_TIMEOUT, |
| | version=None, |
| | read=False, |
| | tag=None, |
| | retry=True, |
| | ): |
| | """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. |
| | |
| | Return True if the value was stored, False otherwise. |
| | |
| | :param key: key for item |
| | :param value: value for item |
| | :param float timeout: seconds until the item expires |
| | (default 300 seconds) |
| | :param int version: key version number (default None, cache parameter) |
| | :param bool read: read value as bytes from file (default False) |
| | :param str tag: text to associate with key (default None) |
| | :param bool retry: retry if database timeout occurs (default True) |
| | :return: True if item was added |
| | |
| | """ |
| | |
| | key = self.make_key(key, version=version) |
| | timeout = self.get_backend_timeout(timeout=timeout) |
| | return self._cache.add(key, value, timeout, read, tag, retry) |
| |
|
| | def get( |
| | self, |
| | key, |
| | default=None, |
| | version=None, |
| | read=False, |
| | expire_time=False, |
| | tag=False, |
| | retry=False, |
| | ): |
| | """Fetch a given key from the cache. If the key does not exist, return |
| | default, which itself defaults to None. |
| | |
| | :param key: key for item |
| | :param default: return value if key is missing (default None) |
| | :param int version: key version number (default None, cache parameter) |
| | :param bool read: if True, return file handle to value |
| | (default False) |
| | :param float expire_time: if True, return expire_time in tuple |
| | (default False) |
| | :param tag: if True, return tag in tuple (default False) |
| | :param bool retry: retry if database timeout occurs (default False) |
| | :return: value for item if key is found else default |
| | |
| | """ |
| | |
| | key = self.make_key(key, version=version) |
| | return self._cache.get(key, default, read, expire_time, tag, retry) |
| |
|
| | def read(self, key, version=None): |
| | """Return file handle corresponding to `key` from Cache. |
| | |
| | :param key: Python key to retrieve |
| | :param int version: key version number (default None, cache parameter) |
| | :return: file open for reading in binary mode |
| | :raises KeyError: if key is not found |
| | |
| | """ |
| | key = self.make_key(key, version=version) |
| | return self._cache.read(key) |
| |
|
| | def set( |
| | self, |
| | key, |
| | value, |
| | timeout=DEFAULT_TIMEOUT, |
| | version=None, |
| | read=False, |
| | tag=None, |
| | retry=True, |
| | ): |
| | """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. |
| | |
| | :param key: key for item |
| | :param value: value for item |
| | :param float timeout: seconds until the item expires |
| | (default 300 seconds) |
| | :param int version: key version number (default None, cache parameter) |
| | :param bool read: read value as bytes from file (default False) |
| | :param str tag: text to associate with key (default None) |
| | :param bool retry: retry if database timeout occurs (default True) |
| | :return: True if item was set |
| | |
| | """ |
| | |
| | key = self.make_key(key, version=version) |
| | timeout = self.get_backend_timeout(timeout=timeout) |
| | return self._cache.set(key, value, timeout, read, tag, retry) |
| |
|
| | def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None, retry=True): |
| | """Touch a key in the cache. If timeout is given, that timeout will be |
| | used for the key; otherwise the default cache timeout will be used. |
| | |
| | :param key: key for item |
| | :param float timeout: seconds until the item expires |
| | (default 300 seconds) |
| | :param int version: key version number (default None, cache parameter) |
| | :param bool retry: retry if database timeout occurs (default True) |
| | :return: True if key was touched |
| | |
| | """ |
| | |
| | key = self.make_key(key, version=version) |
| | timeout = self.get_backend_timeout(timeout=timeout) |
| | return self._cache.touch(key, timeout, retry) |
| |
|
| | def pop( |
| | self, |
| | key, |
| | default=None, |
| | version=None, |
| | expire_time=False, |
| | tag=False, |
| | retry=True, |
| | ): |
| | """Remove corresponding item for `key` from cache and return value. |
| | |
| | If `key` is missing, return `default`. |
| | |
| | Operation is atomic. Concurrent operations will be serialized. |
| | |
| | :param key: key for item |
| | :param default: return value if key is missing (default None) |
| | :param int version: key version number (default None, cache parameter) |
| | :param float expire_time: if True, return expire_time in tuple |
| | (default False) |
| | :param tag: if True, return tag in tuple (default False) |
| | :param bool retry: retry if database timeout occurs (default True) |
| | :return: value for item if key is found else default |
| | |
| | """ |
| | key = self.make_key(key, version=version) |
| | return self._cache.pop(key, default, expire_time, tag, retry) |
| |
|
| | def delete(self, key, version=None, retry=True): |
| | """Delete a key from the cache, failing silently. |
| | |
| | :param key: key for item |
| | :param int version: key version number (default None, cache parameter) |
| | :param bool retry: retry if database timeout occurs (default True) |
| | :return: True if item was deleted |
| | |
| | """ |
| | |
| | key = self.make_key(key, version=version) |
| | return self._cache.delete(key, retry) |
| |
|
| | def incr(self, key, delta=1, version=None, default=None, retry=True): |
| | """Increment value by delta for item with key. |
| | |
| | If key is missing and default is None then raise KeyError. Else if key |
| | is missing and default is not None then use default for value. |
| | |
| | Operation is atomic. All concurrent increment operations will be |
| | counted individually. |
| | |
| | Assumes value may be stored in a SQLite column. Most builds that target |
| | machines with 64-bit pointer widths will support 64-bit signed |
| | integers. |
| | |
| | :param key: key for item |
| | :param int delta: amount to increment (default 1) |
| | :param int version: key version number (default None, cache parameter) |
| | :param int default: value if key is missing (default None) |
| | :param bool retry: retry if database timeout occurs (default True) |
| | :return: new value for item on success else None |
| | :raises ValueError: if key is not found and default is None |
| | |
| | """ |
| | |
| | key = self.make_key(key, version=version) |
| | try: |
| | return self._cache.incr(key, delta, default, retry) |
| | except KeyError: |
| | raise ValueError("Key '%s' not found" % key) from None |
| |
|
| | def decr(self, key, delta=1, version=None, default=None, retry=True): |
| | """Decrement value by delta for item with key. |
| | |
| | If key is missing and default is None then raise KeyError. Else if key |
| | is missing and default is not None then use default for value. |
| | |
| | Operation is atomic. All concurrent decrement operations will be |
| | counted individually. |
| | |
| | Unlike Memcached, negative values are supported. Value may be |
| | decremented below zero. |
| | |
| | Assumes value may be stored in a SQLite column. Most builds that target |
| | machines with 64-bit pointer widths will support 64-bit signed |
| | integers. |
| | |
| | :param key: key for item |
| | :param int delta: amount to decrement (default 1) |
| | :param int version: key version number (default None, cache parameter) |
| | :param int default: value if key is missing (default None) |
| | :param bool retry: retry if database timeout occurs (default True) |
| | :return: new value for item on success else None |
| | :raises ValueError: if key is not found and default is None |
| | |
| | """ |
| | |
| | return self.incr(key, -delta, version, default, retry) |
| |
|
| | def has_key(self, key, version=None): |
| | """Returns True if the key is in the cache and has not expired. |
| | |
| | :param key: key for item |
| | :param int version: key version number (default None, cache parameter) |
| | :return: True if key is found |
| | |
| | """ |
| | key = self.make_key(key, version=version) |
| | return key in self._cache |
| |
|
| | def expire(self): |
| | """Remove expired items from cache. |
| | |
| | :return: count of items removed |
| | |
| | """ |
| | return self._cache.expire() |
| |
|
| | def stats(self, enable=True, reset=False): |
| | """Return cache statistics hits and misses. |
| | |
| | :param bool enable: enable collecting statistics (default True) |
| | :param bool reset: reset hits and misses to 0 (default False) |
| | :return: (hits, misses) |
| | |
| | """ |
| | return self._cache.stats(enable=enable, reset=reset) |
| |
|
| | def create_tag_index(self): |
| | """Create tag index on cache database. |
| | |
| | Better to initialize cache with `tag_index=True` than use this. |
| | |
| | :raises Timeout: if database timeout occurs |
| | |
| | """ |
| | self._cache.create_tag_index() |
| |
|
| | def drop_tag_index(self): |
| | """Drop tag index on cache database. |
| | |
| | :raises Timeout: if database timeout occurs |
| | |
| | """ |
| | self._cache.drop_tag_index() |
| |
|
| | def evict(self, tag): |
| | """Remove items with matching `tag` from cache. |
| | |
| | :param str tag: tag identifying items |
| | :return: count of items removed |
| | |
| | """ |
| | return self._cache.evict(tag) |
| |
|
| | def cull(self): |
| | """Cull items from cache until volume is less than size limit. |
| | |
| | :return: count of items removed |
| | |
| | """ |
| | return self._cache.cull() |
| |
|
| | def clear(self): |
| | """Remove *all* values from the cache at once.""" |
| | return self._cache.clear() |
| |
|
| | def close(self, **kwargs): |
| | """Close the cache connection.""" |
| | |
| | self._cache.close() |
| |
|
| | def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT): |
| | """Return seconds to expiration. |
| | |
| | :param float timeout: seconds until the item expires |
| | (default 300 seconds) |
| | |
| | """ |
| | if timeout == DEFAULT_TIMEOUT: |
| | timeout = self.default_timeout |
| | elif timeout == 0: |
| | |
| | timeout = -1 |
| | return None if timeout is None else timeout |
| |
|
| | def memoize( |
| | self, |
| | name=None, |
| | timeout=DEFAULT_TIMEOUT, |
| | version=None, |
| | typed=False, |
| | tag=None, |
| | ignore=(), |
| | ): |
| | """Memoizing cache decorator. |
| | |
| | Decorator to wrap callable with memoizing function using cache. |
| | Repeated calls with the same arguments will lookup result in cache and |
| | avoid function evaluation. |
| | |
| | If name is set to None (default), the callable name will be determined |
| | automatically. |
| | |
| | When timeout is set to zero, function results will not be set in the |
| | cache. Cache lookups still occur, however. Read |
| | :doc:`case-study-landing-page-caching` for example usage. |
| | |
| | If typed is set to True, function arguments of different types will be |
| | cached separately. For example, f(3) and f(3.0) will be treated as |
| | distinct calls with distinct results. |
| | |
| | The original underlying function is accessible through the __wrapped__ |
| | attribute. This is useful for introspection, for bypassing the cache, |
| | or for rewrapping the function with a different cache. |
| | |
| | An additional `__cache_key__` attribute can be used to generate the |
| | cache key used for the given arguments. |
| | |
| | Remember to call memoize when decorating a callable. If you forget, |
| | then a TypeError will occur. |
| | |
| | :param str name: name given for callable (default None, automatic) |
| | :param float timeout: seconds until the item expires |
| | (default 300 seconds) |
| | :param int version: key version number (default None, cache parameter) |
| | :param bool typed: cache different types separately (default False) |
| | :param str tag: text to associate with arguments (default None) |
| | :param set ignore: positional or keyword args to ignore (default ()) |
| | :return: callable decorator |
| | |
| | """ |
| | |
| | if callable(name): |
| | raise TypeError('name cannot be callable') |
| |
|
| | def decorator(func): |
| | """Decorator created by memoize() for callable `func`.""" |
| | base = (full_name(func),) if name is None else (name,) |
| |
|
| | @wraps(func) |
| | def wrapper(*args, **kwargs): |
| | """Wrapper for callable to cache arguments and return values.""" |
| | key = wrapper.__cache_key__(*args, **kwargs) |
| | result = self.get(key, ENOVAL, version, retry=True) |
| |
|
| | if result is ENOVAL: |
| | result = func(*args, **kwargs) |
| | valid_timeout = ( |
| | timeout is None |
| | or timeout == DEFAULT_TIMEOUT |
| | or timeout > 0 |
| | ) |
| | if valid_timeout: |
| | self.set( |
| | key, |
| | result, |
| | timeout, |
| | version, |
| | tag=tag, |
| | retry=True, |
| | ) |
| |
|
| | return result |
| |
|
| | def __cache_key__(*args, **kwargs): |
| | """Make key for cache given function arguments.""" |
| | return args_to_key(base, args, kwargs, typed, ignore) |
| |
|
| | wrapper.__cache_key__ = __cache_key__ |
| | return wrapper |
| |
|
| | return decorator |
| |
|