tidalf/plugin.audio.qobuz

View on GitHub
resources/lib/qobuz/cache/base_cache.py

Summary

Maintainability
B
5 hrs
Test Coverage
'''
    qobuz.cache.base
    ~~~~~~~~~~~~~~~~

    A class to handle caching

    ::cached decorator that will cache a function call based on his
    positional and named parameter

    :part_of: kodi-qobuz
    :copyright: (c) 2012-2018 by Joachim Basmaison, Cyril Leclerc
    :license: GPLv3, see LICENSE for more details.
'''
from time import time

__seed__ = __name__ + '0.0.1'
__magic__ = 0
pos = 0
for i in [ord(c) for c in __seed__[:]]:
    __magic__ += i * 2**pos
    pos += 1
BadMagic = 1 << 1
BadKey = 1 << 2
NoData = 1 << 3
StoreError = 1 << 4
DeleteError = 1 << 5


class BaseCache(object):
    """A base class for caching
    """

    def __init__(self, *a, **ka):
        self.cached_function_name = __name__
        if 'black_keys' not in self.__dict__:
            self.black_keys = []

    def cached(self, f, *a, **ka):
        """Decorator
            All positional and named parameters are used to make the key
        """
        that = self
        self.cached_function_name = f.__name__

        def wrapped_function(self, *a, **ka):
            noRemote = False
            if 'noRemote' in ka:
                noRemote = bool(ka['noRemote'])
                del ka['noRemote']
            that.error = 0
            key = that.make_key(*a, **ka)
            data = that.load(key, *a, **ka)
            if data:
                if not that.check_magic(data, *a, **ka):
                    that.error &= BadMagic
                elif not that.check_key(data, key, *a, **ka):
                    that.error &= BadKey
                elif that.is_fresh(key, data, *a, **ka):
                    return data['data']
                if not that.delete(key):
                    that.error = DeleteError
            if noRemote:
                return None
            data = f(self, *a, **ka)
            if data is None or not data:
                that.error &= NoData
                return None
            for black_key in that.black_keys:
                if black_key in ka:
                    del ka[black_key]
            entry = {
                'updated_on': time(),
                'data': data,
                'ttl': that.get_ttl(key, *a, **ka),
                'pa': a,
                'ka': ka,
                'magic': __magic__,
                'key': key
            }
            if not that.sync(key, entry):
                that.error &= StoreError
                return None
            return data

        return wrapped_function

    @classmethod
    def is_fresh(cls, key, data, *a, **ka):
        if 'updated_on' not in data:
            return False
        updated_on = data['updated_on']
        ttl = data['ttl']
        if ttl == 0:
            return -1
        diff = (updated_on + ttl) - time()
        if diff <= 0:
            return 0
        return diff

    @classmethod
    def check_magic(cls, data, *a, **ka):
        if 'magic' not in data:
            return False
        if data['magic'] != __magic__:
            return False
        return True

    @classmethod
    def check_key(cls, data, key, *a, **ka):
        if 'key' not in data:
            return False
        if data['key'] != key:
            return False
        return True

    def load(self, key, *a, **ka):
        """Return tuple (Status, Data)
            Status: Bool
            Data: Arbitrary data
        """
        raise NotImplementedError()

    def load_from_store(self, path):
        raise NotImplementedError()

    def sync(self, key, data, *a, **ka):
        raise NotImplementedError()

    def delete(self, key, *a, **ka):
        raise NotImplementedError()

    def make_key(self, *a, **ka):
        raise NotImplementedError()

    @classmethod
    def get_ttl(cls, *a, **ka):
        raise NotImplementedError()