How to use Flask-Cache with Flask-Restful

Question:

How do I use Flask-Cache @cache.cached() decorator with Flask-Restful? For example, I have a class Foo inherited from Resource, and Foo has get, post, put, and delete methods.

How can I can invalidate cached results after a POST?

@api.resource('/whatever')
class Foo(Resource):
    @cache.cached(timeout=10)
    def get(self):
        return expensive_db_operation()

    def post(self):
        update_db_here()

        ## How do I invalidate the value cached in get()?
        return something_useful()
Asked By: cyberra

||

Answers:

Yes, you can use like that.

Maybe you will still need to read: flask-cache memoize URL query string parameters as well

Answered By: codeskyblue

You can invalidate cache using cache.clear() method.
For more detials see: https://pythonhosted.org/Flask-Cache/#flask.ext.cache.Cache.clear and Clearing Cache section in https://pythonhosted.org/Flask-Cache/

Answered By: Nemanja Trifunovic

As Flask-Cache implementation doesn’t give you access to the underlying cache object, you’ll have to explicitly instantiate a Redis client and use it’s keys method (list all cache keys).

  • The cache_key method is used to override the default key generation in your cache.cached decorator.
  • The clear_cache method will clear only the portion of the cache corresponding to the current resource.

This is a solution that was tested only for Redis and the implementation will probably differ a little when using a different cache engine.

from app import cache # The Flask-Cache object
from config import CACHE_REDIS_HOST, CACHE_REDIS_PORT # The Flask-Cache config
from redis import Redis
from flask import request
import urllib

redis_client = Redis(CACHE_REDIS_HOST, CACHE_REDIS_PORT)

def cache_key():
   args = request.args
   key = request.path + '?' + urllib.urlencode([
     (k, v) for k in sorted(args) for v in sorted(args.getlist(k))
   ])
   return key

@api.resource('/whatever')
class Foo(Resource):

    @cache.cached(timeout=10, key_prefix=cache_key)
    def get(self):
        return expensive_db_operation()

    def post(self):
        update_db_here()
        self.clear_cache()
        return something_useful()

    def clear_cache(self):
        # Note: we have to use the Redis client to delete key by prefix,
        # so we can't use the 'cache' Flask extension for this one.
        key_prefix = request.path
        keys = [key for key in redis_client.keys() if key.startswith(key_prefix)]
        nkeys = len(keys)
        for key in keys:
            redis_client.delete(key)
        if nkeys > 0:
            log.info("Cleared %s cache keys" % nkeys)
            log.info(keys)
Answered By: JahMyst
##create a decarator
from werkzeug.contrib.cache import SimpleCache
CACHE_TIMEOUT = 300
cache = SimpleCache()
class cached(object):

 def __init__(self, timeout=None):
    self.timeout = timeout or CACHE_TIMEOUT

 def __call__(self, f):
    def decorator(*args, **kwargs):
        response = cache.get(request.path)
        if response is None:
            response = f(*args, **kwargs)
            cache.set(request.path, response, self.timeout)
        return response
    return decorator


#add this decarator to your views like below
@app.route('/buildingTotal',endpoint='buildingTotal') 
@cached()
def eventAlert():
  return 'something'

@app.route('/buildingTenants',endpoint='buildingTenants')
@cached()
def buildingTenants():
  return 'something'
Answered By: y durga prasad

Answer from @JahMyst didn’t work for me.
Flask-Cache doesn’t work with Flask restful framework. @cache.Cached & @cache.memoize can’t handle mutable objects per their documentation.

Using mutable objects (classes, etc) as part of the cache key can become tricky. It is suggested to not pass in an object instance into a memoized function. However, the memoize does perform a repr() on the passed in arguments so that if the object has a __repr__ function that returns a uniquely identifying string for that object, that will be used as part of the cache key.

Had to come-up with my own implementation. Leaving this code snippet incase someone else gets stuck with the same issue.

cache_key function converts the user req into hash.
cache_res_pickled function is being used to pickle or unpickle the data

|-flask-app
   |-app.py
   |-resource
      |--some_resource.py
import json
import logging
import pickle
import time
import urllib

from flask import Response, abort, request
from redis import Redis

redis_client = Redis("127.0.0.1", "6379")
exp_setting_s = 1500


def json_serial(obj):
    """
    JSON serializer for objects not serializable by default json code"
    Args:
            obj: JSON serialized object for dates

    Returns:
            serialized JSON data
    """
    if isinstance(obj, datetime.datetime):
        return obj.__str__()


def cache_key():
    """ ""
    Returns: Hashed string of request made by the user.

    """
    args = request.args
    key = (
        request.path
        + "?"
        + urllib.parse.urlencode(
            [(k, v) for k in sorted(args) for v in sorted(args.getlist(k))]
        )
    )
    key_hashed = hashlib.sha256(key.encode())
    return key_hashed.hexdigest()


def cache_res_pickled(data, encode):
    """
    Args:
        data (dict): Data in dict format
        encode (Boolean): Encode (true) or decode (false) the data

    Returns: Result after pickling
    """
    if encode:
        return pickle.dumps(data)
    else:
        data = pickle.loads(data)
        return data


class SomeResource(Resource):
    @auth.login_required
    def get(self):
        # Get the key for request in hashed format SHA256
        key = cache_key()
        result = redis_client.get(key)

        def generate():
            """
            A lagging generator to stream JSON so we don't have to hold everything in memory
            This is a little tricky, as we need to omit the last comma to make valid JSON,
            thus we use a lagging generator, similar to http://stackoverflow.com/questions/1630320/
            """
            releases = res.__iter__()
            try:
                prev_release = next(releases)  # get first result
                # We have some releases. First, yield the opening json
                yield '{"data": ['
                # Iterate over the releases
                for release in releases:
                    yield json.dumps(prev_release, default=json_serial) + ", "
                    prev_release = release
                logging.info(f"For {key} # records returned = {len(res)}")
                # Now yield the last iteration without comma but with the closing brackets
                yield json.dumps(prev_release, default=json_serial) + "]}"
            except StopIteration:
                # StopIteration here means the length was zero, so yield a valid releases doc and stop
                logging.info(f"For {key} # records returned = {len(res)}")
                yield '{"data": []}'

        if result is None:
            # Secure a key on Redis server.
            redis_client.set(key, cache_res_pickled({}, True), ex=exp_setting_s)

            try:
                # Do the querying to the DB or math here to get res. It should be in dict format as shown below
                res = {"A": 1, "B": 2, "C": 2}
                # Update the key on Redis server with the latest data
                redis_client.set(key, cache_res_pickled(res, True), ex=exp_setting_s)
                return Response(generate(), content_type="application/json")
            except Exception as e:
                logging.exception(e)
                abort(505, description="Resource not found. error - {}".format(e))
        else:
            res = cache_res_pickled(result, False)
            if res:
                logging.info(
                    f"The data already exists!  loading the data form Redis cache for Key - {key} "
                )
                return Response(generate(), content_type="application/json")
            else:
                logging.info(
                    f"There is already a request for this key. But there is no data in it. Key: {key}."
                )
                s = time.time()
                counter = 0
                # loops aimlessly till the data is available on the Redis
                while not any(res):
                    result = redis_client.get(key)
                    res = cache_res_pickled(result, False)
                    counter += 1
                logging.info(
                    f"The data was available after {time.time() - s} seconds. Had to loop {counter} times. ‍"
                )
                return Response(generate(), content_type="application/json")
Answered By: Rohith R Pai

Inspired from durga’s answer I wrote a very basic decorator which uses redis directly instead of any library.

from src.consts import config
from src.utils.external_services import redis_connector
import json
import jsons
import base64


class cached(object):

    def __init__(self, req, timeout=None):
        self.timeout = timeout or config.CACHE_DEFAULT_TIMEOUT
        self.request = req
        self.cache = redis_connector.get_redis_instance()

    def __call__(self, f):
        def decorator(*args, **kwargs):

            redis_healthy = True
            if self.cache is not None:
                try:
                    self.cache.ping()
                except Exception as ex:
                    redis_healthy = False
            else:
                redis_healthy = False

            if self.request is not None and self.request.values is not None and self.request.path is not None and redis_healthy:
                cache_key = "{}-{}".format(self.request.path, json.dumps(jsons.dump(self.request.values), sort_keys=True))
                cache_key_base_64 = base64.b64encode(cache_key.encode("ascii")).decode("ascii")

                response = self.cache.get(cache_key_base_64)
                if response is None:
                    response = f(*args, **kwargs)
                    self.cache.setex(cache_key_base_64, self.timeout, jsons.dumps(response))
                else:
                    response = json.loads(response)
            else:
                response = f(*args, **kwargs)

            return response

        return decorator

 

Now use this decorator on your api functions

from flask import g, request
from flask_restful import Resource
from webargs.flaskparser import use_args

class GetProducts(Resource):
    @use_args(gen_args.argsGetProducts)
    @cached(request)
    def get(self, args):
        return "hello from products"
Answered By: humble_wolf