File: core.py

package info (click to toggle)
python-memoize 1.0.3-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 108 kB
  • sloc: python: 324; makefile: 3
file content (195 lines) | stat: -rw-r--r-- 6,745 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import sys

from .time import time
from .func import MemoizedFunction
from .options import call_or_pass


DEFAULT_TIMEOUT = 10
CURRENT_PROTOCOL_VERSION = '1'
PROTOCOL_INDEX, CREATION_INDEX, EXPIRY_INDEX, ETAG_INDEX, VALUE_INDEX = list(range(5))


class Memoizer(object):
    """Cache and memoizer."""

    def __init__(self, store, **kwargs):
        kwargs['store'] = store
        self.regions = dict(default=kwargs)

    def _expand_opts(self, key, opts):
        region = None
        while region != 'default':

            # We look in the original opts (ie. specific to this function call)
            # for the region to start out in.
            if region is None:
                region = opts.get('region', 'default')

            # We keep looking at the parent of the current region, simulating
            # an inheritance chain.
            else:
                region = self.regions[region].get('parent', 'default')

            # Apply the region settings to the options.
            for k, v in self.regions[region].items():
                opts.setdefault(k, v)

        namespace = opts.get('namespace')
        if namespace:
            key = '%s:%s' % (namespace, key)

        store = opts['store']
        return key, store

    def _has_expired(self, data, opts):
        protocol, creation, old_expiry, old_etag, value = data
        assert protocol == CURRENT_PROTOCOL_VERSION, 'wrong protocol version: %r' % protocol

        current_time = time()

        # This one is obvious...
        if old_expiry and old_expiry < current_time:
            return True

        # It is expired if an etag has been set and provided, but they don't
        # match.
        etag = opts.get('etag')
        if etag is not None and etag != old_etag:
            return True

        # The new expiry time is too old. This seems odd to do... Oh well.
        expiry = opts.get('expiry')
        if expiry and expiry < current_time:
            return True

        # See if the creation time is too long ago for a given max_age.
        max_age = opts.get('max_age')
        if max_age is not None and (creation + max_age) < current_time:
            return True

    def get(self, key, func=None, args=(), kwargs=None, **opts):
        """Manually retrieve a value from the cache, calculating as needed.

        Params:
            key -> string to store/retrieve value from.
            func -> callable to generate value if it does not exist, or has
                expired.
            args -> positional arguments to call the function with.
            kwargs -> keyword arguments to call the function with.

        Keyword Params (options):
            These will be combined with region values (as selected by the
            "region" keyword argument, and then selected by "parent" values
            of those regions all the way up the chain to the "default" region).

            namespace -> string prefix to apply to the key before get/set.
            lock -> lock constructor. See README.
            expiry -> float unix expiration time.
            max_age -> float number of seconds until the value expires. Only
                provide expiry OR max_age, not both.

        """
        kwargs = kwargs or {}
        key, store = self._expand_opts(key, opts)

        # Resolve the etag.
        opts['etag'] = call_or_pass(opts.get('etag') or opts.get('etagger'), args, kwargs)

        if not isinstance(key, str):
            raise TypeError('non-string key of type %s' % type(key))

        data = store.get(key)
        if data is not None:
            if not self._has_expired(data, opts):
                return data[VALUE_INDEX]

        if func is None:
            return None

        # Prioritize passed options over a store's native lock.
        lock_func = opts.get('lock') or getattr(store, 'lock', None)
        lock = lock_func and lock_func(key)
        locked = lock and lock.acquire(opts.get('timeout', DEFAULT_TIMEOUT))

        try:
            value = func(*args, **kwargs)
        finally:
            if locked:
                lock.release()

        creation = time()
        expiry = call_or_pass(opts.get('expiry'), args, kwargs)
        max_age = call_or_pass(opts.get('max_age'), args, kwargs)
        if max_age is not None:
            expiry = min(x for x in (expiry, creation + max_age) if x is not None)

        # Need to be careful as this is the only place where we do not use the
        # lovely index constants.
        store[key] = (CURRENT_PROTOCOL_VERSION, creation, expiry, opts.get('etag'), value)

        return value

    def delete(self, key, **opts):
        """Remove a key from the cache."""
        key, store = self._expand_opts(key, opts)
        try:
            del store[key]
        except KeyError:
            pass

    def expire_at(self, key, expiry, **opts):
        """Set the explicit unix expiry time of a key."""
        key, store = self._expand_opts(key, opts)
        data = store.get(key)
        if data is not None:
            data = list(data)
            data[EXPIRY_INDEX] = expiry
            store[key] = tuple(data)
        else:
            raise KeyError(key)

    def expire(self, key, max_age, **opts):
        """Set the maximum age of a given key, in seconds."""
        self.expire_at(key, time() + max_age, **opts)

    def ttl(self, key, **opts):
        """Get the time-to-live of a given key; None if not set."""
        key, store = self._expand_opts(key, opts)
        if hasattr(store, 'ttl'):
            return store.ttl(key)
        data = store.get(key)
        if data is None:
            return None
        expiry = data[EXPIRY_INDEX]
        if expiry is not None:
            return max(0, expiry - time()) or None

    def etag(self, key, **opts):
        key, store = self._expand_opts(key, opts)
        data = store.get(key)
        return data and data[ETAG_INDEX]

    def exists(self, key, **opts):
        """Return if a key exists in the cache."""
        key, store = self._expand_opts(key, opts)
        data = store.get(key)
        # Note that we do not actually delete the thing here as the max_age
        # just for this call may have triggered a False.
        if not data or self._has_expired(data, opts):
            return False
        return True

    def __call__(self, *args, **opts):
        """A decorator to wrap around a function."""
        if args and hasattr(args[0], '__call__'):
            func = args[0]
            args = args[1:]
        else:
            # Build the decorator.
            return lambda func: self(func, *args, **opts)

        master_key = ','.join(map(repr, args)) if args else None
        return MemoizedFunction(self, func, master_key, opts)