def django_cache_function(timeout: int = 5 * 60,
                          cache_key: str = '',
                          debug_cache: bool = False):
    """
    Decorator to add caching to a function in Django.
    Uses the Django default cache.

    Args:

        timeout: timeout in seconds; use None for "never expire", as 0 means
            "do not cache".

        cache_key: optional cache key to use (if falsy, we'll invent one)
        debug_cache: show hits/misses?
    """
    cache_key = cache_key or None

    def decorator(fn):
        def wrapper(*args, **kwargs):
            # - NOTE that Django returns None from cache.get() for "not in
            #   cache", so can't cache a None value;
            #   https://docs.djangoproject.com/en/1.10/topics/cache/#basic-usage  # noqa
            # - We need to store a bit more than just the function result
            #   anyway, to detect hash collisions when the user doesn't specify
            #   the cache_key, so we may as well use that format even if the
            #   user does specify the cache_key, and then we can store a None
            #   result properly as well.
            if cache_key:
                # User specified a cache key. This is easy.
                call_sig = ''
                _cache_key = cache_key
                check_stored_call_sig = False
            else:
                # User didn't specify a cache key, so we'll do one
                # automatically. Since we do this via a hash, there is a small
                # but non-zero chance of a hash collision.
                call_sig = get_call_signature(fn, args, kwargs)
                _cache_key = make_cache_key(call_sig)
                check_stored_call_sig = True
            if debug_cache:
                log.critical("Checking cache for key: " + _cache_key)
            cache_result_tuple = cache.get(_cache_key)  # TALKS TO CACHE HERE
            if cache_result_tuple is None:
                if debug_cache:
                    log.debug("Cache miss")
            else:
                if debug_cache:
                    log.debug("Cache hit")
                cached_call_sig, func_result = cache_result_tuple
                if (not check_stored_call_sig) or cached_call_sig == call_sig:
                    return func_result
                log.warning(
                    "... Cache hit was due to hash collision; cached_call_sig "
                    "{} != call_sig {}".format(
                        repr(cached_call_sig), repr(call_sig)))
                # If we get here, either it wasn't in the cache, or something
                # was in the cache that matched by cache_key but was actually a
                # hash collision. Either way, we must do the real work.
            func_result = fn(*args, **kwargs)
            cache_result_tuple = (call_sig, func_result)
            cache.set(key=_cache_key, value=cache_result_tuple,
                      timeout=timeout)  # TALKS TO CACHE HERE
            return func_result

        return wrapper

    return decorator
Decorator to add caching to a function in Django.
    Uses the Django default cache.

    Args:

        timeout: timeout in seconds; use None for "never expire", as 0 means
            "do not cache".

        cache_key: optional cache key to use (if falsy, we'll invent one)
        debug_cache: show hits/misses?