예제 #1
0
파일: cache.py 프로젝트: Hermy/foofind-web
 def __init__(self, *args, **kwargs):
     CacheBase.__init__(self, *args, **kwargs)
     self._local_cache = LimitedSizeDict(size_limit=10000)
     self._regexp_cache = LimitedSizeDict(size_limit=5000)
예제 #2
0
파일: cache.py 프로젝트: Hermy/foofind-web
class Cache(CacheBase):
    '''
    Objeto de caché heredado de Flask-cache con correcciones y mejoras.
    '''

    def __init__(self, *args, **kwargs):
        CacheBase.__init__(self, *args, **kwargs)
        self._local_cache = LimitedSizeDict(size_limit=10000)
        self._regexp_cache = LimitedSizeDict(size_limit=5000)


    def regexp(self, r):
        if not r in self._regexp_cache:
            self._regexp_cache[r] = re.compile(r)
        return self._regexp_cache[r]

    def local_get(self, key):
        '''
        Obtiene una clave del caché local para esta instancia de la aplicación.

        @type key: str
        @param key: clave de caché
        '''
        value, expiration = self._local_cache.get(key, (None, None))
        if expiration is None:
            if value is None and key is self._local_cache:
                del self._local_cache[key]
            return value
        elif expiration > time.time():
            return value
        del self._local_cache[key]
        return None

    def append(self, key, value, create=False, separator="", timeout=0):
        '''
        Concatena un valor dado al valor de una entrada existente. Puede crear la entrada si no existe.

        @type key: str
        @param key: clave del cache

        '''
        must_append = True
        if create and self.cache._client.add(key, value, timeout):
            must_append = False

        if must_append:
            return self.cache._client.append(key, separator+value, timeout)
        else:
            return True

    def local_set(self, key, value, timeout=None):
        '''
        Asigna una clave del caché local para esta instancia de la aplicación.

        @type key: str
        @param key:
        '''
        self._local_cache[key] = (value, (time.time()+timeout) if timeout > 0 else None)

    def local_delete(self, key):
        '''
        Borra una clave del caché local para esta instancia de la aplicación.

        @type key: str
        @param key: clave de caché
        '''
        if key in self._local_cache:
            del self._local_cache[key]

    def local_delete_multi(self, *keys):
        '''
        Borra varias claves de caché local para esta instancia de la aplicación.

        @type key: str
        @param key: clave de caché
        '''
        for key in keys:
            if key in self._local_cache:
                del self._local_cache[key]

    '''
    @type cacheme: bool
    @ivar cacheme: si se establece a False, los decoradores no guardarán caché
                   en el contexto actual (se requiere contexto de Flask).
    '''
    @property
    def cacheme(self):
        if hasattr(g, "cache_cacheme"):
            return bool(g.cache_cacheme)
        return True

    @cacheme.setter
    def cacheme(self, v):
        try:
            g.cache_cacheme = bool(v)
        except RuntimeError as e:
            # Si no hay contexto no hace falta controlar el caché
            logging.debug("cache.cacheme accessed without context", extra={"e":e})

    @property
    def skip(self):
        if hasattr(g, "cache_skip"):
            return bool(g.cache_skip)
        return False

    @skip.setter
    def skip(self, v):
        g.cache_skip = v

    @classmethod
    def throw_fallback(cls):
        '''
        Función de conveniencia que lanza una excepción que hace saltar el
        fallback si registrar excepción.
        '''
        frame = currentframe()
        if not frame is None:
            # Comprobación: la pila no es accesible en todas las implementaciones de python
            while frame:
                if frame.f_locals.get("__", None) is ThrowFallback._enabled:
                    break
                frame = frame.f_back
            else:
                raise RuntimeError, "Cache.throw_fallback called outside any fallback scope"
        raise ThrowFallback

    def clear(self):
        '''Borrar todas las claves de caché.'''
        self.cache.clear()

    def cached(self, timeout=None, key_prefix='view%s', unless=None):
        '''
        Decorador para cachear vistas

        @type timeout: int o None
        @param timeout: segundos que debe de mantenerse el caché, si es None se
                        cacheará todo el tiempo que se pueda.

        @type key_prefix: str
        @param key_prefix: prefijo (formato) para la clave de memcache,
                           Se admite formato (con clave) de strings, el objeto
                           request será usado como objeto mapeado.
                           Si contiene %s, será reemplazado por request.path
                           (por retrocompatibilidad).
        @type unless: None o callable
        @param unless: Si es callable, y el resultado de la llamada es True,
                       no se cacheará.
        '''
        def decorator(f):
            uncached_fnc = self._get_uncached(f)

            @wraps(f)
            def decorated_function(*args, **kwargs):

                if callable(unless) and unless() is True:
                    return f(*args, **kwargs)

                cache_key = decorated_function.make_cache_key(*args, **kwargs)

                rv = self.get(cache_key) if not self.skip else None
                if rv is None:
                    rv = f(*args, **kwargs)
                    '''# Optimización: Si la respuesta es html, minificamos
                    if hasattr(rv, "mimetype") and response.mimetype == "text/html":
                        rv.data = html_slimmer(rv.data)
                    elif isinstance(rv, basestring) and rv.startswith("<!DOCTYPE html>"):
                        rv = html_slimmer(rv)
                    '''
                    if self.cacheme:
                        self.set(cache_key, rv, timeout=decorated_function.cache_timeout)
                return rv

            def make_cache_key(*args, **kwargs):
                if self._self_given(args, uncached_fnc):
                    args = args[1:]
                if callable(key_prefix):
                    cache_key = key_prefix()
                elif "%s" in key_prefix:
                    cache_key = key_prefix % request.path
                else:
                    cache_key = key_prefix % request.__dict__
                return cache_key.encode('utf-8')

            decorated_function.uncached = f
            decorated_function.cache_timeout = timeout
            decorated_function.make_cache_key = make_cache_key

            return decorated_function
        return decorator

    def cached_GET(self, timeout=None, params=None, unless=None):
        '''
        Cachea vista teniendo en cuenta los argumentos GET

        @type timeout: int
        @param timeout: segundos de vida de la clave

        @type params: iterable
        @param params: iterable con los parámetros GET que se tendrán en cuenta,
                      si es None, se tendrán todos en cuenta, y si es un
                      diccionario, los valores que sean "callable" serán ejecutados
                      con el valor del parámetro correspondiente, y su retorno
                      será el valor que se usará como clave de cache.

        @type unless: callable
        @param unless: será ejecutado sin parámetros y si su retorno es True
                      no se cacheará.
        '''
        if hasattr(params, "__contains__"):
            if hasattr(params, "__getitem__"):
                key_prefix = lambda: "view%s/%s" % (
                    request.path,
                    md5(repr({i:(params[i](request.args[i]) if callable(params[i]) else request.args[i])
                        for i in request.args if i in params})).hexdigest()
                    )
            else:
                key_prefix = lambda: "view%s/%s" % (
                    request.path,
                    md5(repr({i:request.args[i] for i in request.args if i in params})).hexdigest()
                    )
        else:
            key_prefix = lambda: "view%s/%s" % (
                request.path,
                md5(repr(dict(request.args))).hexdigest()
                )
        return self.cached(
            timeout,
            key_prefix=key_prefix,
            unless=unless
            )

    def _fnc_name(self, f):
        '''
        Genera un nombre único para la función dada
        '''
        try:
            lineno = getsourcelines(f)[1]
        except IOError:
            lineno = "unknown"
        return "%s:%s:%s" % (
            f.__module__ if hasattr(f, "__module__") else "__main__",
            lineno, f.__name__)

    def _self_given(self, args, uncached_fnc):
        '''
        Detecta si el primer elemento de args es "self" para uncached_fnc
        '''
        if args:
            namesake = getattr(args[0], uncached_fnc.__name__, None)
            return namesake and self._get_uncached(namesake) == uncached_fnc
        return False

    def _get_uncached(self, f):
        '''
        Retorna, recursivamente, la función original no decorada.
        '''
        if hasattr(f, "uncached"):
            return self._get_uncached(f.uncached)
        return f

    def local_memoize(self, timeout=None):
        '''
        Decorador para cachear en el hilo local. Se tendrán en cuenta los parámetros.

        @type timeout: int o None
        @param timeout: segundos que debe de mantenerse el caché, si es None se
                        cacheará todo el tiempo que se pueda.

        '''
        def memoize(f):
            funcname = self._fnc_name(f)
            uncached_fnc = self._get_uncached(f)

            @wraps(f)
            def decorated_function(*args, **kwargs):
                cache_key = decorated_function.make_cache_key(*args, **kwargs)
                rv = self.local_get(cache_key) if not self.skip else None
                if rv is None:
                    rv = f(*args, **kwargs)
                    if self.cacheme:
                        self.local_set(cache_key, rv,
                                       timeout=decorated_function.cache_timeout)
                return rv

            def make_cache_key(*args, **kwargs):
                if self._self_given(args, uncached_fnc):
                    args = args[1:]
                return "memoized/%s/%s" % (
                    funcname,
                    md5("%s:%s" % (repr(args), repr(kwargs))).hexdigest()
                    )

            def flush(*args, **kwargs):
                cache_key = decorated_function.make_cache_key(*args, **kwargs)
                self.local_delete(cache_key)

            decorated_function.flush = flush
            decorated_function.uncached = f
            decorated_function.cache_timeout = timeout
            decorated_function.make_cache_key = make_cache_key

            return decorated_function
        return memoize

    def memoize(self, timeout=None):
        '''
        Decorador para cachear funciones. Se tendrán en cuenta los parámetros.

        @type timeout: int o None
        @param timeout: segundos que debe de mantenerse el caché, si es None se
                        cacheará todo el tiempo que se pueda.

        '''
        def memoize(f):
            funcname = self._fnc_name(f)
            uncached_fnc = self._get_uncached(f)

            @wraps(f)
            def decorated_function(*args, **kwargs):
                cache_key = decorated_function.make_cache_key(*args, **kwargs)
                rv = self.get(cache_key) if not self.skip else None
                if rv is None:
                    rv = f(*args, **kwargs)
                    if self.cacheme:
                        self.set(cache_key, rv,
                                       timeout=decorated_function.cache_timeout)
                        self._memoized.append((funcname, cache_key))
                return rv

            def make_cache_key(*args, **kwargs):
                if self._self_given(args, uncached_fnc):
                    args = args[1:]
                return "memoized/%s/%s" % (
                    funcname,
                    md5("%s:%s" % (repr(args), repr(kwargs))).hexdigest()
                    )

            def flush(*args, **kwargs):
                cache_key = decorated_function.make_cache_key(*args, **kwargs)
                self.delete(cache_key)

            decorated_function.flush = flush
            decorated_function.uncached = f
            decorated_function.cache_timeout = timeout
            decorated_function.make_cache_key = make_cache_key

            return decorated_function
        return memoize

    def fallback(self, errors=(), timeout=0):
        '''
        Decorador para retornar caché en caso de error.

        @type errors: iterable o type
        @param errors: lista de tipos de errores que serán capturados para
                       retornar una respuesta cacheada, si está vacío, se
                       capturarán todos los errores.

        @type timeout: int o None
        @param timeout: segundos que debe de mantenerse el caché, si es cero
                        (por defecto) se cacheará todo el tiempo que se pueda,
                        si es None se cacheará según el default_timeout.

        '''
        def memoize(f):
            funcname = self._fnc_name(f)
            uncached_fnc = self._get_uncached(f)

            @wraps(f)
            def decorated_function(*args, **kwargs):
                __ = ThrowFallback._enabled
                cache_key = decorated_function.make_cache_key(*args, **kwargs)
                try:
                    rv = f(*args, **kwargs)
                    self.cache.set(cache_key, rv, timeout=decorated_function.cache_timeout)
                    return rv
                except ThrowFallback:
                    # Se ha ordenado usar el fallback
                    self.cacheme = False
                except BaseException as e:
                    if errors and not isinstance(e, errors):
                        # No se ha especificado capturar el error, propagamos
                        raise
                    self.cacheme = False
                    logging.exception(e)
                return self.cache.get(cache_key)

            def make_cache_key(*args, **kwargs):
                if self._self_given(args, uncached_fnc):
                    args = args[1:]
                return "fallback/%s/%s" % (
                    funcname,
                    md5("%s:%s" % (repr(args), repr(kwargs))).hexdigest()
                    )

            def flush(*args, **kwargs):
                cache_key = decorated_function.make_cache_key(*args, **kwargs)
                self.delete(cache_key)

            decorated_function.flush = flush
            decorated_function.uncached = f
            decorated_function.cache_timeout = timeout
            decorated_function.make_cache_key = make_cache_key

            return decorated_function
        return memoize

    def inc(self, key, delta=1):
        if isinstance(key, unicode):
            key = key.encode('utf-8')
        if self.cache.key_prefix:
            key = self.cache.key_prefix + key
        return self.cache._client.incr(key, delta)