/
__init__.py
577 lines (455 loc) · 20.4 KB
/
__init__.py
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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# -*- coding: utf-8 -*-
"""
flask.ext.cache
~~~~~~~~~~~~~~
Adds cache support to your application.
:copyright: (c) 2010 by Thadeus Burgess.
:license: BSD, see LICENSE for more details
"""
__version__ = '0.12'
__versionfull__ = __version__
import base64
import uuid
import hashlib
import inspect
import functools
import warnings
import logging
from werkzeug import import_string
from flask import request, current_app
logger = logging.getLogger(__name__)
TEMPLATE_FRAGMENT_KEY_TEMPLATE = '_template_fragment_cache_%s%s'
def function_namespace(f, args=None):
"""
Attempts to returns unique namespace for function
"""
m_args = inspect.getargspec(f)[0]
if len(m_args) and args:
if m_args[0] == 'self':
return '%s.%s.%s' % (f.__module__, args[0].__class__.__name__, f.__name__)
elif m_args[0] == 'cls':
return '%s.%s.%s' % (f.__module__, args[0].__name__, f.__name__)
if hasattr(f, '__func__'):
return '%s.%s.%s' % (f.__module__, f.__self__.__class__.__name__, f.__name__)
elif hasattr(f, '__class__'):
return '%s.%s.%s' % (f.__module__, f.__class__.__name__, f.__name__)
else:
return '%s.%s' % (f.__module__, f.__name__)
def make_template_fragment_key(fragment_name, vary_on=[]):
"""
Make a cache key for a specific fragment name
"""
if vary_on:
fragment_name = "%s_" % fragment_name
return TEMPLATE_FRAGMENT_KEY_TEMPLATE % (fragment_name, "_".join(vary_on))
#: Cache Object
################
class Cache(object):
"""
This class is used to control the cache objects.
"""
def __init__(self, app=None, with_jinja2_ext=True, config=None):
self.with_jinja2_ext = with_jinja2_ext
self.config = config
self.app = app
if app is not None:
self.init_app(app, config)
def init_app(self, app, config=None):
"This is used to initialize cache with your app object"
if not (config is None or isinstance(config, dict)):
raise ValueError("`config` must be an instance of dict or None")
if config is None:
config = self.config
if config is None:
config = app.config
config.setdefault('CACHE_DEFAULT_TIMEOUT', 300)
config.setdefault('CACHE_THRESHOLD', 500)
config.setdefault('CACHE_KEY_PREFIX', 'flask_cache_')
config.setdefault('CACHE_MEMCACHED_SERVERS', None)
config.setdefault('CACHE_DIR', None)
config.setdefault('CACHE_OPTIONS', None)
config.setdefault('CACHE_ARGS', [])
config.setdefault('CACHE_TYPE', 'null')
config.setdefault('CACHE_NO_NULL_WARNING', False)
if config['CACHE_TYPE'] == 'null' and not config['CACHE_NO_NULL_WARNING']:
warnings.warn("Flask-Cache: CACHE_TYPE is set to null, "
"caching is effectively disabled.")
if self.with_jinja2_ext:
from .jinja2ext import CacheExtension, JINJA_CACHE_ATTR_NAME
setattr(app.jinja_env, JINJA_CACHE_ATTR_NAME, self)
app.jinja_env.add_extension(CacheExtension)
self._set_cache(app, config)
def _set_cache(self, app, config):
import_me = config['CACHE_TYPE']
if '.' not in import_me:
from . import backends
try:
cache_obj = getattr(backends, import_me)
except AttributeError:
raise ImportError("%s is not a valid FlaskCache backend" % (
import_me))
else:
cache_obj = import_string(import_me)
cache_args = config['CACHE_ARGS'][:]
cache_options = {'default_timeout': config['CACHE_DEFAULT_TIMEOUT']}
if config['CACHE_OPTIONS']:
cache_options.update(config['CACHE_OPTIONS'])
if not hasattr(app, 'extensions'):
app.extensions = {}
app.extensions.setdefault('cache', {})
app.extensions['cache'][self] = cache_obj(
app, config, cache_args, cache_options)
@property
def cache(self):
app = self.app or current_app
return app.extensions['cache'][self]
def get(self, *args, **kwargs):
"Proxy function for internal cache object."
return self.cache.get(*args, **kwargs)
def set(self, *args, **kwargs):
"Proxy function for internal cache object."
self.cache.set(*args, **kwargs)
def add(self, *args, **kwargs):
"Proxy function for internal cache object."
self.cache.add(*args, **kwargs)
def delete(self, *args, **kwargs):
"Proxy function for internal cache object."
self.cache.delete(*args, **kwargs)
def delete_many(self, *args, **kwargs):
"Proxy function for internal cache object."
self.cache.delete_many(*args, **kwargs)
def clear(self):
"Proxy function for internal cache object."
self.cache.clear()
def get_many(self, *args, **kwargs):
"Proxy function for internal cache object."
return self.cache.get_many(*args, **kwargs)
def set_many(self, *args, **kwargs):
"Proxy function for internal cache object."
self.cache.set_many(*args, **kwargs)
def cached(self, timeout=None, key_prefix='view/%s', unless=None):
"""
Decorator. Use this to cache a function. By default the cache key
is `view/request.path`. You are able to use this decorator with any
function by changing the `key_prefix`. If the token `%s` is located
within the `key_prefix` then it will replace that with `request.path`
Example::
# An example view function
@cache.cached(timeout=50)
def big_foo():
return big_bar_calc()
# An example misc function to cache.
@cache.cached(key_prefix='MyCachedList')
def get_list():
return [random.randrange(0, 1) for i in range(50000)]
my_list = get_list()
.. note::
You MUST have a request context to actually called any functions
that are cached.
.. versionadded:: 0.4
The returned decorated function now has three function attributes
assigned to it. These attributes are readable/writable.
**uncached**
The original undecorated function
**cache_timeout**
The cache timeout value for this function. For a custom value
to take affect, this must be set before the function is called.
**make_cache_key**
A function used in generating the cache_key used.
:param timeout: Default None. If set to an integer, will cache for that
amount of time. Unit of time is in seconds.
:param key_prefix: Default 'view/%(request.path)s'. Beginning key to .
use for the cache key.
.. versionadded:: 0.3.4
Can optionally be a callable which takes no arguments
but returns a string that will be used as the cache_key.
:param unless: Default None. Cache will *always* execute the caching
facilities unless this callable is true.
This will bypass the caching entirely.
"""
def decorator(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
#: Bypass the cache entirely.
if callable(unless) and unless() is True:
return f(*args, **kwargs)
try:
cache_key = decorated_function.make_cache_key(*args, **kwargs)
rv = self.cache.get(cache_key)
except Exception:
if current_app.debug:
raise
logger.exception("Exception possibly due to cache backend.")
return f(*args, **kwargs)
if rv is None:
rv = f(*args, **kwargs)
try:
self.cache.set(cache_key, rv,
timeout=decorated_function.cache_timeout)
except Exception:
if current_app.debug:
raise
logger.exception("Exception possibly due to cache backend.")
return f(*args, **kwargs)
return rv
def make_cache_key(*args, **kwargs):
if callable(key_prefix):
cache_key = key_prefix()
elif '%s' in key_prefix:
cache_key = key_prefix % request.path
else:
cache_key = key_prefix
cache_key = cache_key.encode('utf-8')
return cache_key
decorated_function.uncached = f
decorated_function.cache_timeout = timeout
decorated_function.make_cache_key = make_cache_key
return decorated_function
return decorator
def _memvname(self, funcname):
return funcname + '_memver'
def memoize_make_version_hash(self):
return base64.b64encode(uuid.uuid4().bytes)[:6].decode('utf-8')
def memoize_make_cache_key(self, make_name=None):
"""
Function used to create the cache_key for memoized functions.
"""
def make_cache_key(f, *args, **kwargs):
fname = function_namespace(f, args)
version_key = self._memvname(fname)
version_data = self.cache.get(version_key)
if version_data is None:
version_data = self.memoize_make_version_hash()
self.cache.set(version_key, version_data)
cache_key = hashlib.md5()
#: this should have to be after version_data, so that it
#: does not break the delete_memoized functionality.
if callable(make_name):
altfname = make_name(fname)
else:
altfname = fname
if callable(f):
keyargs, keykwargs = self.memoize_kwargs_to_args(f,
*args,
**kwargs)
else:
keyargs, keykwargs = args, kwargs
try:
updated = "{0}{1}{2}".format(altfname, keyargs, keykwargs)
except AttributeError:
updated = "%s%s%s" % (altfname, keyargs, keykwargs)
cache_key.update(updated.encode('utf-8'))
cache_key = base64.b64encode(cache_key.digest())[:16]
cache_key = cache_key.decode('utf-8')
cache_key += version_data
return cache_key
return make_cache_key
def memoize_kwargs_to_args(self, f, *args, **kwargs):
#: Inspect the arguments to the function
#: This allows the memoization to be the same
#: whether the function was called with
#: 1, b=2 is equivilant to a=1, b=2, etc.
new_args = []
arg_num = 0
argspec = inspect.getargspec(f)
args_len = len(argspec.args)
for i in range(args_len):
if i == 0 and argspec.args[i] in ('self', 'cls'):
#: use the repr of the class instance
#: this supports instance methods for
#: the memoized functions, giving more
#: flexibility to developers
arg = repr(args[0])
arg_num += 1
elif argspec.args[i] in kwargs:
arg = kwargs[argspec.args[i]]
elif arg_num < len(args):
arg = args[arg_num]
arg_num += 1
elif abs(i-args_len) <= len(argspec.defaults):
arg = argspec.defaults[i-args_len]
arg_num += 1
else:
arg = None
arg_num += 1
#: Attempt to convert all arguments to a
#: hash/id or a representation?
#: Not sure if this is necessary, since
#: using objects as keys gets tricky quickly.
# if hasattr(arg, '__class__'):
# try:
# arg = hash(arg)
# except:
# arg = repr(arg)
#: Or what about a special __cacherepr__ function
#: on an object, this allows objects to act normal
#: upon inspection, yet they can define a representation
#: that can be used to make the object unique in the
#: cache key. Given that a case comes across that
#: an object "must" be used as a cache key
# if hasattr(arg, '__cacherepr__'):
# arg = arg.__cacherepr__
new_args.append(arg)
return tuple(new_args), {}
def memoize(self, timeout=None, make_name=None, unless=None):
"""
Use this to cache the result of a function, taking its arguments into
account in the cache key.
Information on
`Memoization <http://en.wikipedia.org/wiki/Memoization>`_.
Example::
@cache.memoize(timeout=50)
def big_foo(a, b):
return a + b + random.randrange(0, 1000)
.. code-block:: pycon
>>> big_foo(5, 2)
753
>>> big_foo(5, 3)
234
>>> big_foo(5, 2)
753
.. versionadded:: 0.4
The returned decorated function now has three function attributes
assigned to it.
**uncached**
The original undecorated function. readable only
**cache_timeout**
The cache timeout value for this function. For a custom value
to take affect, this must be set before the function is called.
readable and writable
**make_cache_key**
A function used in generating the cache_key used.
readable and writable
:param timeout: Default None. If set to an integer, will cache for that
amount of time. Unit of time is in seconds.
:param make_name: Default None. If set this is a function that accepts
a single argument, the function name, and returns a
new string to be used as the function name. If not set
then the function name is used.
:param unless: Default None. Cache will *always* execute the caching
facilities unelss this callable is true.
This will bypass the caching entirely.
.. versionadded:: 0.5
params ``make_name``, ``unless``
"""
def memoize(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
#: bypass cache
if callable(unless) and unless() is True:
return f(*args, **kwargs)
try:
cache_key = decorated_function.make_cache_key(f, *args, **kwargs)
rv = self.cache.get(cache_key)
except Exception:
if current_app.debug:
raise
logger.exception("Exception possibly due to cache backend.")
return f(*args, **kwargs)
if rv is None:
rv = f(*args, **kwargs)
try:
self.cache.set(cache_key, rv,
timeout=decorated_function.cache_timeout)
except Exception:
if current_app.debug:
raise
logger.exception("Exception possibly due to cache backend.")
return f(*args, **kwargs)
return rv
decorated_function.uncached = f
decorated_function.cache_timeout = timeout
decorated_function.make_cache_key = self.memoize_make_cache_key(make_name)
decorated_function.delete_memoized = lambda: self.delete_memoized(f)
return decorated_function
return memoize
def delete_memoized(self, f, *args, **kwargs):
"""
Deletes the specified functions caches, based by given parameters.
If parameters are given, only the functions that were memoized with them
will be erased. Otherwise all versions of the caches will be forgotten.
Example::
@cache.memoize(50)
def random_func():
return random.randrange(1, 50)
@cache.memoize()
def param_func(a, b):
return a+b+random.randrange(1, 50)
.. code-block:: pycon
>>> random_func()
43
>>> random_func()
43
>>> cache.delete_memoized('random_func')
>>> random_func()
16
>>> param_func(1, 2)
32
>>> param_func(1, 2)
32
>>> param_func(2, 2)
47
>>> cache.delete_memoized('param_func', 1, 2)
>>> param_func(1, 2)
13
>>> param_func(2, 2)
47
:param fname: Name of the memoized function, or a reference to the function.
:param \*args: A list of positional parameters used with memoized function.
:param \**kwargs: A dict of named parameters used with memoized function.
.. note::
Flask-Cache uses inspect to order kwargs into positional args when
the function is memoized. If you pass a function reference into ``fname``
instead of the function name, Flask-Cache will be able to place
the args/kwargs in the proper order, and delete the positional cache.
However, if ``delete_memozied`` is just called with the name of the
function, be sure to pass in potential arguments in the same order
as defined in your function as args only, otherwise Flask-Cache
will not be able to compute the same cache key.
.. note::
Flask-Cache maintains an internal random version hash for the function.
Using delete_memoized will only swap out the version hash, causing
the memoize function to recompute results and put them into another key.
This leaves any computed caches for this memoized function within the
caching backend.
It is recommended to use a very high timeout with memoize if using
this function, so that when the version has is swapped, the old cached
results would eventually be reclaimed by the caching backend.
"""
if not callable(f):
raise DeprecationWarning("Deleting messages by relative name is no longer"
" reliable, please switch to a function reference")
_fname = function_namespace(f, args)
try:
if not args and not kwargs:
version_key = self._memvname(_fname)
version_data = self.memoize_make_version_hash()
self.cache.set(version_key, version_data)
else:
cache_key = f.make_cache_key(f.uncached, *args, **kwargs)
self.cache.delete(cache_key)
except Exception:
if current_app.debug:
raise
logger.exception("Exception possibly due to cache backend.")
def delete_memoized_verhash(self, f, *args):
"""
Delete the version hash associated with the function.
..warning::
Performing this operation could leave keys behind that have
been created with this version hash. It is up to the application
to make sure that all keys that may have been created with this
version hash at least have timeouts so they will not sit orphaned
in the cache backend.
"""
if not callable(f):
raise DeprecationWarning("Deleting messages by relative name is no longer"
" reliable, please use a function reference")
_fname = function_namespace(f, args)
try:
version_key = self._memvname(_fname)
self.cache.delete(version_key)
except Exception:
if current_app.debug:
raise
logger.exception("Exception possibly due to cache backend.")