Source code for zengine.lib.cache

# -*-  coding: utf-8 -*-
"""
Base Cache object and some builtin subclasses of it.
"""

# Copyright (C) 2015 ZetaOps Inc.
#
# This file is licensed under the GNU General Public License v3
# (GPLv3).  See LICENSE.txt for details.
import json

import time

from zengine.config import settings
from redis import Redis

redis_host, redis_port = settings.REDIS_SERVER.split(':')
cache = Redis(redis_host, redis_port)

REMOVE_SCRIPT = """
local keys = redis.call('keys', ARGV[1])
for i=1, #keys, 5000 do
    redis.call('del', unpack(keys, i, math.min(i+4999, #keys)))
end
return keys
"""

_remove_keys = cache.register_script(REMOVE_SCRIPT)


[docs]class Cache(object): """ Base cache object to implement specific cache object for each use case. Subclasses of this class can be consist of just a ```PREFIX``` attribute; .. code-block:: python class MyFooCache(Cache): PREFIX = 'FOO' # create cache object mycache = MyFooCache(*args) # set value mycache.set(value) # clear the whole PREFIX namespace MyFooCache.flush() # initial part(s) of keys can be used for finer control over keys. MyFooCache.flush('EXTRA_PREFIX') Or you can override the __init__ method to define strict positional args with docstrings. .. code-block:: python class MyFooCache(Cache): PREFIX = 'FOO' def __init__(self, model_name, obj_key): super(MyFooCache, self).__init__(model_name, obj_key) """ PREFIX = 'DFT' SERIALIZE = True def __init__(self, *args, **kwargs): self.serialize = kwargs.get('serialize', self.SERIALIZE) self.key = self._make_key(args) @classmethod def _make_key(cls, args): return "%s:%s" % (cls.PREFIX, ':'.join(args)) def __unicode__(self): return 'Cache object for %s' % self.key
[docs] def get(self, default=None): """ return the cached value or default if it can't be found :param default: default value :return: cached value """ d = cache.get(self.key) return ((json.loads(d.decode('utf-8')) if self.serialize else d) if d is not None else default)
[docs] def set(self, val, lifetime=None): """ set cache value :param val: any picklable object :param lifetime: exprition time in sec :return: val """ cache.set(self.key, (json.dumps(val) if self.serialize else val), lifetime or settings.DEFAULT_CACHE_EXPIRE_TIME) return val
[docs] def get_data_to_cache(self): return ""
[docs] def get_or_set(self, lifetime=None): return self.get() or self.set(self.get_data_to_cache(), lifetime)
[docs] def delete(self): """ Deletes the object. Returns: Cache backend response. """ return cache.delete(self.key)
[docs] def incr(self, delta=1): """ Increment the value of item. Args: delta: Incrementation amount. Returns: Cache backend response. """ return cache.incr(self.key, delta)
[docs] def decr(self, delta=1): """ Reduce the value of item. Args: delta: Reduction amount. Returns: Cache backend response. """ return cache.decr(self.key, delta)
[docs] def add(self, val): """ Add given value to item (list) Args: val: A JSON serializable object. Returns: Cache backend response. """ return cache.lpush(self.key, json.dumps(val) if self.serialize else val)
[docs] def get_all(self): """ Get all list items. Returns: Cache backend response. """ result = cache.lrange(self.key, 0, -1) return (json.loads(item.decode('utf-8')) for item in result if item) if self.serialize else result
[docs] def remove_all(self): """ Remove items of the list. Returns: Cache backend response. """ return cache.ltrim(self.key, -1, 0)
[docs] def remove_item(self, val): """ Removes given item from the list. Args: val: Item Returns: Cache backend response. """ return cache.lrem(self.key, json.dumps(val))
@classmethod
[docs] def flush(cls, *args): """ Removes all keys of this namespace Without args, clears all keys starting with cls.PREFIX if called with args, clears keys starting with given cls.PREFIX + args Args: *args: Arbitrary number of arguments. Returns: List of removed keys. """ return _remove_keys([], [(cls._make_key(args) if args else cls.PREFIX) + '*'])
[docs]class CatalogCache(Cache): """ Cache object for the CatalogData. Args: lang_code: Language code key: Item key """ PREFIX = 'CTDT' def __init__(self, lang_code, key): super(CatalogCache, self).__init__(lang_code, key)
[docs]class UserSessionID(Cache): """ Cache object for the User -> Active Session ID. Args: user_id: User key """ PREFIX = 'USID' SERIALIZE = False def __init__(self, user_id): if user_id: super(UserSessionID, self).__init__(user_id)
[docs]class KeepAlive(Cache): """ Websocket keepalive request timestamp store Args: user_id: User key sess_id: Session id """ PREFIX = 'KEEP' SERIALIZE = False SESSION_EXPIRE_TIME = 300 # sec def __init__(self, user_id=None, sess_id=None): self.user_id = user_id or Session(sess_id).get('user_id') self.sess_id = sess_id if self.user_id: super(KeepAlive, self).__init__(self.user_id)
[docs] def update_or_expire_session(self): """ Deletes session if keepalive request expired otherwise updates the keepalive timestamp value """ if not hasattr(self, 'key'): return now = time.time() timestamp = float(self.get() or 0) or now sess_id = self.sess_id or UserSessionID(self.user_id).get() if sess_id and now - timestamp > self.SESSION_EXPIRE_TIME: Session(sess_id).delete() return False else: self.set(now) return True
[docs] def reset(self): self.set(time.time())
[docs] def is_alive(self): if not hasattr(self, 'key'): return return time.time() - float(self.get(0.0)) < self.SESSION_EXPIRE_TIME
[docs]class ClearCache(Cache): """ Empty cache object to flush all cache entries """ PREFIX = ''
[docs]class Session(object): """ Redis based dict like session object to store user session data Examples: .. code-block:: python sess = Session(session_key) sess['user_data'] = {"foo":"bar"} sess Args: sessid: user session id. """ PREFIX = 'SES' def __init__(self, sessid=''): self.key = "" self.sess_id = sessid self.key = self._make_key(sessid) def _j_load(self, val): return json.loads(val.decode()) def __getitem__(self, key): val = self.get(key) if val: return val else: raise KeyError def __delitem__(self, key): key = self._make_key(key) cache.delete(key) def __setitem__(self, key, value): key = self._make_key(key) cache.set(key, json.dumps(value)) def __contains__(self, item): try: self.__getitem__(item) return True except KeyError: return False def _make_key(self, args=None): return "%s%s" % (self.key or self.PREFIX, ":%s" % args if args else "") def _keys(self): return cache.keys(self._make_key() + "*")
[docs] def get(self, key, default=None): key = self._make_key(key) val = cache.get(key) return self._j_load(val) if val else default
[docs] def keys(self): return [k[len(self.key) + 1:] for k in self._keys()]
[docs] def values(self): return (self._j_load(cache.get(k)) for k in self._keys())
[docs] def items(self): return ((k[len(self.key) + 1:], self._j_load(cache.get(k))) for k in self._keys())
[docs] def delete(self): """ Removes all contents attached to this session object. If sessid is empty, all sessions will be cleaned up. """ return _remove_keys([], [self.key + '*'])
[docs]class WFSpecNames(Cache): """ """ PREFIX = "WFSPECNAMES" def __init__(self): super(WFSpecNames, self).__init__('wf_spec_names')
[docs] def get_data_to_cache(self): return self.get_data()
@staticmethod
[docs] def get_data(): from zengine.models import BPMNWorkflow return BPMNWorkflow.objects.values_list('name', 'title', 'menu_category')
[docs] def refresh(self): self.delete() self.get_or_set()