简体   繁体   中英

How to use Flask-Cache with Flask-Restful

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()

Yes, you can use like that.

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

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)

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/

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")
##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'

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM