peterbe / django-cache-memoize
1 4
from functools import wraps
2

3 4
import hashlib
4 4
from urllib.parse import quote
5

6 4
from django.core.cache import caches, DEFAULT_CACHE_ALIAS
7

8 4
from django.utils.encoding import force_bytes
9

10 4
MARKER = object()
11

12

13 4
def cache_memoize(
14
    timeout,
15
    prefix=None,
16
    args_rewrite=None,
17
    hit_callable=None,
18
    miss_callable=None,
19
    key_generator_callable=None,
20
    store_result=True,
21
    cache_exceptions=(),
22
    cache_alias=DEFAULT_CACHE_ALIAS,
23
):
24
    """Decorator for memoizing function calls where we use the
25
    "local cache" to store the result.
26

27
    :arg int timeout: Number of seconds to store the result if not None
28
    :arg string prefix: If None becomes the function name.
29
    :arg function args_rewrite: Callable that rewrites the args first useful
30
    if your function needs nontrivial types but you know a simple way to
31
    re-represent them for the sake of the cache key.
32
    :arg function hit_callable: Gets executed if key was in cache.
33
    :arg function miss_callable: Gets executed if key was *not* in cache.
34
    :arg key_generator_callable: Custom cache key name generator.
35
    :arg bool store_result: If you know the result is not important, just
36
    that the cache blocked it from running repeatedly, set this to False.
37
    :arg Exception cache_exceptions: Accepts an Exception or a tuple of
38
    Exceptions. If the cached function raises any of these exceptions is the
39
    exception cached and raised as normal. Subsequent cached calls will
40
    immediately re-raise the exception and the function will not be executed.
41
    this tuple will be cached, all other will be propagated.
42
    :arg string cache_alias: The cache alias to use; defaults to 'default'.
43

44
    Usage::
45

46
        @cache_memoize(
47
            300,  # 5 min
48
            args_rewrite=lambda user: user.email,
49
            hit_callable=lambda: print("Cache hit!"),
50
            miss_callable=lambda: print("Cache miss :("),
51
        )
52
        def hash_user_email(user):
53
            dk = hashlib.pbkdf2_hmac('sha256', user.email, b'salt', 100000)
54
            return binascii.hexlify(dk)
55

56
    Or, when you don't actually need the result, useful if you know it's not
57
    valuable to store the execution result::
58

59
        @cache_memoize(
60
            300,  # 5 min
61
            store_result=False,
62
        )
63
        def send_email(email):
64
            somelib.send(email, subject="You rock!", ...)
65

66
    Also, whatever you do where things get cached, you can undo that.
67
    For example::
68

69
        @cache_memoize(100)
70
        def callmeonce(arg1):
71
            print(arg1)
72

73
        callmeonce('peter')  # will print 'peter'
74
        callmeonce('peter')  # nothing printed
75
        callmeonce.invalidate('peter')
76
        callmeonce('peter')  # will print 'peter'
77

78
    Suppose you know for good reason you want to bypass the cache and
79
    really let the decorator let you through you can set one extra
80
    keyword argument called `_refresh`. For example::
81

82
        @cache_memoize(100)
83
        def callmeonce(arg1):
84
            print(arg1)
85

86
        callmeonce('peter')                 # will print 'peter'
87
        callmeonce('peter')                 # nothing printed
88
        callmeonce('peter', _refresh=True)  # will print 'peter'
89

90
    """
91

92 4
    if args_rewrite is None:
93

94 4
        def noop(*args):
95 4
            return args
96

97 4
        args_rewrite = noop
98

99 4
    def decorator(func):
100 4
        def _default_make_cache_key(*args, **kwargs):
101 4
            cache_key = ":".join(
102
                [quote(str(x)) for x in args_rewrite(*args)]
103
                + [quote("{}={}".format(k, v)) for k, v in kwargs.items()]
104
            )
105 4
            return hashlib.md5(
106
                force_bytes("cache_memoize" + (prefix or func.__qualname__) + cache_key)
107
            ).hexdigest()
108

109 4
        _make_cache_key = key_generator_callable or _default_make_cache_key
110

111 4
        @wraps(func)
112
        def inner(*args, **kwargs):
113
            # The cache backend is fetched here (not in the outer decorator scope)
114
            # to guarantee thread-safety at runtime.
115 4
            cache = caches[cache_alias]
116
            # The cache key string should never be dependent on special keyword
117
            # arguments like _refresh. So extract it into a variable as soon as
118
            # possible.
119 4
            _refresh = bool(kwargs.pop("_refresh", False))
120 4
            cache_key = _make_cache_key(*args, **kwargs)
121 4
            if _refresh:
122 4
                result = MARKER
123
            else:
124 4
                result = cache.get(cache_key, MARKER)
125 4
            if result is MARKER:
126

127
                # If the function all raises an exception we want to cache,
128
                # catch it, else let it propagate.
129 4
                try:
130 4
                    result = func(*args, **kwargs)
131 4
                except cache_exceptions as exception:
132 4
                    result = exception
133

134 4
                if not store_result:
135
                    # Then the result isn't valuable/important to store but
136
                    # we want to store something. Just to remember that
137
                    # it has be done.
138 4
                    cache.set(cache_key, True, timeout)
139
                else:
140 4
                    cache.set(cache_key, result, timeout)
141 4
                if miss_callable:
142 4
                    miss_callable(*args, **kwargs)
143 4
            elif hit_callable:
144 4
                hit_callable(*args, **kwargs)
145

146
            # If the result is an exception we've caught and cached, raise it
147
            # in the end as to not change the API of the function we're caching.
148 4
            if isinstance(result, Exception):
149 4
                raise result
150 4
            return result
151

152 4
        def invalidate(*args, **kwargs):
153
            # The cache backend is fetched here (not in the outer decorator scope)
154
            # to guarantee thread-safety at runtime.
155 4
            cache = caches[cache_alias]
156 4
            kwargs.pop("_refresh", None)
157 4
            cache_key = _make_cache_key(*args, **kwargs)
158 4
            cache.delete(cache_key)
159

160 4
        inner.invalidate = invalidate
161 4
        return inner
162

163 4
    return decorator

Read our documentation on viewing source code .

Loading