diskcache-community 5.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diskcache/__init__.py +113 -0
- diskcache/cli.py +1 -0
- diskcache/core.py +3328 -0
- diskcache/djangocache.py +480 -0
- diskcache/fanout.py +717 -0
- diskcache/persistent.py +1253 -0
- diskcache/recipes.py +491 -0
- diskcache_community-5.7.0.dist-info/METADATA +491 -0
- diskcache_community-5.7.0.dist-info/RECORD +13 -0
- diskcache_community-5.7.0.dist-info/WHEEL +5 -0
- diskcache_community-5.7.0.dist-info/licenses/LICENSE +12 -0
- diskcache_community-5.7.0.dist-info/licenses/NOTICE +9 -0
- diskcache_community-5.7.0.dist-info/top_level.txt +1 -0
diskcache/core.py
ADDED
|
@@ -0,0 +1,3328 @@
|
|
|
1
|
+
"""Core disk and file backed cache API.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
import codecs
|
|
5
|
+
import collections as co
|
|
6
|
+
import contextlib as cl
|
|
7
|
+
import datetime as dt
|
|
8
|
+
import decimal
|
|
9
|
+
import errno
|
|
10
|
+
import fractions
|
|
11
|
+
import functools as ft
|
|
12
|
+
import inspect
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import os.path as op
|
|
17
|
+
import pathlib
|
|
18
|
+
import pickle
|
|
19
|
+
import pickletools
|
|
20
|
+
import sqlite3
|
|
21
|
+
import struct
|
|
22
|
+
import tempfile
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
26
|
+
import warnings
|
|
27
|
+
import weakref
|
|
28
|
+
import zlib
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def full_name(func):
|
|
32
|
+
"""Return full name of `func` by adding the module and function name."""
|
|
33
|
+
return func.__module__ + '.' + func.__qualname__
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Constant(tuple):
|
|
37
|
+
"""Pretty display of immutable constant."""
|
|
38
|
+
|
|
39
|
+
def __new__(cls, name):
|
|
40
|
+
return tuple.__new__(cls, (name,))
|
|
41
|
+
|
|
42
|
+
def __repr__(self):
|
|
43
|
+
return '%s' % self[0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
DBNAME = 'cache.db'
|
|
47
|
+
ENOVAL = Constant('ENOVAL')
|
|
48
|
+
UNKNOWN = Constant('UNKNOWN')
|
|
49
|
+
|
|
50
|
+
MODE_NONE = 0
|
|
51
|
+
MODE_RAW = 1
|
|
52
|
+
MODE_BINARY = 2
|
|
53
|
+
MODE_TEXT = 3
|
|
54
|
+
MODE_PICKLE = 4
|
|
55
|
+
|
|
56
|
+
DEFAULT_SETTINGS = {
|
|
57
|
+
'statistics': 0, # False
|
|
58
|
+
'tag_index': 0, # False
|
|
59
|
+
'eviction_policy': 'least-recently-stored',
|
|
60
|
+
'size_limit': 2**30, # 1gb
|
|
61
|
+
'cull_limit': 10,
|
|
62
|
+
'sqlite_auto_vacuum': 1, # FULL
|
|
63
|
+
'sqlite_cache_size': 2**13, # 8,192 pages
|
|
64
|
+
'sqlite_journal_mode': 'wal',
|
|
65
|
+
'sqlite_mmap_size': 2**26, # 64mb
|
|
66
|
+
'sqlite_synchronous': 1, # NORMAL
|
|
67
|
+
'disk_min_file_size': 2**15, # 32kb
|
|
68
|
+
'disk_pickle_protocol': pickle.HIGHEST_PROTOCOL,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
METADATA = {
|
|
72
|
+
'count': 0,
|
|
73
|
+
'size': 0,
|
|
74
|
+
'hits': 0,
|
|
75
|
+
'misses': 0,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Triggers that keep the "count" and "size" metadata in the Settings table in
|
|
79
|
+
# step with the Cache table, as (name, definition) pairs where the definition
|
|
80
|
+
# is the text that follows "CREATE TRIGGER <name>". Caches written by
|
|
81
|
+
# versions before 5.7.0 store these triggers with double-quoted literals,
|
|
82
|
+
# which SQLite builds compiled with SQLITE_DQS=0 reject (issue #343), so
|
|
83
|
+
# Cache.__init__ compares the stored trigger text with these definitions and
|
|
84
|
+
# recreates the triggers when they differ.
|
|
85
|
+
|
|
86
|
+
SETTINGS_TRIGGERS = (
|
|
87
|
+
(
|
|
88
|
+
'Settings_count_insert',
|
|
89
|
+
'AFTER INSERT ON Cache FOR EACH ROW BEGIN'
|
|
90
|
+
' UPDATE Settings SET value = value + 1'
|
|
91
|
+
" WHERE key = 'count'; END",
|
|
92
|
+
),
|
|
93
|
+
(
|
|
94
|
+
'Settings_count_delete',
|
|
95
|
+
'AFTER DELETE ON Cache FOR EACH ROW BEGIN'
|
|
96
|
+
' UPDATE Settings SET value = value - 1'
|
|
97
|
+
" WHERE key = 'count'; END",
|
|
98
|
+
),
|
|
99
|
+
(
|
|
100
|
+
'Settings_size_insert',
|
|
101
|
+
'AFTER INSERT ON Cache FOR EACH ROW BEGIN'
|
|
102
|
+
' UPDATE Settings SET value = value + NEW.size'
|
|
103
|
+
" WHERE key = 'size'; END",
|
|
104
|
+
),
|
|
105
|
+
(
|
|
106
|
+
'Settings_size_update',
|
|
107
|
+
'AFTER UPDATE ON Cache FOR EACH ROW BEGIN'
|
|
108
|
+
' UPDATE Settings'
|
|
109
|
+
' SET value = value + NEW.size - OLD.size'
|
|
110
|
+
" WHERE key = 'size'; END",
|
|
111
|
+
),
|
|
112
|
+
(
|
|
113
|
+
'Settings_size_delete',
|
|
114
|
+
'AFTER DELETE ON Cache FOR EACH ROW BEGIN'
|
|
115
|
+
' UPDATE Settings SET value = value - OLD.size'
|
|
116
|
+
" WHERE key = 'size'; END",
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
EVICTION_POLICY = {
|
|
121
|
+
'none': {
|
|
122
|
+
'init': None,
|
|
123
|
+
'get': None,
|
|
124
|
+
'cull': None,
|
|
125
|
+
},
|
|
126
|
+
'least-recently-stored': {
|
|
127
|
+
'init': (
|
|
128
|
+
'CREATE INDEX IF NOT EXISTS Cache_store_time ON'
|
|
129
|
+
' Cache (store_time)'
|
|
130
|
+
),
|
|
131
|
+
'get': None,
|
|
132
|
+
'cull': 'SELECT {fields} FROM Cache ORDER BY store_time LIMIT ?',
|
|
133
|
+
},
|
|
134
|
+
'least-recently-used': {
|
|
135
|
+
'init': (
|
|
136
|
+
'CREATE INDEX IF NOT EXISTS Cache_access_time ON'
|
|
137
|
+
' Cache (access_time)'
|
|
138
|
+
),
|
|
139
|
+
'get': 'access_time = {now}',
|
|
140
|
+
'cull': 'SELECT {fields} FROM Cache ORDER BY access_time LIMIT ?',
|
|
141
|
+
},
|
|
142
|
+
'least-frequently-used': {
|
|
143
|
+
'init': (
|
|
144
|
+
'CREATE INDEX IF NOT EXISTS Cache_access_count ON'
|
|
145
|
+
' Cache (access_count)'
|
|
146
|
+
),
|
|
147
|
+
'get': 'access_count = access_count + 1',
|
|
148
|
+
'cull': 'SELECT {fields} FROM Cache ORDER BY access_count LIMIT ?',
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def native_filename(filename):
|
|
154
|
+
"""Return `filename` with the path separators of the current platform.
|
|
155
|
+
|
|
156
|
+
The Cache table's `filename` column stores a path relative to the cache
|
|
157
|
+
directory with the separator of the operating system that wrote the
|
|
158
|
+
value, so a cache written on Windows holds backslashes and a cache written
|
|
159
|
+
anywhere else holds slashes (upstream issue 304). Both forms are accepted:
|
|
160
|
+
on Windows, slashes become backslashes so paths compare equal to those
|
|
161
|
+
returned by `os.walk`; elsewhere, backslashes become slashes. Filenames are
|
|
162
|
+
generated by :meth:`Disk.filename`, which never uses either character
|
|
163
|
+
except as a separator, so no stored name is misread.
|
|
164
|
+
|
|
165
|
+
:param str filename: filename relative to the cache directory
|
|
166
|
+
:return: filename with native separators
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
if os.sep == '/':
|
|
170
|
+
return filename.replace('\\', '/')
|
|
171
|
+
return filename.replace('/', os.sep)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _makedirs_private(directory):
|
|
175
|
+
"""Create `directory` and its missing parents with mode ``0o700``.
|
|
176
|
+
|
|
177
|
+
Unlike :func:`os.makedirs`, the mode applies to every directory created,
|
|
178
|
+
not only to the last one, so no directory the cache creates is readable
|
|
179
|
+
by other users. Directories that already exist keep their mode. Windows
|
|
180
|
+
ignores the mode bits.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
parent = op.dirname(directory)
|
|
184
|
+
|
|
185
|
+
if parent and parent != directory and not op.isdir(parent):
|
|
186
|
+
_makedirs_private(parent)
|
|
187
|
+
|
|
188
|
+
with cl.suppress(FileExistsError):
|
|
189
|
+
os.mkdir(directory, 0o700)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Disk:
|
|
193
|
+
"""Cache key and value serialization for SQLite database and files."""
|
|
194
|
+
|
|
195
|
+
def __init__(self, directory, min_file_size=0, pickle_protocol=0):
|
|
196
|
+
"""Initialize disk instance.
|
|
197
|
+
|
|
198
|
+
:param str directory: directory path
|
|
199
|
+
:param int min_file_size: minimum size for file use
|
|
200
|
+
:param int pickle_protocol: pickle protocol for serialization
|
|
201
|
+
|
|
202
|
+
"""
|
|
203
|
+
self._directory = directory
|
|
204
|
+
self.min_file_size = min_file_size
|
|
205
|
+
self.pickle_protocol = pickle_protocol
|
|
206
|
+
|
|
207
|
+
def hash(self, key):
|
|
208
|
+
"""Compute portable hash for `key`.
|
|
209
|
+
|
|
210
|
+
:param key: key to hash
|
|
211
|
+
:return: hash value
|
|
212
|
+
|
|
213
|
+
"""
|
|
214
|
+
mask = 0xFFFFFFFF
|
|
215
|
+
disk_key, _ = self.put(key)
|
|
216
|
+
type_disk_key = type(disk_key)
|
|
217
|
+
|
|
218
|
+
if type_disk_key is sqlite3.Binary:
|
|
219
|
+
return zlib.adler32(disk_key) & mask
|
|
220
|
+
elif type_disk_key is str:
|
|
221
|
+
return zlib.adler32(disk_key.encode('utf-8')) & mask # noqa
|
|
222
|
+
elif type_disk_key is int:
|
|
223
|
+
return disk_key % mask
|
|
224
|
+
else:
|
|
225
|
+
assert type_disk_key is float
|
|
226
|
+
return zlib.adler32(struct.pack('!d', disk_key)) & mask
|
|
227
|
+
|
|
228
|
+
def put(self, key):
|
|
229
|
+
"""Convert `key` to fields key and raw for Cache table.
|
|
230
|
+
|
|
231
|
+
:param key: key to convert
|
|
232
|
+
:return: (database key, raw boolean) pair
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
# pylint: disable=unidiomatic-typecheck
|
|
236
|
+
type_key = type(key)
|
|
237
|
+
|
|
238
|
+
if type_key is bytes:
|
|
239
|
+
return sqlite3.Binary(key), True
|
|
240
|
+
elif (
|
|
241
|
+
(type_key is str)
|
|
242
|
+
or (
|
|
243
|
+
type_key is int
|
|
244
|
+
and -9223372036854775808 <= key <= 9223372036854775807
|
|
245
|
+
)
|
|
246
|
+
or (type_key is float)
|
|
247
|
+
):
|
|
248
|
+
return key, True
|
|
249
|
+
else:
|
|
250
|
+
data = pickle.dumps(key, protocol=self.pickle_protocol)
|
|
251
|
+
result = pickletools.optimize(data)
|
|
252
|
+
return sqlite3.Binary(result), False
|
|
253
|
+
|
|
254
|
+
def get(self, key, raw):
|
|
255
|
+
"""Convert fields `key` and `raw` from Cache table to key.
|
|
256
|
+
|
|
257
|
+
:param key: database key to convert
|
|
258
|
+
:param bool raw: flag indicating raw database storage
|
|
259
|
+
:return: corresponding Python key
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
# pylint: disable=unidiomatic-typecheck
|
|
263
|
+
if raw:
|
|
264
|
+
return bytes(key) if type(key) is sqlite3.Binary else key
|
|
265
|
+
else:
|
|
266
|
+
return pickle.load(io.BytesIO(key))
|
|
267
|
+
|
|
268
|
+
def store(self, value, read, key=UNKNOWN):
|
|
269
|
+
"""Convert `value` to fields size, mode, filename, and value for Cache
|
|
270
|
+
table.
|
|
271
|
+
|
|
272
|
+
:param value: value to convert
|
|
273
|
+
:param bool read: True when value is file-like object
|
|
274
|
+
:param key: key for item (default UNKNOWN)
|
|
275
|
+
:return: (size, mode, filename, value) tuple for Cache table
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
# pylint: disable=unidiomatic-typecheck
|
|
279
|
+
type_value = type(value)
|
|
280
|
+
min_file_size = self.min_file_size
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
(type_value is str and len(value) < min_file_size)
|
|
284
|
+
or (
|
|
285
|
+
type_value is int
|
|
286
|
+
and -9223372036854775808 <= value <= 9223372036854775807
|
|
287
|
+
)
|
|
288
|
+
or (type_value is float)
|
|
289
|
+
):
|
|
290
|
+
return 0, MODE_RAW, None, value
|
|
291
|
+
elif type_value is bytes:
|
|
292
|
+
if len(value) < min_file_size:
|
|
293
|
+
return 0, MODE_RAW, None, sqlite3.Binary(value)
|
|
294
|
+
else:
|
|
295
|
+
filename, full_path = self.filename(key, value)
|
|
296
|
+
self._write(full_path, value, 'xb')
|
|
297
|
+
return len(value), MODE_BINARY, filename, None
|
|
298
|
+
elif type_value is str:
|
|
299
|
+
filename, full_path = self.filename(key, value)
|
|
300
|
+
self._write(full_path, io.StringIO(value), 'x', 'UTF-8')
|
|
301
|
+
size = op.getsize(full_path)
|
|
302
|
+
return size, MODE_TEXT, filename, None
|
|
303
|
+
elif read:
|
|
304
|
+
reader = ft.partial(value.read, 2**22)
|
|
305
|
+
filename, full_path = self.filename(key, value)
|
|
306
|
+
iterator = iter(reader, b'')
|
|
307
|
+
size = self._write(full_path, iterator, 'xb')
|
|
308
|
+
return size, MODE_BINARY, filename, None
|
|
309
|
+
else:
|
|
310
|
+
result = pickle.dumps(value, protocol=self.pickle_protocol)
|
|
311
|
+
|
|
312
|
+
if len(result) < min_file_size:
|
|
313
|
+
return 0, MODE_PICKLE, None, sqlite3.Binary(result)
|
|
314
|
+
else:
|
|
315
|
+
filename, full_path = self.filename(key, value)
|
|
316
|
+
self._write(full_path, result, 'xb')
|
|
317
|
+
return len(result), MODE_PICKLE, filename, None
|
|
318
|
+
|
|
319
|
+
def _write(self, full_path, iterator_or_bytes, mode, encoding=None):
|
|
320
|
+
full_dir, _ = op.split(full_path)
|
|
321
|
+
|
|
322
|
+
for count in range(1, 11):
|
|
323
|
+
with cl.suppress(OSError):
|
|
324
|
+
_makedirs_private(full_dir)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
# Another cache may have deleted the directory before
|
|
328
|
+
# the file could be opened.
|
|
329
|
+
writer = open(full_path, mode, encoding=encoding)
|
|
330
|
+
except OSError:
|
|
331
|
+
if count == 10:
|
|
332
|
+
# Give up after 10 tries to open the file.
|
|
333
|
+
raise
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
with writer:
|
|
337
|
+
if isinstance(iterator_or_bytes, bytes):
|
|
338
|
+
writer.write(iterator_or_bytes)
|
|
339
|
+
return len(iterator_or_bytes)
|
|
340
|
+
else:
|
|
341
|
+
size = 0
|
|
342
|
+
for chunk in iterator_or_bytes:
|
|
343
|
+
size += len(chunk)
|
|
344
|
+
writer.write(chunk)
|
|
345
|
+
return size
|
|
346
|
+
|
|
347
|
+
def fetch(self, mode, filename, value, read):
|
|
348
|
+
"""Convert fields `mode`, `filename`, and `value` from Cache table to
|
|
349
|
+
value.
|
|
350
|
+
|
|
351
|
+
:param int mode: value mode raw, binary, text, or pickle
|
|
352
|
+
:param str filename: filename of corresponding value, with the path
|
|
353
|
+
separators of the operating system that stored it
|
|
354
|
+
:param value: database value
|
|
355
|
+
:param bool read: when True, return an open file handle
|
|
356
|
+
:return: corresponding Python value
|
|
357
|
+
:raises: IOError if the value cannot be read
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
# pylint: disable=unidiomatic-typecheck,consider-using-with
|
|
361
|
+
if mode == MODE_RAW:
|
|
362
|
+
return bytes(value) if type(value) is sqlite3.Binary else value
|
|
363
|
+
elif mode == MODE_BINARY:
|
|
364
|
+
full_path = op.join(self._directory, native_filename(filename))
|
|
365
|
+
if read:
|
|
366
|
+
return open(full_path, 'rb')
|
|
367
|
+
else:
|
|
368
|
+
with open(full_path, 'rb') as reader:
|
|
369
|
+
return reader.read()
|
|
370
|
+
elif mode == MODE_TEXT:
|
|
371
|
+
full_path = op.join(self._directory, native_filename(filename))
|
|
372
|
+
with open(full_path, 'r', encoding='UTF-8') as reader:
|
|
373
|
+
return reader.read()
|
|
374
|
+
elif mode == MODE_PICKLE:
|
|
375
|
+
if value is None:
|
|
376
|
+
full_path = op.join(self._directory, native_filename(filename))
|
|
377
|
+
with open(full_path, 'rb') as reader:
|
|
378
|
+
return pickle.load(reader)
|
|
379
|
+
else:
|
|
380
|
+
return pickle.load(io.BytesIO(value))
|
|
381
|
+
|
|
382
|
+
def filename(self, key=UNKNOWN, value=UNKNOWN):
|
|
383
|
+
"""Return filename and full-path tuple for file storage.
|
|
384
|
+
|
|
385
|
+
Filename will be a randomly generated 28 character hexadecimal string
|
|
386
|
+
with ".val" suffixed. Two levels of sub-directories will be used to
|
|
387
|
+
reduce the size of directories. On older filesystems, lookups in
|
|
388
|
+
directories with many files may be slow.
|
|
389
|
+
|
|
390
|
+
The default implementation ignores the `key` and `value` parameters.
|
|
391
|
+
|
|
392
|
+
In some scenarios, for example :meth:`Cache.push
|
|
393
|
+
<diskcache.Cache.push>`, the `key` or `value` may not be known when the
|
|
394
|
+
item is stored in the cache.
|
|
395
|
+
|
|
396
|
+
:param key: key for item (default UNKNOWN)
|
|
397
|
+
:param value: value for item (default UNKNOWN)
|
|
398
|
+
|
|
399
|
+
"""
|
|
400
|
+
# pylint: disable=unused-argument
|
|
401
|
+
hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
|
|
402
|
+
sub_dir = op.join(hex_name[:2], hex_name[2:4])
|
|
403
|
+
name = hex_name[4:] + '.val'
|
|
404
|
+
filename = op.join(sub_dir, name)
|
|
405
|
+
full_path = op.join(self._directory, filename)
|
|
406
|
+
return filename, full_path
|
|
407
|
+
|
|
408
|
+
def remove(self, file_path):
|
|
409
|
+
"""Remove a file given by `file_path`.
|
|
410
|
+
|
|
411
|
+
This method is cross-thread and cross-process safe. If an OSError
|
|
412
|
+
occurs, it is suppressed.
|
|
413
|
+
|
|
414
|
+
:param str file_path: relative path to file, with the path separators
|
|
415
|
+
of the operating system that stored it
|
|
416
|
+
|
|
417
|
+
"""
|
|
418
|
+
full_path = op.join(self._directory, native_filename(file_path))
|
|
419
|
+
full_dir, _ = op.split(full_path)
|
|
420
|
+
|
|
421
|
+
# Suppress OSError that may occur if two caches attempt to delete the
|
|
422
|
+
# same file or directory at the same time.
|
|
423
|
+
|
|
424
|
+
with cl.suppress(OSError):
|
|
425
|
+
os.remove(full_path)
|
|
426
|
+
|
|
427
|
+
with cl.suppress(OSError):
|
|
428
|
+
os.removedirs(full_dir)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class JSONDisk(Disk):
|
|
432
|
+
"""Cache key and value using JSON serialization with zlib compression."""
|
|
433
|
+
|
|
434
|
+
def __init__(self, directory, compress_level=1, **kwargs):
|
|
435
|
+
"""Initialize JSON disk instance.
|
|
436
|
+
|
|
437
|
+
Keys and values are compressed using the zlib library. The
|
|
438
|
+
`compress_level` is an integer from 0 to 9 controlling the level of
|
|
439
|
+
compression; 1 is fastest and produces the least compression, 9 is
|
|
440
|
+
slowest and produces the most compression, and 0 is no compression.
|
|
441
|
+
|
|
442
|
+
:param str directory: directory path
|
|
443
|
+
:param int compress_level: zlib compression level (default 1)
|
|
444
|
+
:param kwargs: super class arguments
|
|
445
|
+
|
|
446
|
+
"""
|
|
447
|
+
self.compress_level = compress_level
|
|
448
|
+
super().__init__(directory, **kwargs)
|
|
449
|
+
|
|
450
|
+
def put(self, key):
|
|
451
|
+
json_bytes = json.dumps(key).encode('utf-8')
|
|
452
|
+
data = zlib.compress(json_bytes, self.compress_level)
|
|
453
|
+
return super().put(data)
|
|
454
|
+
|
|
455
|
+
def get(self, key, raw):
|
|
456
|
+
data = super().get(key, raw)
|
|
457
|
+
return json.loads(zlib.decompress(data).decode('utf-8'))
|
|
458
|
+
|
|
459
|
+
def store(self, value, read, key=UNKNOWN):
|
|
460
|
+
if not read:
|
|
461
|
+
json_bytes = json.dumps(value).encode('utf-8')
|
|
462
|
+
value = zlib.compress(json_bytes, self.compress_level)
|
|
463
|
+
return super().store(value, read, key=key)
|
|
464
|
+
|
|
465
|
+
def fetch(self, mode, filename, value, read):
|
|
466
|
+
data = super().fetch(mode, filename, value, read)
|
|
467
|
+
if not read:
|
|
468
|
+
data = json.loads(zlib.decompress(data).decode('utf-8'))
|
|
469
|
+
return data
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# Names that SafeDisk lets pickle resolve while reading, keyed by the module
|
|
473
|
+
# recorded in the pickle. Protocols 0 to 2 record the Python 2 module names
|
|
474
|
+
# and use copyreg._reconstructor and _codecs.encode to rebuild objects, so
|
|
475
|
+
# those spellings and helpers are included. Every entry is a standard-library
|
|
476
|
+
# constructor whose call cannot run code beyond building the object itself.
|
|
477
|
+
# Helpers that reach attributes, such as builtins.getattr, which pickle uses
|
|
478
|
+
# for zoneinfo.ZoneInfo, are deliberately absent.
|
|
479
|
+
|
|
480
|
+
_SAFE_BUILTINS = frozenset(
|
|
481
|
+
{
|
|
482
|
+
'bytearray',
|
|
483
|
+
'bytes',
|
|
484
|
+
'complex',
|
|
485
|
+
'dict',
|
|
486
|
+
'float',
|
|
487
|
+
'frozenset',
|
|
488
|
+
'int',
|
|
489
|
+
'list',
|
|
490
|
+
'object',
|
|
491
|
+
'range',
|
|
492
|
+
'set',
|
|
493
|
+
'slice',
|
|
494
|
+
'str',
|
|
495
|
+
'tuple',
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
SAFE_PICKLE_NAMES = {
|
|
500
|
+
'builtins': _SAFE_BUILTINS,
|
|
501
|
+
'__builtin__': _SAFE_BUILTINS | {'long', 'unicode', 'xrange'},
|
|
502
|
+
'collections': frozenset(
|
|
503
|
+
{'Counter', 'OrderedDict', 'defaultdict', 'deque'}
|
|
504
|
+
),
|
|
505
|
+
'copyreg': frozenset({'_reconstructor'}),
|
|
506
|
+
'copy_reg': frozenset({'_reconstructor'}),
|
|
507
|
+
'_codecs': frozenset({'encode'}),
|
|
508
|
+
'datetime': frozenset(
|
|
509
|
+
{'date', 'datetime', 'time', 'timedelta', 'timezone'}
|
|
510
|
+
),
|
|
511
|
+
'decimal': frozenset({'Decimal'}),
|
|
512
|
+
'fractions': frozenset({'Fraction'}),
|
|
513
|
+
'uuid': frozenset({'UUID'}),
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# Python types that SafeDisk accepts for writing: exactly the types that the
|
|
517
|
+
# names above read back. Subclasses are rejected because pickle records the
|
|
518
|
+
# subclass, which the reader would refuse.
|
|
519
|
+
|
|
520
|
+
SAFE_TYPES = frozenset(
|
|
521
|
+
{
|
|
522
|
+
type(None),
|
|
523
|
+
bool,
|
|
524
|
+
int,
|
|
525
|
+
float,
|
|
526
|
+
complex,
|
|
527
|
+
str,
|
|
528
|
+
bytes,
|
|
529
|
+
bytearray,
|
|
530
|
+
list,
|
|
531
|
+
tuple,
|
|
532
|
+
dict,
|
|
533
|
+
set,
|
|
534
|
+
frozenset,
|
|
535
|
+
range,
|
|
536
|
+
slice,
|
|
537
|
+
co.Counter,
|
|
538
|
+
co.OrderedDict,
|
|
539
|
+
co.defaultdict,
|
|
540
|
+
co.deque,
|
|
541
|
+
dt.date,
|
|
542
|
+
dt.datetime,
|
|
543
|
+
dt.time,
|
|
544
|
+
dt.timedelta,
|
|
545
|
+
dt.timezone,
|
|
546
|
+
decimal.Decimal,
|
|
547
|
+
fractions.Fraction,
|
|
548
|
+
uuid.UUID,
|
|
549
|
+
}
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
_SAFE_MAPPINGS = (dict, co.Counter, co.OrderedDict, co.defaultdict)
|
|
553
|
+
_SAFE_SEQUENCES = (list, tuple, set, frozenset, co.deque)
|
|
554
|
+
_SAFE_TZ_TYPES = (type(None), dt.timezone)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class SafeUnpickler(pickle.Unpickler):
|
|
558
|
+
"""Unpickler that resolves only the names in SAFE_PICKLE_NAMES.
|
|
559
|
+
|
|
560
|
+
Every pickle opcode that can call code (GLOBAL, STACK_GLOBAL, INST, OBJ,
|
|
561
|
+
NEWOBJ, NEWOBJ_EX and REDUCE) obtains its callable through `find_class`,
|
|
562
|
+
so restricting `find_class` to a fixed set of constructors is enough to
|
|
563
|
+
keep a crafted pickle from running code.
|
|
564
|
+
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
def find_class(self, module, name):
|
|
568
|
+
if name in SAFE_PICKLE_NAMES.get(module, ()):
|
|
569
|
+
return super().find_class(module, name)
|
|
570
|
+
raise pickle.UnpicklingError(
|
|
571
|
+
'SafeDisk refuses to unpickle %s.%s' % (module, name)
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class SafeDisk(Disk):
|
|
576
|
+
"""Disk that reads back only an allowlist of standard-library types.
|
|
577
|
+
|
|
578
|
+
The default :class:`Disk` stores keys and values that SQLite cannot hold
|
|
579
|
+
natively as pickles and reads them back with :func:`pickle.load`, so
|
|
580
|
+
anyone who can write into the cache directory can run code in the process
|
|
581
|
+
that reads from it (CVE-2025-69872). :class:`SafeDisk` closes that path:
|
|
582
|
+
|
|
583
|
+
* Pickled keys and values are read with an unpickler that resolves only
|
|
584
|
+
the standard-library constructors in ``SAFE_PICKLE_NAMES`` and raises
|
|
585
|
+
:exc:`pickle.UnpicklingError` for anything else.
|
|
586
|
+
* Keys and values are checked before they are written, and a key or value
|
|
587
|
+
whose type the reader would refuse raises :exc:`TypeError` at write
|
|
588
|
+
time instead of failing later at read time.
|
|
589
|
+
* Paths read from the ``filename`` column are confined to the cache
|
|
590
|
+
directory, and a path that escapes it raises :exc:`ValueError`.
|
|
591
|
+
|
|
592
|
+
The on-disk format is the one :class:`Disk` writes, so a directory
|
|
593
|
+
written with :class:`SafeDisk` opens with :class:`Disk` and the reverse,
|
|
594
|
+
as long as the values are within the allowlist. Nothing about the choice
|
|
595
|
+
of disk is persisted; select it with ``Cache(directory, disk=SafeDisk)``,
|
|
596
|
+
``FanoutCache(directory, disk=SafeDisk)``, ``Deque(disk=SafeDisk)``,
|
|
597
|
+
``Index(disk=SafeDisk)`` or the ``DISK`` option of the Django backend.
|
|
598
|
+
|
|
599
|
+
Objects outside the allowlist, such as instances of your own classes,
|
|
600
|
+
Django model instances and responses, and the ``type`` objects that
|
|
601
|
+
``memoize(typed=True)`` puts into keys, need the default :class:`Disk`.
|
|
602
|
+
|
|
603
|
+
"""
|
|
604
|
+
|
|
605
|
+
def __init__(self, directory, min_file_size=0, pickle_protocol=0):
|
|
606
|
+
super().__init__(directory, min_file_size, pickle_protocol)
|
|
607
|
+
self._real_directory = op.realpath(directory)
|
|
608
|
+
|
|
609
|
+
def _check(self, obj, what):
|
|
610
|
+
"""Raise TypeError unless every object in `obj` is in SAFE_TYPES."""
|
|
611
|
+
seen = set()
|
|
612
|
+
stack = [obj]
|
|
613
|
+
|
|
614
|
+
while stack:
|
|
615
|
+
item = stack.pop()
|
|
616
|
+
type_item = type(item)
|
|
617
|
+
|
|
618
|
+
if type_item not in SAFE_TYPES:
|
|
619
|
+
raise TypeError(
|
|
620
|
+
'SafeDisk cannot store %s of type %s.%s'
|
|
621
|
+
% (what, type_item.__module__, type_item.__qualname__)
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
if type_item in _SAFE_MAPPINGS:
|
|
625
|
+
if id(item) in seen:
|
|
626
|
+
continue
|
|
627
|
+
seen.add(id(item))
|
|
628
|
+
if type_item is co.defaultdict:
|
|
629
|
+
factory = item.default_factory
|
|
630
|
+
if factory is not None and factory not in SAFE_TYPES:
|
|
631
|
+
raise TypeError(
|
|
632
|
+
'SafeDisk cannot store %s with defaultdict'
|
|
633
|
+
' factory %r' % (what, factory)
|
|
634
|
+
)
|
|
635
|
+
stack.extend(item.keys())
|
|
636
|
+
stack.extend(item.values())
|
|
637
|
+
elif type_item in _SAFE_SEQUENCES:
|
|
638
|
+
if id(item) in seen:
|
|
639
|
+
continue
|
|
640
|
+
seen.add(id(item))
|
|
641
|
+
stack.extend(item)
|
|
642
|
+
elif type_item in (dt.datetime, dt.time):
|
|
643
|
+
if type(item.tzinfo) not in _SAFE_TZ_TYPES:
|
|
644
|
+
raise TypeError(
|
|
645
|
+
'SafeDisk cannot store %s with tzinfo of type %s.%s'
|
|
646
|
+
% (
|
|
647
|
+
what,
|
|
648
|
+
type(item.tzinfo).__module__,
|
|
649
|
+
type(item.tzinfo).__qualname__,
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
def _confine(self, filename):
|
|
654
|
+
"""Raise ValueError unless `filename` stays in the cache directory."""
|
|
655
|
+
normalized = str(filename).replace('\\', '/')
|
|
656
|
+
|
|
657
|
+
if (
|
|
658
|
+
normalized.startswith('/')
|
|
659
|
+
or op.isabs(normalized)
|
|
660
|
+
or op.splitdrive(normalized)[0]
|
|
661
|
+
or '..' in normalized.split('/')
|
|
662
|
+
):
|
|
663
|
+
raise ValueError(
|
|
664
|
+
'SafeDisk refuses the path %r outside the cache directory'
|
|
665
|
+
% (filename,)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
directory = self._real_directory
|
|
669
|
+
full_path = op.realpath(op.join(directory, filename))
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
inside = op.commonpath((directory, full_path)) == directory
|
|
673
|
+
except ValueError: # Different drives on Windows.
|
|
674
|
+
inside = False
|
|
675
|
+
|
|
676
|
+
if not inside:
|
|
677
|
+
raise ValueError(
|
|
678
|
+
'SafeDisk refuses the path %r outside the cache directory'
|
|
679
|
+
% (filename,)
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
def put(self, key):
|
|
683
|
+
# pylint: disable=unidiomatic-typecheck
|
|
684
|
+
if type(key) not in (bytes, str, int, float):
|
|
685
|
+
self._check(key, 'key')
|
|
686
|
+
return super().put(key)
|
|
687
|
+
|
|
688
|
+
def get(self, key, raw):
|
|
689
|
+
if raw:
|
|
690
|
+
return super().get(key, raw)
|
|
691
|
+
return SafeUnpickler(io.BytesIO(key)).load()
|
|
692
|
+
|
|
693
|
+
def store(self, value, read, key=UNKNOWN):
|
|
694
|
+
# pylint: disable=unidiomatic-typecheck
|
|
695
|
+
if not read and type(value) not in (bytes, str, int, float):
|
|
696
|
+
self._check(value, 'value')
|
|
697
|
+
return super().store(value, read, key=key)
|
|
698
|
+
|
|
699
|
+
def fetch(self, mode, filename, value, read):
|
|
700
|
+
if filename is not None:
|
|
701
|
+
self._confine(filename)
|
|
702
|
+
|
|
703
|
+
if mode == MODE_PICKLE:
|
|
704
|
+
if value is None:
|
|
705
|
+
value = super().fetch(MODE_BINARY, filename, None, False)
|
|
706
|
+
return SafeUnpickler(io.BytesIO(value)).load()
|
|
707
|
+
|
|
708
|
+
return super().fetch(mode, filename, value, read)
|
|
709
|
+
|
|
710
|
+
def remove(self, file_path):
|
|
711
|
+
self._confine(file_path)
|
|
712
|
+
super().remove(file_path)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _meta_result(value, expire_time, tag, store_time, meta=(None, None, None)):
|
|
716
|
+
"""Return `value`, or a tuple of `value` and the requested metadata.
|
|
717
|
+
|
|
718
|
+
The tuple holds, after the value, the expire time when `expire_time` is
|
|
719
|
+
true, the tag when `tag` is true and the store time when `store_time` is
|
|
720
|
+
true, in that order. `meta` supplies the three database values; the
|
|
721
|
+
default is used when the key is missing.
|
|
722
|
+
|
|
723
|
+
"""
|
|
724
|
+
if not (expire_time or tag or store_time):
|
|
725
|
+
return value
|
|
726
|
+
db_expire_time, db_tag, db_store_time = meta
|
|
727
|
+
result = (value,)
|
|
728
|
+
if expire_time:
|
|
729
|
+
result += (db_expire_time,)
|
|
730
|
+
if tag:
|
|
731
|
+
result += (db_tag,)
|
|
732
|
+
if store_time:
|
|
733
|
+
result += (db_store_time,)
|
|
734
|
+
return result
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
class Timeout(Exception):
|
|
738
|
+
"""Database timeout expired."""
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class ReadOnlyError(sqlite3.OperationalError): # pylint: disable=R0903
|
|
742
|
+
"""Write attempted on a read-only cache.
|
|
743
|
+
|
|
744
|
+
Raised by every method that would modify a cache opened read-only, either
|
|
745
|
+
explicitly with ``Cache(directory, read_only=True)`` or because the cache
|
|
746
|
+
database or its directory is not writable. A subclass of
|
|
747
|
+
:exc:`sqlite3.OperationalError`, the error SQLite itself raises for
|
|
748
|
+
writes to a read-only database, so existing handlers keep working.
|
|
749
|
+
"""
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
# Pragmas that are stored in the database file rather than set per
|
|
753
|
+
# connection; a read-only cache leaves them as the file has them.
|
|
754
|
+
_PERSISTENT_PRAGMAS = frozenset({'auto_vacuum', 'journal_mode'})
|
|
755
|
+
|
|
756
|
+
# SQL condition selecting rows that have not expired, evaluated by SQLite
|
|
757
|
+
# itself so it can be embedded in a statement without a parameter. Used by
|
|
758
|
+
# read-only caches, which cannot delete expired rows while reading.
|
|
759
|
+
_LIVE_CONDITION = (
|
|
760
|
+
'(expire_time IS NULL'
|
|
761
|
+
" OR expire_time > (julianday('now') - 2440587.5) * 86400.0)"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
class UnknownFileWarning(UserWarning):
|
|
766
|
+
"""Warning used by Cache.check for unknown files."""
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
class EmptyDirWarning(UserWarning):
|
|
770
|
+
"""Warning used by Cache.check for empty directories."""
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def args_to_key(base, args, kwargs, typed, ignore):
|
|
774
|
+
"""Create cache key out of function arguments.
|
|
775
|
+
|
|
776
|
+
:param tuple base: base of key
|
|
777
|
+
:param tuple args: function arguments
|
|
778
|
+
:param dict kwargs: function keyword arguments
|
|
779
|
+
:param bool typed: include types in cache key
|
|
780
|
+
:param set ignore: positional or keyword args to ignore
|
|
781
|
+
:return: cache key tuple
|
|
782
|
+
|
|
783
|
+
"""
|
|
784
|
+
args = tuple(arg for index, arg in enumerate(args) if index not in ignore)
|
|
785
|
+
key = base + args + (None,)
|
|
786
|
+
|
|
787
|
+
if kwargs:
|
|
788
|
+
kwargs = {key: val for key, val in kwargs.items() if key not in ignore}
|
|
789
|
+
sorted_items = sorted(kwargs.items())
|
|
790
|
+
|
|
791
|
+
for item in sorted_items:
|
|
792
|
+
key += item
|
|
793
|
+
|
|
794
|
+
if typed:
|
|
795
|
+
key += tuple(type(arg) for arg in args)
|
|
796
|
+
|
|
797
|
+
if kwargs:
|
|
798
|
+
key += tuple(type(value) for _, value in sorted_items)
|
|
799
|
+
|
|
800
|
+
return key
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
_POSITIONAL_KINDS = (
|
|
804
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
805
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def resolve_ignore(func, ignore):
|
|
810
|
+
"""Expand `ignore` with the positions of the parameters it names.
|
|
811
|
+
|
|
812
|
+
`args_to_key` drops positional arguments by index and keyword arguments by
|
|
813
|
+
name, so a name in `ignore` used to match an argument only when the caller
|
|
814
|
+
passed it by keyword (upstream issue 240). Adding the position of each
|
|
815
|
+
positional parameter with that name makes the name match the argument
|
|
816
|
+
however it is passed. Keys of calls that do not pass an ignored parameter
|
|
817
|
+
positionally are unchanged.
|
|
818
|
+
|
|
819
|
+
:param func: callable being memoized
|
|
820
|
+
:param set ignore: positional or keyword args to ignore
|
|
821
|
+
:return: `ignore`, or a frozenset of `ignore` plus the resolved positions
|
|
822
|
+
|
|
823
|
+
"""
|
|
824
|
+
if not ignore:
|
|
825
|
+
return ignore
|
|
826
|
+
|
|
827
|
+
try:
|
|
828
|
+
parameters = inspect.signature(func).parameters.values()
|
|
829
|
+
except (TypeError, ValueError): # No signature, like some builtins.
|
|
830
|
+
return ignore
|
|
831
|
+
|
|
832
|
+
positions = set()
|
|
833
|
+
|
|
834
|
+
for index, parameter in enumerate(parameters):
|
|
835
|
+
if parameter.kind not in _POSITIONAL_KINDS:
|
|
836
|
+
break
|
|
837
|
+
if parameter.name in ignore:
|
|
838
|
+
positions.add(index)
|
|
839
|
+
|
|
840
|
+
if not positions:
|
|
841
|
+
return ignore
|
|
842
|
+
|
|
843
|
+
return frozenset(ignore) | positions
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
class _ThreadSentinel: # pylint: disable=too-few-public-methods
|
|
847
|
+
"""Marker kept in a Cache's thread-local state next to its connection.
|
|
848
|
+
|
|
849
|
+
Thread-local state is destroyed when its thread exits, so once a weak
|
|
850
|
+
reference to the sentinel is dead the connection's thread is gone and
|
|
851
|
+
the connection may be closed from another thread.
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
__slots__ = ('__weakref__',)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
class _Connections:
|
|
858
|
+
"""Registry of the SQLite connections one Cache opened in this process.
|
|
859
|
+
|
|
860
|
+
Connections are opened per thread (see ``Cache._con``). The registry
|
|
861
|
+
lets ``Cache.close`` and garbage collection close the connections of
|
|
862
|
+
threads that have already exited, and lets a forked child process
|
|
863
|
+
forget the connections it inherited without closing them.
|
|
864
|
+
"""
|
|
865
|
+
|
|
866
|
+
def __init__(self):
|
|
867
|
+
self._lock = threading.Lock()
|
|
868
|
+
self._items = {}
|
|
869
|
+
self.pid = os.getpid()
|
|
870
|
+
|
|
871
|
+
def add(self, ident, sentinel, con):
|
|
872
|
+
"""Register `con` for thread `ident`; return connections to close.
|
|
873
|
+
|
|
874
|
+
The returned connections belong to threads that have exited, or are
|
|
875
|
+
an earlier connection of the same thread whose thread-local state
|
|
876
|
+
was lost (a foreign thread that got a new interpreter thread state).
|
|
877
|
+
"""
|
|
878
|
+
with self._lock:
|
|
879
|
+
stale = self._pop_stale()
|
|
880
|
+
previous = self._items.pop(ident, None)
|
|
881
|
+
self._items[ident] = (weakref.ref(sentinel), con)
|
|
882
|
+
if previous is not None and previous[1] is not con:
|
|
883
|
+
stale.append(previous[1])
|
|
884
|
+
return stale
|
|
885
|
+
|
|
886
|
+
def pop(self, ident):
|
|
887
|
+
"""Remove the connection of thread `ident` and return it with the
|
|
888
|
+
connections of threads that have exited, all to be closed."""
|
|
889
|
+
with self._lock:
|
|
890
|
+
stale = self._pop_stale()
|
|
891
|
+
item = self._items.pop(ident, None)
|
|
892
|
+
if item is not None:
|
|
893
|
+
stale.append(item[1])
|
|
894
|
+
return stale
|
|
895
|
+
|
|
896
|
+
def _pop_stale(self):
|
|
897
|
+
# The caller holds the lock.
|
|
898
|
+
stale = []
|
|
899
|
+
for ident, (ref, con) in list(self._items.items()):
|
|
900
|
+
if ref() is None:
|
|
901
|
+
del self._items[ident]
|
|
902
|
+
stale.append(con)
|
|
903
|
+
return stale
|
|
904
|
+
|
|
905
|
+
def close_all(self):
|
|
906
|
+
"""Close every registered connection.
|
|
907
|
+
|
|
908
|
+
Runs when the Cache is garbage collected. In a child process the
|
|
909
|
+
connections were inherited and are discarded instead.
|
|
910
|
+
"""
|
|
911
|
+
if os.getpid() != self.pid:
|
|
912
|
+
self.discard()
|
|
913
|
+
return
|
|
914
|
+
with self._lock:
|
|
915
|
+
cons = [con for _, con in self._items.values()]
|
|
916
|
+
self._items.clear()
|
|
917
|
+
for con in cons:
|
|
918
|
+
con.close()
|
|
919
|
+
|
|
920
|
+
def discard(self):
|
|
921
|
+
"""Forget the connections after a fork without closing them.
|
|
922
|
+
|
|
923
|
+
SQLite forbids a child process from using, or even closing, a
|
|
924
|
+
connection the parent opened: the child inherits the parent's lock
|
|
925
|
+
state, and closing may roll back or unlock underneath the parent
|
|
926
|
+
and corrupt the database. The connections stay referenced for the
|
|
927
|
+
life of the child so garbage collection never closes them either;
|
|
928
|
+
their file descriptors leak on purpose, as SQLite recommends.
|
|
929
|
+
"""
|
|
930
|
+
_INHERITED_CONNECTIONS.extend(con for _, con in self._items.values())
|
|
931
|
+
self._items = {}
|
|
932
|
+
self._lock = threading.Lock() # The parent's lock may be held.
|
|
933
|
+
self.pid = os.getpid()
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
class _ForkGate:
|
|
937
|
+
"""Delay ``os.fork`` while another thread is inside a transaction.
|
|
938
|
+
|
|
939
|
+
A child process inherits the parent's SQLite state as of the fork. A
|
|
940
|
+
transaction in flight in another thread leaves the child's copy of that
|
|
941
|
+
state holding a write lock that nothing in the child ever releases, so
|
|
942
|
+
the child's own writes to that database would fail with ``Timeout``.
|
|
943
|
+
The gate holds new transactions back and makes fork wait, up to
|
|
944
|
+
`timeout` seconds, until the transactions in flight in other threads
|
|
945
|
+
finish; the held-back transactions proceed once the fork is done. The
|
|
946
|
+
forking thread's own transaction cannot be waited for and is not
|
|
947
|
+
counted; forking inside a transaction is unsupported.
|
|
948
|
+
"""
|
|
949
|
+
|
|
950
|
+
timeout = 60
|
|
951
|
+
|
|
952
|
+
def __init__(self):
|
|
953
|
+
self._cond = threading.Condition(threading.Lock())
|
|
954
|
+
self._active = 0
|
|
955
|
+
self._forking = False
|
|
956
|
+
self._local = threading.local()
|
|
957
|
+
|
|
958
|
+
def __enter__(self):
|
|
959
|
+
depth = getattr(self._local, 'depth', 0)
|
|
960
|
+
if not depth:
|
|
961
|
+
with self._cond:
|
|
962
|
+
while self._forking:
|
|
963
|
+
self._cond.wait()
|
|
964
|
+
self._active += 1
|
|
965
|
+
self._local.depth = depth + 1
|
|
966
|
+
|
|
967
|
+
def __exit__(self, *exception):
|
|
968
|
+
depth = getattr(self._local, 'depth', 0) - 1
|
|
969
|
+
if depth < 0:
|
|
970
|
+
return # A child process reset the gate during a transaction.
|
|
971
|
+
self._local.depth = depth
|
|
972
|
+
if not depth:
|
|
973
|
+
with self._cond:
|
|
974
|
+
self._active -= 1
|
|
975
|
+
if not self._active:
|
|
976
|
+
self._cond.notify_all()
|
|
977
|
+
|
|
978
|
+
def before_fork(self):
|
|
979
|
+
"""Hold new transactions back, then wait for the active ones."""
|
|
980
|
+
own = 1 if getattr(self._local, 'depth', 0) else 0
|
|
981
|
+
deadline = time.monotonic() + self.timeout
|
|
982
|
+
with self._cond:
|
|
983
|
+
while self._forking: # Another thread is forking.
|
|
984
|
+
self._cond.wait()
|
|
985
|
+
self._forking = True
|
|
986
|
+
while self._active > own:
|
|
987
|
+
remaining = deadline - time.monotonic()
|
|
988
|
+
if remaining <= 0:
|
|
989
|
+
warnings.warn(
|
|
990
|
+
'forking while a Cache transaction is in progress in'
|
|
991
|
+
' another thread; the child process may be unable'
|
|
992
|
+
' to write to that cache',
|
|
993
|
+
RuntimeWarning,
|
|
994
|
+
stacklevel=2,
|
|
995
|
+
)
|
|
996
|
+
break
|
|
997
|
+
self._cond.wait(remaining)
|
|
998
|
+
|
|
999
|
+
def after_fork_parent(self):
|
|
1000
|
+
"""Let transactions proceed again."""
|
|
1001
|
+
with self._cond:
|
|
1002
|
+
self._forking = False
|
|
1003
|
+
self._cond.notify_all()
|
|
1004
|
+
|
|
1005
|
+
def after_fork_child(self):
|
|
1006
|
+
"""Start over: the child has one thread and no transactions."""
|
|
1007
|
+
self._cond = threading.Condition(threading.Lock())
|
|
1008
|
+
self._active = 0
|
|
1009
|
+
self._forking = False
|
|
1010
|
+
self._local = threading.local()
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
_FORK_GATE = _ForkGate()
|
|
1014
|
+
|
|
1015
|
+
# Every live Cache, so a child process can forget inherited connections.
|
|
1016
|
+
_CACHES: 'weakref.WeakSet[Cache]' = weakref.WeakSet()
|
|
1017
|
+
|
|
1018
|
+
# Connections inherited from a parent process, kept open on purpose.
|
|
1019
|
+
_INHERITED_CONNECTIONS: list[sqlite3.Connection] = []
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def _before_fork():
|
|
1023
|
+
# SQLite advises closing every connection before fork. The gate waits
|
|
1024
|
+
# for other threads' transactions; then each cache closes the forking
|
|
1025
|
+
# thread's own connection and those of exited threads, which the
|
|
1026
|
+
# parent reopens on next use. Connections held by other live threads
|
|
1027
|
+
# cannot be closed here and are discarded in the child.
|
|
1028
|
+
_FORK_GATE.before_fork()
|
|
1029
|
+
for cache in list(_CACHES):
|
|
1030
|
+
cache._close_before_fork() # pylint: disable=protected-access
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _after_fork_in_child():
|
|
1034
|
+
_FORK_GATE.after_fork_child()
|
|
1035
|
+
for cache in list(_CACHES):
|
|
1036
|
+
cache._discard_connections() # pylint: disable=protected-access
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
if hasattr(os, 'register_at_fork'):
|
|
1040
|
+
os.register_at_fork(
|
|
1041
|
+
before=_before_fork,
|
|
1042
|
+
after_in_parent=_FORK_GATE.after_fork_parent,
|
|
1043
|
+
after_in_child=_after_fork_in_child,
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
class Cache:
|
|
1048
|
+
"""Disk and file backed cache."""
|
|
1049
|
+
|
|
1050
|
+
def __init__(
|
|
1051
|
+
self,
|
|
1052
|
+
directory=None,
|
|
1053
|
+
timeout=60,
|
|
1054
|
+
disk=Disk,
|
|
1055
|
+
*,
|
|
1056
|
+
read_only=False,
|
|
1057
|
+
**settings
|
|
1058
|
+
):
|
|
1059
|
+
"""Initialize cache instance.
|
|
1060
|
+
|
|
1061
|
+
:param str directory: cache directory
|
|
1062
|
+
:param float timeout: SQLite connection timeout
|
|
1063
|
+
:param disk: Disk type or subclass for serialization
|
|
1064
|
+
:param bool read_only: open the cache read-only (default False)
|
|
1065
|
+
:param settings: any of DEFAULT_SETTINGS
|
|
1066
|
+
|
|
1067
|
+
The cache directory, any missing parents and the subdirectories that
|
|
1068
|
+
hold large values are created with mode ``0o700``, so only the owner
|
|
1069
|
+
can read them. A directory that already exists keeps its mode; the
|
|
1070
|
+
cache never changes permissions. Windows ignores the mode bits.
|
|
1071
|
+
|
|
1072
|
+
Opening an existing cache writes nothing unless a setting differs
|
|
1073
|
+
from the stored one. A cache whose database file or directory is not
|
|
1074
|
+
writable opens read-only, as does a cache opened with `read_only`;
|
|
1075
|
+
reads work and every method that would write raises
|
|
1076
|
+
:exc:`ReadOnlyError`. The read-only state is not persisted and
|
|
1077
|
+
`read_only` is not a setting. See the :ref:`tutorial
|
|
1078
|
+
<tutorial-read-only>` for details.
|
|
1079
|
+
|
|
1080
|
+
"""
|
|
1081
|
+
try:
|
|
1082
|
+
assert issubclass(disk, Disk)
|
|
1083
|
+
except (TypeError, AssertionError):
|
|
1084
|
+
raise ValueError('disk must subclass diskcache.Disk') from None
|
|
1085
|
+
|
|
1086
|
+
if directory is None:
|
|
1087
|
+
directory = tempfile.mkdtemp(prefix='diskcache-')
|
|
1088
|
+
directory = str(directory)
|
|
1089
|
+
directory = op.expanduser(directory)
|
|
1090
|
+
directory = op.expandvars(directory)
|
|
1091
|
+
|
|
1092
|
+
self._directory = directory
|
|
1093
|
+
self._timeout = 0 # Manually handle retries during initialization.
|
|
1094
|
+
self._local = threading.local()
|
|
1095
|
+
self._cons = _Connections()
|
|
1096
|
+
self._txn_id = None
|
|
1097
|
+
_CACHES.add(self)
|
|
1098
|
+
finalizer = weakref.finalize(self, self._cons.close_all)
|
|
1099
|
+
finalizer.atexit = False
|
|
1100
|
+
|
|
1101
|
+
self._read_only = self._resolve_read_only(directory, read_only)
|
|
1102
|
+
|
|
1103
|
+
if not op.isdir(directory):
|
|
1104
|
+
try:
|
|
1105
|
+
_makedirs_private(op.abspath(directory))
|
|
1106
|
+
except OSError as error:
|
|
1107
|
+
if error.errno != errno.EEXIST:
|
|
1108
|
+
raise EnvironmentError(
|
|
1109
|
+
error.errno,
|
|
1110
|
+
'Cache directory "%s" does not exist'
|
|
1111
|
+
' and could not be created' % self._directory,
|
|
1112
|
+
) from None
|
|
1113
|
+
|
|
1114
|
+
sql = self._sql_retry
|
|
1115
|
+
|
|
1116
|
+
# Setup Settings table.
|
|
1117
|
+
|
|
1118
|
+
try:
|
|
1119
|
+
current_settings = dict(
|
|
1120
|
+
sql('SELECT key, value FROM Settings').fetchall()
|
|
1121
|
+
)
|
|
1122
|
+
except sqlite3.OperationalError:
|
|
1123
|
+
if self._read_only:
|
|
1124
|
+
raise ReadOnlyError(
|
|
1125
|
+
'read-only cache in %r has no Settings table' % directory
|
|
1126
|
+
) from None
|
|
1127
|
+
current_settings = {}
|
|
1128
|
+
|
|
1129
|
+
sets = DEFAULT_SETTINGS.copy()
|
|
1130
|
+
sets.update(current_settings)
|
|
1131
|
+
sets.update(settings)
|
|
1132
|
+
|
|
1133
|
+
for key in METADATA:
|
|
1134
|
+
sets.pop(key, None)
|
|
1135
|
+
|
|
1136
|
+
# Chance to set pragmas before any tables are created.
|
|
1137
|
+
|
|
1138
|
+
for key, value in sorted(sets.items()):
|
|
1139
|
+
if key.startswith('sqlite_'):
|
|
1140
|
+
self.reset(key, value, update=False)
|
|
1141
|
+
|
|
1142
|
+
if not self._read_only:
|
|
1143
|
+
sql(
|
|
1144
|
+
'CREATE TABLE IF NOT EXISTS Settings ('
|
|
1145
|
+
' key TEXT NOT NULL UNIQUE,'
|
|
1146
|
+
' value)'
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
# Setup Disk object (must happen after settings initialized).
|
|
1150
|
+
|
|
1151
|
+
kwargs = {
|
|
1152
|
+
key[5:]: value
|
|
1153
|
+
for key, value in sets.items()
|
|
1154
|
+
if key.startswith('disk_')
|
|
1155
|
+
}
|
|
1156
|
+
self._disk = disk(directory, **kwargs)
|
|
1157
|
+
|
|
1158
|
+
self._init_settings(sql, sets, current_settings)
|
|
1159
|
+
|
|
1160
|
+
((self._page_size,),) = sql('PRAGMA page_size').fetchall()
|
|
1161
|
+
|
|
1162
|
+
if self._read_only:
|
|
1163
|
+
# The schema, triggers and tag index are whatever the writer
|
|
1164
|
+
# left; reads need none of them to change.
|
|
1165
|
+
self.close()
|
|
1166
|
+
self._timeout = timeout
|
|
1167
|
+
self._sql # pylint: disable=pointless-statement
|
|
1168
|
+
return
|
|
1169
|
+
|
|
1170
|
+
self._init_schema(sql)
|
|
1171
|
+
|
|
1172
|
+
# Close and re-open database connection with given timeout.
|
|
1173
|
+
|
|
1174
|
+
self.close()
|
|
1175
|
+
self._timeout = timeout
|
|
1176
|
+
self._sql # pylint: disable=pointless-statement
|
|
1177
|
+
|
|
1178
|
+
@staticmethod
|
|
1179
|
+
def _resolve_read_only(directory, read_only):
|
|
1180
|
+
# A database that exists but cannot be written, or whose directory
|
|
1181
|
+
# cannot be written, is opened read-only. Opening read-only on
|
|
1182
|
+
# purpose needs an existing database, since nothing is created.
|
|
1183
|
+
db_path = op.join(directory, DBNAME)
|
|
1184
|
+
db_exists = op.isfile(db_path)
|
|
1185
|
+
|
|
1186
|
+
if read_only and not db_exists:
|
|
1187
|
+
raise ReadOnlyError(
|
|
1188
|
+
'no cache database to open read-only in %r' % directory
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
writable = os.access(directory, os.W_OK) and os.access(
|
|
1192
|
+
db_path, os.W_OK
|
|
1193
|
+
)
|
|
1194
|
+
return bool(read_only) or (db_exists and not writable)
|
|
1195
|
+
|
|
1196
|
+
def _init_settings(self, sql, sets, current_settings):
|
|
1197
|
+
# Set cached attributes: updates settings and sets pragmas. Only a
|
|
1198
|
+
# setting that is missing or differs from the stored value is
|
|
1199
|
+
# written, so opening an existing cache leaves the database alone.
|
|
1200
|
+
|
|
1201
|
+
for key, value in sets.items():
|
|
1202
|
+
if (
|
|
1203
|
+
not self._read_only
|
|
1204
|
+
and current_settings.get(key, ENOVAL) != value
|
|
1205
|
+
):
|
|
1206
|
+
query = 'INSERT OR REPLACE INTO Settings VALUES (?, ?)'
|
|
1207
|
+
sql(query, (key, value))
|
|
1208
|
+
self.reset(key, value, update=False)
|
|
1209
|
+
|
|
1210
|
+
for key, value in METADATA.items():
|
|
1211
|
+
if key not in current_settings and not self._read_only:
|
|
1212
|
+
query = 'INSERT OR IGNORE INTO Settings VALUES (?, ?)'
|
|
1213
|
+
sql(query, (key, value))
|
|
1214
|
+
self.reset(key)
|
|
1215
|
+
|
|
1216
|
+
def _init_schema(self, sql):
|
|
1217
|
+
# Create the Cache table, its indexes and the triggers when they are
|
|
1218
|
+
# missing. Every statement is a no-op on an existing database.
|
|
1219
|
+
|
|
1220
|
+
sql(
|
|
1221
|
+
'CREATE TABLE IF NOT EXISTS Cache ('
|
|
1222
|
+
' rowid INTEGER PRIMARY KEY,'
|
|
1223
|
+
' key BLOB,'
|
|
1224
|
+
' raw INTEGER,'
|
|
1225
|
+
' store_time REAL,'
|
|
1226
|
+
' expire_time REAL,'
|
|
1227
|
+
' access_time REAL,'
|
|
1228
|
+
' access_count INTEGER DEFAULT 0,'
|
|
1229
|
+
' tag BLOB,'
|
|
1230
|
+
' size INTEGER DEFAULT 0,'
|
|
1231
|
+
' mode INTEGER DEFAULT 0,'
|
|
1232
|
+
' filename TEXT,'
|
|
1233
|
+
' value BLOB)'
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
sql(
|
|
1237
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS Cache_key_raw ON'
|
|
1238
|
+
' Cache(key, raw)'
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
sql(
|
|
1242
|
+
'CREATE INDEX IF NOT EXISTS Cache_expire_time ON'
|
|
1243
|
+
' Cache (expire_time) WHERE expire_time IS NOT NULL'
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
query = EVICTION_POLICY[self.eviction_policy]['init']
|
|
1247
|
+
|
|
1248
|
+
if query is not None:
|
|
1249
|
+
sql(query)
|
|
1250
|
+
|
|
1251
|
+
# Use triggers to keep Metadata updated.
|
|
1252
|
+
|
|
1253
|
+
for name, definition in SETTINGS_TRIGGERS:
|
|
1254
|
+
sql('CREATE TRIGGER IF NOT EXISTS %s %s' % (name, definition))
|
|
1255
|
+
|
|
1256
|
+
self._update_triggers(sql)
|
|
1257
|
+
|
|
1258
|
+
# Create or drop the tag index only when the database disagrees
|
|
1259
|
+
# with the setting, so a reopen does not write.
|
|
1260
|
+
|
|
1261
|
+
((has_tag_index,),) = sql(
|
|
1262
|
+
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'index'"
|
|
1263
|
+
" AND name = 'Cache_tag_rowid'"
|
|
1264
|
+
).fetchall()
|
|
1265
|
+
|
|
1266
|
+
if self.tag_index and not has_tag_index: # pylint: disable=no-member
|
|
1267
|
+
self.create_tag_index()
|
|
1268
|
+
elif has_tag_index and not self.tag_index: # pylint: disable=no-member
|
|
1269
|
+
self.drop_tag_index()
|
|
1270
|
+
|
|
1271
|
+
@property
|
|
1272
|
+
def directory(self):
|
|
1273
|
+
"""Cache directory."""
|
|
1274
|
+
return self._directory
|
|
1275
|
+
|
|
1276
|
+
@property
|
|
1277
|
+
def timeout(self):
|
|
1278
|
+
"""SQLite connection timeout value in seconds."""
|
|
1279
|
+
return self._timeout
|
|
1280
|
+
|
|
1281
|
+
@property
|
|
1282
|
+
def disk(self):
|
|
1283
|
+
"""Disk used for serialization."""
|
|
1284
|
+
return self._disk
|
|
1285
|
+
|
|
1286
|
+
@property
|
|
1287
|
+
def read_only(self):
|
|
1288
|
+
"""True when the cache was opened read-only."""
|
|
1289
|
+
return self._read_only
|
|
1290
|
+
|
|
1291
|
+
def _check_writable(self):
|
|
1292
|
+
if self._read_only:
|
|
1293
|
+
raise ReadOnlyError('cache in %r is read-only' % self._directory)
|
|
1294
|
+
|
|
1295
|
+
@property
|
|
1296
|
+
def _con(self):
|
|
1297
|
+
# Check the process ID to support forking. If it changed, the
|
|
1298
|
+
# connections were inherited from the parent process: forget them
|
|
1299
|
+
# without closing them (see _Connections.discard) and open new ones.
|
|
1300
|
+
# The at-fork handler normally does this before the cache is used.
|
|
1301
|
+
|
|
1302
|
+
if self._cons.pid != os.getpid():
|
|
1303
|
+
self._discard_connections()
|
|
1304
|
+
|
|
1305
|
+
con = getattr(self._local, 'con', None)
|
|
1306
|
+
|
|
1307
|
+
if con is None:
|
|
1308
|
+
con = self._connect()
|
|
1309
|
+
|
|
1310
|
+
return con
|
|
1311
|
+
|
|
1312
|
+
def _connect(self):
|
|
1313
|
+
# check_same_thread=False lets close() and the connection registry
|
|
1314
|
+
# close a connection from a thread other than the one that opened
|
|
1315
|
+
# it, once that thread has exited. Each connection is still used by
|
|
1316
|
+
# one thread at a time, which is all SQLite's multi-thread mode asks.
|
|
1317
|
+
|
|
1318
|
+
path = op.join(self._directory, DBNAME)
|
|
1319
|
+
|
|
1320
|
+
if self._read_only:
|
|
1321
|
+
# mode=ro refuses writes at the SQLite level. A reader of a WAL
|
|
1322
|
+
# database needs to create the -shm file next to it, so when the
|
|
1323
|
+
# directory is not writable immutable=1 skips that file and all
|
|
1324
|
+
# locking, which is safe only because nothing can write there.
|
|
1325
|
+
path = pathlib.Path(op.abspath(path)).as_uri() + '?mode=ro'
|
|
1326
|
+
if not os.access(self._directory, os.W_OK):
|
|
1327
|
+
path += '&immutable=1'
|
|
1328
|
+
|
|
1329
|
+
con = sqlite3.connect(
|
|
1330
|
+
path,
|
|
1331
|
+
timeout=self._timeout,
|
|
1332
|
+
isolation_level=None,
|
|
1333
|
+
check_same_thread=False,
|
|
1334
|
+
uri=self._read_only,
|
|
1335
|
+
)
|
|
1336
|
+
sentinel = _ThreadSentinel()
|
|
1337
|
+
stale = self._cons.add(threading.get_ident(), sentinel, con)
|
|
1338
|
+
self._local.con = con
|
|
1339
|
+
self._local.sentinel = sentinel
|
|
1340
|
+
|
|
1341
|
+
for old in stale:
|
|
1342
|
+
old.close()
|
|
1343
|
+
|
|
1344
|
+
# Some SQLite pragmas work on a per-connection basis so
|
|
1345
|
+
# query the Settings table and reset the pragmas. The
|
|
1346
|
+
# Settings table may not exist so catch and ignore the
|
|
1347
|
+
# OperationalError that may occur.
|
|
1348
|
+
|
|
1349
|
+
try:
|
|
1350
|
+
select = 'SELECT key, value FROM Settings'
|
|
1351
|
+
settings = con.execute(select).fetchall()
|
|
1352
|
+
except sqlite3.OperationalError:
|
|
1353
|
+
pass
|
|
1354
|
+
else:
|
|
1355
|
+
for key, value in settings:
|
|
1356
|
+
if key.startswith('sqlite_'):
|
|
1357
|
+
self.reset(key, value, update=False)
|
|
1358
|
+
|
|
1359
|
+
return con
|
|
1360
|
+
|
|
1361
|
+
def _close_before_fork(self):
|
|
1362
|
+
# Called in the parent right before fork. Inside a transaction the
|
|
1363
|
+
# connection cannot be closed; forking there is unsupported.
|
|
1364
|
+
if self._txn_id != threading.get_ident():
|
|
1365
|
+
self.close()
|
|
1366
|
+
|
|
1367
|
+
def _discard_connections(self):
|
|
1368
|
+
# In a child process: forget the connections inherited from the
|
|
1369
|
+
# parent without closing them, and start with fresh per-thread state.
|
|
1370
|
+
self._cons.discard()
|
|
1371
|
+
self._local = threading.local()
|
|
1372
|
+
self._txn_id = None
|
|
1373
|
+
|
|
1374
|
+
@property
|
|
1375
|
+
def _sql(self):
|
|
1376
|
+
return self._con.execute
|
|
1377
|
+
|
|
1378
|
+
@property
|
|
1379
|
+
def _sql_retry(self):
|
|
1380
|
+
sql = self._sql
|
|
1381
|
+
|
|
1382
|
+
# 2018-11-01 GrantJ - Some SQLite builds/versions handle
|
|
1383
|
+
# the SQLITE_BUSY return value and connection parameter
|
|
1384
|
+
# "timeout" differently. For a more reliable duration,
|
|
1385
|
+
# manually retry the statement for 60 seconds. Only used
|
|
1386
|
+
# by statements which modify the database and do not use
|
|
1387
|
+
# a transaction (like those in ``__init__`` or ``reset``).
|
|
1388
|
+
# See Issue #85 for and tests/issue_85.py for more details.
|
|
1389
|
+
|
|
1390
|
+
def _execute_with_retry(statement, *args, **kwargs):
|
|
1391
|
+
start = time.time()
|
|
1392
|
+
while True:
|
|
1393
|
+
try:
|
|
1394
|
+
return sql(statement, *args, **kwargs)
|
|
1395
|
+
except sqlite3.OperationalError as exc:
|
|
1396
|
+
if str(exc) != 'database is locked':
|
|
1397
|
+
raise
|
|
1398
|
+
diff = time.time() - start
|
|
1399
|
+
if diff > 60:
|
|
1400
|
+
raise
|
|
1401
|
+
time.sleep(0.001)
|
|
1402
|
+
|
|
1403
|
+
return _execute_with_retry
|
|
1404
|
+
|
|
1405
|
+
def _triggers_stale(self, sql):
|
|
1406
|
+
# Compare the trigger text stored in sqlite_master with the current
|
|
1407
|
+
# definitions. SQLite stores the statement without IF NOT EXISTS,
|
|
1408
|
+
# with the leading keywords in upper case and the whitespace after
|
|
1409
|
+
# them collapsed, so compare whitespace-normalized text.
|
|
1410
|
+
|
|
1411
|
+
names = tuple(name for name, _ in SETTINGS_TRIGGERS)
|
|
1412
|
+
select = (
|
|
1413
|
+
'SELECT name, sql FROM sqlite_master'
|
|
1414
|
+
" WHERE type = 'trigger' AND name IN (%s)"
|
|
1415
|
+
% ', '.join('?' * len(names))
|
|
1416
|
+
)
|
|
1417
|
+
stored = dict(sql(select, names).fetchall())
|
|
1418
|
+
|
|
1419
|
+
for name, definition in SETTINGS_TRIGGERS:
|
|
1420
|
+
expected = 'CREATE TRIGGER %s %s' % (name, definition)
|
|
1421
|
+
current = ' '.join(stored.get(name, '').split())
|
|
1422
|
+
if current != ' '.join(expected.split()):
|
|
1423
|
+
return True
|
|
1424
|
+
|
|
1425
|
+
return False
|
|
1426
|
+
|
|
1427
|
+
def _update_triggers(self, sql):
|
|
1428
|
+
# Recreate the Settings triggers only when their stored text differs
|
|
1429
|
+
# from SETTINGS_TRIGGERS, as it does in caches written by versions
|
|
1430
|
+
# before 5.7.0 (double-quoted literals, PR #311 and issue #343).
|
|
1431
|
+
# The comparison keeps DDL off the common path: dropping or creating
|
|
1432
|
+
# a trigger bumps the schema cookie, which makes every other
|
|
1433
|
+
# connection re-prepare its statements, and it needs an exclusive
|
|
1434
|
+
# lock on the database. All five triggers are replaced inside one
|
|
1435
|
+
# IMMEDIATE transaction so no writer can observe a missing trigger.
|
|
1436
|
+
|
|
1437
|
+
if not self._triggers_stale(sql):
|
|
1438
|
+
return
|
|
1439
|
+
|
|
1440
|
+
sql('BEGIN IMMEDIATE')
|
|
1441
|
+
|
|
1442
|
+
try:
|
|
1443
|
+
if self._triggers_stale(sql): # Another opener may have won.
|
|
1444
|
+
for name, definition in SETTINGS_TRIGGERS:
|
|
1445
|
+
sql('DROP TRIGGER IF EXISTS %s' % name)
|
|
1446
|
+
sql('CREATE TRIGGER %s %s' % (name, definition))
|
|
1447
|
+
sql('COMMIT')
|
|
1448
|
+
except BaseException:
|
|
1449
|
+
with cl.suppress(sqlite3.OperationalError):
|
|
1450
|
+
sql('ROLLBACK')
|
|
1451
|
+
raise
|
|
1452
|
+
|
|
1453
|
+
@cl.contextmanager
|
|
1454
|
+
def transact(self, retry=False):
|
|
1455
|
+
"""Context manager to perform a transaction by locking the cache.
|
|
1456
|
+
|
|
1457
|
+
While the cache is locked, no other write operation is permitted.
|
|
1458
|
+
Transactions should therefore be as short as possible. Read and write
|
|
1459
|
+
operations performed in a transaction are atomic. Read operations may
|
|
1460
|
+
occur concurrent to a transaction.
|
|
1461
|
+
|
|
1462
|
+
Transactions may be nested and may not be shared between threads.
|
|
1463
|
+
|
|
1464
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
1465
|
+
`False` (default).
|
|
1466
|
+
|
|
1467
|
+
>>> cache = Cache()
|
|
1468
|
+
>>> with cache.transact(): # Atomically increment two keys.
|
|
1469
|
+
... _ = cache.incr('total', 123.4)
|
|
1470
|
+
... _ = cache.incr('count', 1)
|
|
1471
|
+
>>> with cache.transact(): # Atomically calculate average.
|
|
1472
|
+
... average = cache['total'] / cache['count']
|
|
1473
|
+
>>> average
|
|
1474
|
+
123.4
|
|
1475
|
+
|
|
1476
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
1477
|
+
:return: context manager for use in `with` statement
|
|
1478
|
+
:raises Timeout: if database timeout occurs
|
|
1479
|
+
|
|
1480
|
+
"""
|
|
1481
|
+
with self._transact(retry=retry):
|
|
1482
|
+
yield
|
|
1483
|
+
|
|
1484
|
+
@cl.contextmanager
|
|
1485
|
+
def _transact(self, retry=False, filename=None):
|
|
1486
|
+
with _FORK_GATE:
|
|
1487
|
+
sql = self._sql
|
|
1488
|
+
filenames = []
|
|
1489
|
+
_disk_remove = self._disk.remove
|
|
1490
|
+
tid = threading.get_ident()
|
|
1491
|
+
txn_id = self._txn_id
|
|
1492
|
+
|
|
1493
|
+
if tid == txn_id:
|
|
1494
|
+
begin = False
|
|
1495
|
+
else:
|
|
1496
|
+
# A read-only cache cannot take the reserved lock that
|
|
1497
|
+
# BEGIN IMMEDIATE asks for; its transactions only read.
|
|
1498
|
+
begin_statement = (
|
|
1499
|
+
'BEGIN' if self._read_only else 'BEGIN IMMEDIATE'
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
while True:
|
|
1503
|
+
try:
|
|
1504
|
+
sql(begin_statement)
|
|
1505
|
+
begin = True
|
|
1506
|
+
self._txn_id = tid
|
|
1507
|
+
break
|
|
1508
|
+
except sqlite3.OperationalError:
|
|
1509
|
+
if retry:
|
|
1510
|
+
continue
|
|
1511
|
+
if filename is not None:
|
|
1512
|
+
_disk_remove(filename)
|
|
1513
|
+
raise Timeout from None
|
|
1514
|
+
|
|
1515
|
+
try:
|
|
1516
|
+
yield sql, filenames.append
|
|
1517
|
+
except BaseException as error:
|
|
1518
|
+
if begin:
|
|
1519
|
+
assert self._txn_id == tid
|
|
1520
|
+
self._txn_id = None
|
|
1521
|
+
sql('ROLLBACK')
|
|
1522
|
+
if (
|
|
1523
|
+
self._read_only
|
|
1524
|
+
and isinstance(error, sqlite3.OperationalError)
|
|
1525
|
+
and 'readonly' in str(error)
|
|
1526
|
+
):
|
|
1527
|
+
raise ReadOnlyError(
|
|
1528
|
+
'cache in %r is read-only' % self._directory
|
|
1529
|
+
) from error
|
|
1530
|
+
raise
|
|
1531
|
+
else:
|
|
1532
|
+
if begin:
|
|
1533
|
+
assert self._txn_id == tid
|
|
1534
|
+
self._txn_id = None
|
|
1535
|
+
sql('COMMIT')
|
|
1536
|
+
for name in filenames:
|
|
1537
|
+
if name is not None:
|
|
1538
|
+
_disk_remove(name)
|
|
1539
|
+
|
|
1540
|
+
def set(self, key, value, expire=None, read=False, tag=None, retry=False):
|
|
1541
|
+
"""Set `key` and `value` item in cache.
|
|
1542
|
+
|
|
1543
|
+
When `read` is `True`, `value` should be a file-like object opened
|
|
1544
|
+
for reading in binary mode.
|
|
1545
|
+
|
|
1546
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
1547
|
+
`False` (default).
|
|
1548
|
+
|
|
1549
|
+
:param key: key for item
|
|
1550
|
+
:param value: value for item
|
|
1551
|
+
:param float expire: seconds until item expires
|
|
1552
|
+
(default None, no expiry)
|
|
1553
|
+
:param bool read: read value as bytes from file (default False)
|
|
1554
|
+
:param str tag: text to associate with key (default None)
|
|
1555
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
1556
|
+
:return: True if item was set
|
|
1557
|
+
:raises Timeout: if database timeout occurs
|
|
1558
|
+
|
|
1559
|
+
"""
|
|
1560
|
+
self._check_writable()
|
|
1561
|
+
now = time.time()
|
|
1562
|
+
db_key, raw = self._disk.put(key)
|
|
1563
|
+
expire_time = None if expire is None else now + expire
|
|
1564
|
+
size, mode, filename, db_value = self._disk.store(value, read, key=key)
|
|
1565
|
+
columns = (expire_time, tag, size, mode, filename, db_value)
|
|
1566
|
+
|
|
1567
|
+
# The order of SELECT, UPDATE, and INSERT is important below.
|
|
1568
|
+
#
|
|
1569
|
+
# Typical cache usage pattern is:
|
|
1570
|
+
#
|
|
1571
|
+
# value = cache.get(key)
|
|
1572
|
+
# if value is None:
|
|
1573
|
+
# value = expensive_calculation()
|
|
1574
|
+
# cache.set(key, value)
|
|
1575
|
+
#
|
|
1576
|
+
# Cache.get does not evict expired keys to avoid writes during lookups.
|
|
1577
|
+
# Commonly used/expired keys will therefore remain in the cache making
|
|
1578
|
+
# an UPDATE the preferred path.
|
|
1579
|
+
#
|
|
1580
|
+
# The alternative is to assume the key is not present by first trying
|
|
1581
|
+
# to INSERT and then handling the IntegrityError that occurs from
|
|
1582
|
+
# violating the UNIQUE constraint. This optimistic approach was
|
|
1583
|
+
# rejected based on the common cache usage pattern.
|
|
1584
|
+
#
|
|
1585
|
+
# INSERT OR REPLACE aka UPSERT is not used because the old filename may
|
|
1586
|
+
# need cleanup.
|
|
1587
|
+
|
|
1588
|
+
with self._transact(retry, filename) as (sql, cleanup):
|
|
1589
|
+
rows = sql(
|
|
1590
|
+
'SELECT rowid, filename FROM Cache'
|
|
1591
|
+
' WHERE key = ? AND raw = ?',
|
|
1592
|
+
(db_key, raw),
|
|
1593
|
+
).fetchall()
|
|
1594
|
+
|
|
1595
|
+
if rows:
|
|
1596
|
+
((rowid, old_filename),) = rows
|
|
1597
|
+
cleanup(old_filename)
|
|
1598
|
+
self._row_update(rowid, now, columns)
|
|
1599
|
+
else:
|
|
1600
|
+
self._row_insert(db_key, raw, now, columns)
|
|
1601
|
+
|
|
1602
|
+
self._cull(now, sql, cleanup)
|
|
1603
|
+
|
|
1604
|
+
return True
|
|
1605
|
+
|
|
1606
|
+
def __setitem__(self, key, value):
|
|
1607
|
+
"""Set corresponding `value` for `key` in cache.
|
|
1608
|
+
|
|
1609
|
+
:param key: key for item
|
|
1610
|
+
:param value: value for item
|
|
1611
|
+
:return: corresponding value
|
|
1612
|
+
:raises KeyError: if key is not found
|
|
1613
|
+
|
|
1614
|
+
"""
|
|
1615
|
+
self.set(key, value, retry=True)
|
|
1616
|
+
|
|
1617
|
+
def _row_update(self, rowid, now, columns):
|
|
1618
|
+
sql = self._sql
|
|
1619
|
+
expire_time, tag, size, mode, filename, value = columns
|
|
1620
|
+
sql(
|
|
1621
|
+
'UPDATE Cache SET'
|
|
1622
|
+
' store_time = ?,'
|
|
1623
|
+
' expire_time = ?,'
|
|
1624
|
+
' access_time = ?,'
|
|
1625
|
+
' access_count = ?,'
|
|
1626
|
+
' tag = ?,'
|
|
1627
|
+
' size = ?,'
|
|
1628
|
+
' mode = ?,'
|
|
1629
|
+
' filename = ?,'
|
|
1630
|
+
' value = ?'
|
|
1631
|
+
' WHERE rowid = ?',
|
|
1632
|
+
(
|
|
1633
|
+
now, # store_time
|
|
1634
|
+
expire_time,
|
|
1635
|
+
now, # access_time
|
|
1636
|
+
0, # access_count
|
|
1637
|
+
tag,
|
|
1638
|
+
size,
|
|
1639
|
+
mode,
|
|
1640
|
+
filename,
|
|
1641
|
+
value,
|
|
1642
|
+
rowid,
|
|
1643
|
+
),
|
|
1644
|
+
)
|
|
1645
|
+
|
|
1646
|
+
def _row_insert(self, key, raw, now, columns):
|
|
1647
|
+
sql = self._sql
|
|
1648
|
+
expire_time, tag, size, mode, filename, value = columns
|
|
1649
|
+
sql(
|
|
1650
|
+
'INSERT INTO Cache('
|
|
1651
|
+
' key, raw, store_time, expire_time, access_time,'
|
|
1652
|
+
' access_count, tag, size, mode, filename, value'
|
|
1653
|
+
') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
1654
|
+
(
|
|
1655
|
+
key,
|
|
1656
|
+
raw,
|
|
1657
|
+
now, # store_time
|
|
1658
|
+
expire_time,
|
|
1659
|
+
now, # access_time
|
|
1660
|
+
0, # access_count
|
|
1661
|
+
tag,
|
|
1662
|
+
size,
|
|
1663
|
+
mode,
|
|
1664
|
+
filename,
|
|
1665
|
+
value,
|
|
1666
|
+
),
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
def _cull(self, now, sql, cleanup, limit=None):
|
|
1670
|
+
cull_limit = self.cull_limit if limit is None else limit
|
|
1671
|
+
|
|
1672
|
+
if cull_limit == 0:
|
|
1673
|
+
return
|
|
1674
|
+
|
|
1675
|
+
# Evict expired keys.
|
|
1676
|
+
|
|
1677
|
+
select_expired_template = (
|
|
1678
|
+
'SELECT %s FROM Cache'
|
|
1679
|
+
' WHERE expire_time IS NOT NULL AND expire_time < ?'
|
|
1680
|
+
' ORDER BY expire_time LIMIT ?'
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
select_expired = select_expired_template % 'filename'
|
|
1684
|
+
rows = sql(select_expired, (now, cull_limit)).fetchall()
|
|
1685
|
+
|
|
1686
|
+
if rows:
|
|
1687
|
+
delete_expired = 'DELETE FROM Cache WHERE rowid IN (%s)' % (
|
|
1688
|
+
select_expired_template % 'rowid'
|
|
1689
|
+
)
|
|
1690
|
+
sql(delete_expired, (now, cull_limit))
|
|
1691
|
+
|
|
1692
|
+
for (filename,) in rows:
|
|
1693
|
+
cleanup(filename)
|
|
1694
|
+
|
|
1695
|
+
cull_limit -= len(rows)
|
|
1696
|
+
|
|
1697
|
+
if cull_limit == 0:
|
|
1698
|
+
return
|
|
1699
|
+
|
|
1700
|
+
# Evict keys by policy.
|
|
1701
|
+
|
|
1702
|
+
select_policy = EVICTION_POLICY[self.eviction_policy]['cull']
|
|
1703
|
+
|
|
1704
|
+
if select_policy is None or self.volume() < self.size_limit:
|
|
1705
|
+
return
|
|
1706
|
+
|
|
1707
|
+
select_filename = select_policy.format(fields='filename', now=now)
|
|
1708
|
+
rows = sql(select_filename, (cull_limit,)).fetchall()
|
|
1709
|
+
|
|
1710
|
+
if rows:
|
|
1711
|
+
delete = 'DELETE FROM Cache WHERE rowid IN (%s)' % (
|
|
1712
|
+
select_policy.format(fields='rowid', now=now)
|
|
1713
|
+
)
|
|
1714
|
+
sql(delete, (cull_limit,))
|
|
1715
|
+
|
|
1716
|
+
for (filename,) in rows:
|
|
1717
|
+
cleanup(filename)
|
|
1718
|
+
|
|
1719
|
+
def touch(self, key, expire=None, retry=False):
|
|
1720
|
+
"""Touch `key` in cache and update `expire` time.
|
|
1721
|
+
|
|
1722
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
1723
|
+
`False` (default).
|
|
1724
|
+
|
|
1725
|
+
:param key: key for item
|
|
1726
|
+
:param float expire: seconds until item expires
|
|
1727
|
+
(default None, no expiry)
|
|
1728
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
1729
|
+
:return: True if key was touched
|
|
1730
|
+
:raises Timeout: if database timeout occurs
|
|
1731
|
+
|
|
1732
|
+
"""
|
|
1733
|
+
self._check_writable()
|
|
1734
|
+
now = time.time()
|
|
1735
|
+
db_key, raw = self._disk.put(key)
|
|
1736
|
+
expire_time = None if expire is None else now + expire
|
|
1737
|
+
|
|
1738
|
+
with self._transact(retry) as (sql, _):
|
|
1739
|
+
rows = sql(
|
|
1740
|
+
'SELECT rowid, expire_time FROM Cache'
|
|
1741
|
+
' WHERE key = ? AND raw = ?',
|
|
1742
|
+
(db_key, raw),
|
|
1743
|
+
).fetchall()
|
|
1744
|
+
|
|
1745
|
+
if rows:
|
|
1746
|
+
((rowid, old_expire_time),) = rows
|
|
1747
|
+
|
|
1748
|
+
if old_expire_time is None or old_expire_time > now:
|
|
1749
|
+
sql(
|
|
1750
|
+
'UPDATE Cache SET expire_time = ? WHERE rowid = ?',
|
|
1751
|
+
(expire_time, rowid),
|
|
1752
|
+
)
|
|
1753
|
+
return True
|
|
1754
|
+
|
|
1755
|
+
return False
|
|
1756
|
+
|
|
1757
|
+
def add(self, key, value, expire=None, read=False, tag=None, retry=False):
|
|
1758
|
+
"""Add `key` and `value` item to cache.
|
|
1759
|
+
|
|
1760
|
+
Similar to `set`, but only add to cache if key not present.
|
|
1761
|
+
|
|
1762
|
+
Operation is atomic. Only one concurrent add operation for a given key
|
|
1763
|
+
will succeed.
|
|
1764
|
+
|
|
1765
|
+
When `read` is `True`, `value` should be a file-like object opened
|
|
1766
|
+
for reading in binary mode.
|
|
1767
|
+
|
|
1768
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
1769
|
+
`False` (default).
|
|
1770
|
+
|
|
1771
|
+
:param key: key for item
|
|
1772
|
+
:param value: value for item
|
|
1773
|
+
:param float expire: seconds until the key expires
|
|
1774
|
+
(default None, no expiry)
|
|
1775
|
+
:param bool read: read value as bytes from file (default False)
|
|
1776
|
+
:param str tag: text to associate with key (default None)
|
|
1777
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
1778
|
+
:return: True if item was added
|
|
1779
|
+
:raises Timeout: if database timeout occurs
|
|
1780
|
+
|
|
1781
|
+
"""
|
|
1782
|
+
self._check_writable()
|
|
1783
|
+
now = time.time()
|
|
1784
|
+
db_key, raw = self._disk.put(key)
|
|
1785
|
+
expire_time = None if expire is None else now + expire
|
|
1786
|
+
size, mode, filename, db_value = self._disk.store(value, read, key=key)
|
|
1787
|
+
columns = (expire_time, tag, size, mode, filename, db_value)
|
|
1788
|
+
|
|
1789
|
+
with self._transact(retry, filename) as (sql, cleanup):
|
|
1790
|
+
rows = sql(
|
|
1791
|
+
'SELECT rowid, filename, expire_time FROM Cache'
|
|
1792
|
+
' WHERE key = ? AND raw = ?',
|
|
1793
|
+
(db_key, raw),
|
|
1794
|
+
).fetchall()
|
|
1795
|
+
|
|
1796
|
+
if rows:
|
|
1797
|
+
((rowid, old_filename, old_expire_time),) = rows
|
|
1798
|
+
|
|
1799
|
+
if old_expire_time is None or old_expire_time > now:
|
|
1800
|
+
cleanup(filename)
|
|
1801
|
+
return False
|
|
1802
|
+
|
|
1803
|
+
cleanup(old_filename)
|
|
1804
|
+
self._row_update(rowid, now, columns)
|
|
1805
|
+
else:
|
|
1806
|
+
self._row_insert(db_key, raw, now, columns)
|
|
1807
|
+
|
|
1808
|
+
self._cull(now, sql, cleanup)
|
|
1809
|
+
|
|
1810
|
+
return True
|
|
1811
|
+
|
|
1812
|
+
def incr(self, key, delta=1, default=0, retry=False):
|
|
1813
|
+
"""Increment value by delta for item with key.
|
|
1814
|
+
|
|
1815
|
+
If key is missing and default is None then raise KeyError. Else if key
|
|
1816
|
+
is missing and default is not None then use default for value.
|
|
1817
|
+
|
|
1818
|
+
Operation is atomic. All concurrent increment operations will be
|
|
1819
|
+
counted individually.
|
|
1820
|
+
|
|
1821
|
+
Assumes value may be stored in a SQLite column. Most builds that target
|
|
1822
|
+
machines with 64-bit pointer widths will support 64-bit signed
|
|
1823
|
+
integers.
|
|
1824
|
+
|
|
1825
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
1826
|
+
`False` (default).
|
|
1827
|
+
|
|
1828
|
+
:param key: key for item
|
|
1829
|
+
:param int delta: amount to increment (default 1)
|
|
1830
|
+
:param int default: value if key is missing (default 0)
|
|
1831
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
1832
|
+
:return: new value for item
|
|
1833
|
+
:raises KeyError: if key is not found and default is None
|
|
1834
|
+
:raises Timeout: if database timeout occurs
|
|
1835
|
+
|
|
1836
|
+
"""
|
|
1837
|
+
self._check_writable()
|
|
1838
|
+
now = time.time()
|
|
1839
|
+
db_key, raw = self._disk.put(key)
|
|
1840
|
+
select = (
|
|
1841
|
+
'SELECT rowid, expire_time, filename, value FROM Cache'
|
|
1842
|
+
' WHERE key = ? AND raw = ?'
|
|
1843
|
+
)
|
|
1844
|
+
|
|
1845
|
+
with self._transact(retry) as (sql, cleanup):
|
|
1846
|
+
rows = sql(select, (db_key, raw)).fetchall()
|
|
1847
|
+
|
|
1848
|
+
if not rows:
|
|
1849
|
+
if default is None:
|
|
1850
|
+
raise KeyError(key)
|
|
1851
|
+
|
|
1852
|
+
value = default + delta
|
|
1853
|
+
columns = (None, None) + self._disk.store(
|
|
1854
|
+
value, False, key=key
|
|
1855
|
+
)
|
|
1856
|
+
self._row_insert(db_key, raw, now, columns)
|
|
1857
|
+
self._cull(now, sql, cleanup)
|
|
1858
|
+
return value
|
|
1859
|
+
|
|
1860
|
+
((rowid, expire_time, filename, value),) = rows
|
|
1861
|
+
|
|
1862
|
+
if expire_time is not None and expire_time < now:
|
|
1863
|
+
if default is None:
|
|
1864
|
+
raise KeyError(key)
|
|
1865
|
+
|
|
1866
|
+
value = default + delta
|
|
1867
|
+
columns = (None, None) + self._disk.store(
|
|
1868
|
+
value, False, key=key
|
|
1869
|
+
)
|
|
1870
|
+
self._row_update(rowid, now, columns)
|
|
1871
|
+
self._cull(now, sql, cleanup)
|
|
1872
|
+
cleanup(filename)
|
|
1873
|
+
return value
|
|
1874
|
+
|
|
1875
|
+
value += delta
|
|
1876
|
+
|
|
1877
|
+
columns = 'store_time = ?, value = ?'
|
|
1878
|
+
update_column = EVICTION_POLICY[self.eviction_policy]['get']
|
|
1879
|
+
|
|
1880
|
+
if update_column is not None:
|
|
1881
|
+
columns += ', ' + update_column.format(now=now)
|
|
1882
|
+
|
|
1883
|
+
update = 'UPDATE Cache SET %s WHERE rowid = ?' % columns
|
|
1884
|
+
sql(update, (now, value, rowid))
|
|
1885
|
+
|
|
1886
|
+
return value
|
|
1887
|
+
|
|
1888
|
+
def decr(self, key, delta=1, default=0, retry=False):
|
|
1889
|
+
"""Decrement value by delta for item with key.
|
|
1890
|
+
|
|
1891
|
+
If key is missing and default is None then raise KeyError. Else if key
|
|
1892
|
+
is missing and default is not None then use default for value.
|
|
1893
|
+
|
|
1894
|
+
Operation is atomic. All concurrent decrement operations will be
|
|
1895
|
+
counted individually.
|
|
1896
|
+
|
|
1897
|
+
Unlike Memcached, negative values are supported. Value may be
|
|
1898
|
+
decremented below zero.
|
|
1899
|
+
|
|
1900
|
+
Assumes value may be stored in a SQLite column. Most builds that target
|
|
1901
|
+
machines with 64-bit pointer widths will support 64-bit signed
|
|
1902
|
+
integers.
|
|
1903
|
+
|
|
1904
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
1905
|
+
`False` (default).
|
|
1906
|
+
|
|
1907
|
+
:param key: key for item
|
|
1908
|
+
:param int delta: amount to decrement (default 1)
|
|
1909
|
+
:param int default: value if key is missing (default 0)
|
|
1910
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
1911
|
+
:return: new value for item
|
|
1912
|
+
:raises KeyError: if key is not found and default is None
|
|
1913
|
+
:raises Timeout: if database timeout occurs
|
|
1914
|
+
|
|
1915
|
+
"""
|
|
1916
|
+
return self.incr(key, -delta, default, retry)
|
|
1917
|
+
|
|
1918
|
+
def get(
|
|
1919
|
+
self,
|
|
1920
|
+
key,
|
|
1921
|
+
default=None,
|
|
1922
|
+
read=False,
|
|
1923
|
+
expire_time=False,
|
|
1924
|
+
tag=False,
|
|
1925
|
+
retry=False,
|
|
1926
|
+
store_time=False,
|
|
1927
|
+
):
|
|
1928
|
+
"""Retrieve value from cache. If `key` is missing, return `default`.
|
|
1929
|
+
|
|
1930
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
1931
|
+
`False` (default).
|
|
1932
|
+
|
|
1933
|
+
When `expire_time`, `tag` or `store_time` is true, the result is a
|
|
1934
|
+
tuple of the value followed by the requested metadata in that order:
|
|
1935
|
+
the expire time and store time as seconds since the epoch, the tag as
|
|
1936
|
+
stored. Metadata is `None` when the key is missing.
|
|
1937
|
+
|
|
1938
|
+
:param key: key for item
|
|
1939
|
+
:param default: value to return if key is missing (default None)
|
|
1940
|
+
:param bool read: if True, return file handle to value
|
|
1941
|
+
(default False)
|
|
1942
|
+
:param bool expire_time: if True, return expire_time in tuple
|
|
1943
|
+
(default False)
|
|
1944
|
+
:param bool tag: if True, return tag in tuple (default False)
|
|
1945
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
1946
|
+
:param bool store_time: if True, return store_time in tuple
|
|
1947
|
+
(default False)
|
|
1948
|
+
:return: value for item or default if key not found
|
|
1949
|
+
:raises Timeout: if database timeout occurs
|
|
1950
|
+
|
|
1951
|
+
"""
|
|
1952
|
+
db_key, raw = self._disk.put(key)
|
|
1953
|
+
update_column = EVICTION_POLICY[self.eviction_policy]['get']
|
|
1954
|
+
statistics = self.statistics
|
|
1955
|
+
|
|
1956
|
+
if self._read_only:
|
|
1957
|
+
# Hits, misses and access times cannot be recorded, so take
|
|
1958
|
+
# the path that does not write.
|
|
1959
|
+
update_column = None
|
|
1960
|
+
statistics = False
|
|
1961
|
+
select = (
|
|
1962
|
+
'SELECT rowid, expire_time, tag, mode, filename, value, store_time'
|
|
1963
|
+
' FROM Cache WHERE key = ? AND raw = ?'
|
|
1964
|
+
' AND (expire_time IS NULL OR expire_time > ?)'
|
|
1965
|
+
)
|
|
1966
|
+
|
|
1967
|
+
default = _meta_result(default, expire_time, tag, store_time)
|
|
1968
|
+
|
|
1969
|
+
if not statistics and update_column is None:
|
|
1970
|
+
# Fast path, no transaction necessary.
|
|
1971
|
+
|
|
1972
|
+
rows = self._sql(select, (db_key, raw, time.time())).fetchall()
|
|
1973
|
+
|
|
1974
|
+
if not rows:
|
|
1975
|
+
return default
|
|
1976
|
+
|
|
1977
|
+
(
|
|
1978
|
+
(
|
|
1979
|
+
rowid,
|
|
1980
|
+
db_expire_time,
|
|
1981
|
+
db_tag,
|
|
1982
|
+
mode,
|
|
1983
|
+
filename,
|
|
1984
|
+
db_value,
|
|
1985
|
+
db_store_time,
|
|
1986
|
+
),
|
|
1987
|
+
) = rows
|
|
1988
|
+
|
|
1989
|
+
try:
|
|
1990
|
+
value = self._disk.fetch(mode, filename, db_value, read)
|
|
1991
|
+
except IOError:
|
|
1992
|
+
# Key was deleted before we could retrieve result.
|
|
1993
|
+
return default
|
|
1994
|
+
|
|
1995
|
+
else: # Slow path, transaction required.
|
|
1996
|
+
cache_hit = (
|
|
1997
|
+
"UPDATE Settings SET value = value + 1 WHERE key = 'hits'"
|
|
1998
|
+
)
|
|
1999
|
+
cache_miss = (
|
|
2000
|
+
"UPDATE Settings SET value = value + 1 WHERE key = 'misses'"
|
|
2001
|
+
)
|
|
2002
|
+
|
|
2003
|
+
with self._transact(retry) as (sql, _):
|
|
2004
|
+
rows = sql(select, (db_key, raw, time.time())).fetchall()
|
|
2005
|
+
|
|
2006
|
+
if not rows:
|
|
2007
|
+
if statistics:
|
|
2008
|
+
sql(cache_miss)
|
|
2009
|
+
return default
|
|
2010
|
+
|
|
2011
|
+
(
|
|
2012
|
+
(
|
|
2013
|
+
rowid,
|
|
2014
|
+
db_expire_time,
|
|
2015
|
+
db_tag,
|
|
2016
|
+
mode,
|
|
2017
|
+
filename,
|
|
2018
|
+
db_value,
|
|
2019
|
+
db_store_time,
|
|
2020
|
+
),
|
|
2021
|
+
) = rows
|
|
2022
|
+
|
|
2023
|
+
try:
|
|
2024
|
+
value = self._disk.fetch(mode, filename, db_value, read)
|
|
2025
|
+
except IOError:
|
|
2026
|
+
# Key was deleted before we could retrieve result.
|
|
2027
|
+
if statistics:
|
|
2028
|
+
sql(cache_miss)
|
|
2029
|
+
return default
|
|
2030
|
+
|
|
2031
|
+
if statistics:
|
|
2032
|
+
sql(cache_hit)
|
|
2033
|
+
|
|
2034
|
+
now = time.time()
|
|
2035
|
+
update = 'UPDATE Cache SET %s WHERE rowid = ?'
|
|
2036
|
+
|
|
2037
|
+
if update_column is not None:
|
|
2038
|
+
sql(update % update_column.format(now=now), (rowid,))
|
|
2039
|
+
|
|
2040
|
+
meta = (db_expire_time, db_tag, db_store_time)
|
|
2041
|
+
return _meta_result(value, expire_time, tag, store_time, meta)
|
|
2042
|
+
|
|
2043
|
+
def __getitem__(self, key):
|
|
2044
|
+
"""Return corresponding value for `key` from cache.
|
|
2045
|
+
|
|
2046
|
+
:param key: key matching item
|
|
2047
|
+
:return: corresponding value
|
|
2048
|
+
:raises KeyError: if key is not found
|
|
2049
|
+
|
|
2050
|
+
"""
|
|
2051
|
+
value = self.get(key, default=ENOVAL, retry=True)
|
|
2052
|
+
if value is ENOVAL:
|
|
2053
|
+
raise KeyError(key)
|
|
2054
|
+
return value
|
|
2055
|
+
|
|
2056
|
+
def read(self, key, retry=False):
|
|
2057
|
+
"""Return file handle value corresponding to `key` from cache.
|
|
2058
|
+
|
|
2059
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2060
|
+
`False` (default).
|
|
2061
|
+
|
|
2062
|
+
:param key: key matching item
|
|
2063
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2064
|
+
:return: file open for reading in binary mode
|
|
2065
|
+
:raises KeyError: if key is not found
|
|
2066
|
+
:raises Timeout: if database timeout occurs
|
|
2067
|
+
|
|
2068
|
+
"""
|
|
2069
|
+
handle = self.get(key, default=ENOVAL, read=True, retry=retry)
|
|
2070
|
+
if handle is ENOVAL:
|
|
2071
|
+
raise KeyError(key)
|
|
2072
|
+
return handle
|
|
2073
|
+
|
|
2074
|
+
def __contains__(self, key):
|
|
2075
|
+
"""Return `True` if `key` matching item is found in cache.
|
|
2076
|
+
|
|
2077
|
+
:param key: key matching item
|
|
2078
|
+
:return: True if key matching item
|
|
2079
|
+
|
|
2080
|
+
"""
|
|
2081
|
+
sql = self._sql
|
|
2082
|
+
db_key, raw = self._disk.put(key)
|
|
2083
|
+
select = (
|
|
2084
|
+
'SELECT rowid FROM Cache'
|
|
2085
|
+
' WHERE key = ? AND raw = ?'
|
|
2086
|
+
' AND (expire_time IS NULL OR expire_time > ?)'
|
|
2087
|
+
)
|
|
2088
|
+
|
|
2089
|
+
rows = sql(select, (db_key, raw, time.time())).fetchall()
|
|
2090
|
+
|
|
2091
|
+
return bool(rows)
|
|
2092
|
+
|
|
2093
|
+
def pop(
|
|
2094
|
+
self, key, default=None, expire_time=False, tag=False, retry=False
|
|
2095
|
+
): # noqa: E501
|
|
2096
|
+
"""Remove corresponding item for `key` from cache and return value.
|
|
2097
|
+
|
|
2098
|
+
If `key` is missing, return `default`.
|
|
2099
|
+
|
|
2100
|
+
Operation is atomic. Concurrent operations will be serialized.
|
|
2101
|
+
|
|
2102
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2103
|
+
`False` (default).
|
|
2104
|
+
|
|
2105
|
+
:param key: key for item
|
|
2106
|
+
:param default: value to return if key is missing (default None)
|
|
2107
|
+
:param bool expire_time: if True, return expire_time in tuple
|
|
2108
|
+
(default False)
|
|
2109
|
+
:param bool tag: if True, return tag in tuple (default False)
|
|
2110
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2111
|
+
:return: value for item or default if key not found
|
|
2112
|
+
:raises Timeout: if database timeout occurs
|
|
2113
|
+
|
|
2114
|
+
"""
|
|
2115
|
+
self._check_writable()
|
|
2116
|
+
db_key, raw = self._disk.put(key)
|
|
2117
|
+
select = (
|
|
2118
|
+
'SELECT rowid, expire_time, tag, mode, filename, value'
|
|
2119
|
+
' FROM Cache WHERE key = ? AND raw = ?'
|
|
2120
|
+
' AND (expire_time IS NULL OR expire_time > ?)'
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
if expire_time and tag:
|
|
2124
|
+
default = default, None, None
|
|
2125
|
+
elif expire_time or tag:
|
|
2126
|
+
default = default, None
|
|
2127
|
+
|
|
2128
|
+
with self._transact(retry) as (sql, _):
|
|
2129
|
+
rows = sql(select, (db_key, raw, time.time())).fetchall()
|
|
2130
|
+
|
|
2131
|
+
if not rows:
|
|
2132
|
+
return default
|
|
2133
|
+
|
|
2134
|
+
((rowid, db_expire_time, db_tag, mode, filename, db_value),) = rows
|
|
2135
|
+
|
|
2136
|
+
sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
|
|
2137
|
+
|
|
2138
|
+
try:
|
|
2139
|
+
value = self._disk.fetch(mode, filename, db_value, False)
|
|
2140
|
+
except IOError:
|
|
2141
|
+
# Key was deleted before we could retrieve result.
|
|
2142
|
+
return default
|
|
2143
|
+
finally:
|
|
2144
|
+
if filename is not None:
|
|
2145
|
+
self._disk.remove(filename)
|
|
2146
|
+
|
|
2147
|
+
if expire_time and tag:
|
|
2148
|
+
return value, db_expire_time, db_tag
|
|
2149
|
+
elif expire_time:
|
|
2150
|
+
return value, db_expire_time
|
|
2151
|
+
elif tag:
|
|
2152
|
+
return value, db_tag
|
|
2153
|
+
else:
|
|
2154
|
+
return value
|
|
2155
|
+
|
|
2156
|
+
def __delitem__(self, key, retry=True):
|
|
2157
|
+
"""Delete corresponding item for `key` from cache.
|
|
2158
|
+
|
|
2159
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2160
|
+
`False` (default `True`).
|
|
2161
|
+
|
|
2162
|
+
:param key: key matching item
|
|
2163
|
+
:param bool retry: retry if database timeout occurs (default True)
|
|
2164
|
+
:raises KeyError: if key is not found
|
|
2165
|
+
:raises Timeout: if database timeout occurs
|
|
2166
|
+
|
|
2167
|
+
"""
|
|
2168
|
+
self._check_writable()
|
|
2169
|
+
db_key, raw = self._disk.put(key)
|
|
2170
|
+
|
|
2171
|
+
with self._transact(retry) as (sql, cleanup):
|
|
2172
|
+
rows = sql(
|
|
2173
|
+
'SELECT rowid, filename FROM Cache'
|
|
2174
|
+
' WHERE key = ? AND raw = ?'
|
|
2175
|
+
' AND (expire_time IS NULL OR expire_time > ?)',
|
|
2176
|
+
(db_key, raw, time.time()),
|
|
2177
|
+
).fetchall()
|
|
2178
|
+
|
|
2179
|
+
if not rows:
|
|
2180
|
+
raise KeyError(key)
|
|
2181
|
+
|
|
2182
|
+
((rowid, filename),) = rows
|
|
2183
|
+
sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
|
|
2184
|
+
cleanup(filename)
|
|
2185
|
+
|
|
2186
|
+
return True
|
|
2187
|
+
|
|
2188
|
+
def delete(self, key, retry=False):
|
|
2189
|
+
"""Delete corresponding item for `key` from cache.
|
|
2190
|
+
|
|
2191
|
+
Missing keys are ignored.
|
|
2192
|
+
|
|
2193
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2194
|
+
`False` (default).
|
|
2195
|
+
|
|
2196
|
+
:param key: key matching item
|
|
2197
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2198
|
+
:return: True if item was deleted
|
|
2199
|
+
:raises Timeout: if database timeout occurs
|
|
2200
|
+
|
|
2201
|
+
"""
|
|
2202
|
+
# pylint: disable=unnecessary-dunder-call
|
|
2203
|
+
try:
|
|
2204
|
+
return self.__delitem__(key, retry=retry)
|
|
2205
|
+
except KeyError:
|
|
2206
|
+
return False
|
|
2207
|
+
|
|
2208
|
+
def push(
|
|
2209
|
+
self,
|
|
2210
|
+
value,
|
|
2211
|
+
prefix=None,
|
|
2212
|
+
side='back',
|
|
2213
|
+
expire=None,
|
|
2214
|
+
read=False,
|
|
2215
|
+
tag=None,
|
|
2216
|
+
retry=False,
|
|
2217
|
+
):
|
|
2218
|
+
"""Push `value` onto `side` of queue identified by `prefix` in cache.
|
|
2219
|
+
|
|
2220
|
+
When prefix is None, integer keys are used. Otherwise, string keys are
|
|
2221
|
+
used in the format "prefix-integer". Integer starts at 500 trillion.
|
|
2222
|
+
|
|
2223
|
+
Defaults to pushing value on back of queue. Set side to 'front' to push
|
|
2224
|
+
value on front of queue. Side must be one of 'back' or 'front'.
|
|
2225
|
+
|
|
2226
|
+
Operation is atomic. Concurrent operations will be serialized.
|
|
2227
|
+
|
|
2228
|
+
When `read` is `True`, `value` should be a file-like object opened
|
|
2229
|
+
for reading in binary mode.
|
|
2230
|
+
|
|
2231
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2232
|
+
`False` (default).
|
|
2233
|
+
|
|
2234
|
+
See also `Cache.pull`.
|
|
2235
|
+
|
|
2236
|
+
>>> cache = Cache()
|
|
2237
|
+
>>> print(cache.push('first value'))
|
|
2238
|
+
500000000000000
|
|
2239
|
+
>>> cache.get(500000000000000)
|
|
2240
|
+
'first value'
|
|
2241
|
+
>>> print(cache.push('second value'))
|
|
2242
|
+
500000000000001
|
|
2243
|
+
>>> print(cache.push('third value', side='front'))
|
|
2244
|
+
499999999999999
|
|
2245
|
+
>>> cache.push(1234, prefix='userids')
|
|
2246
|
+
'userids-500000000000000'
|
|
2247
|
+
|
|
2248
|
+
:param value: value for item
|
|
2249
|
+
:param str prefix: key prefix (default None, key is integer)
|
|
2250
|
+
:param str side: either 'back' or 'front' (default 'back')
|
|
2251
|
+
:param float expire: seconds until the key expires
|
|
2252
|
+
(default None, no expiry)
|
|
2253
|
+
:param bool read: read value as bytes from file (default False)
|
|
2254
|
+
:param str tag: text to associate with key (default None)
|
|
2255
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2256
|
+
:return: key for item in cache
|
|
2257
|
+
:raises Timeout: if database timeout occurs
|
|
2258
|
+
|
|
2259
|
+
"""
|
|
2260
|
+
self._check_writable()
|
|
2261
|
+
if prefix is None:
|
|
2262
|
+
min_key = 0
|
|
2263
|
+
max_key = 999999999999999
|
|
2264
|
+
else:
|
|
2265
|
+
min_key = prefix + '-000000000000000'
|
|
2266
|
+
max_key = prefix + '-999999999999999'
|
|
2267
|
+
|
|
2268
|
+
now = time.time()
|
|
2269
|
+
raw = True
|
|
2270
|
+
expire_time = None if expire is None else now + expire
|
|
2271
|
+
size, mode, filename, db_value = self._disk.store(value, read)
|
|
2272
|
+
columns = (expire_time, tag, size, mode, filename, db_value)
|
|
2273
|
+
order = {'back': 'DESC', 'front': 'ASC'}
|
|
2274
|
+
select = (
|
|
2275
|
+
'SELECT key FROM Cache'
|
|
2276
|
+
' WHERE ? < key AND key < ? AND raw = ?'
|
|
2277
|
+
' ORDER BY key %s LIMIT 1'
|
|
2278
|
+
) % order[side]
|
|
2279
|
+
|
|
2280
|
+
with self._transact(retry, filename) as (sql, cleanup):
|
|
2281
|
+
rows = sql(select, (min_key, max_key, raw)).fetchall()
|
|
2282
|
+
|
|
2283
|
+
if rows:
|
|
2284
|
+
((key,),) = rows
|
|
2285
|
+
|
|
2286
|
+
if prefix is not None:
|
|
2287
|
+
num = int(key[(key.rfind('-') + 1) :])
|
|
2288
|
+
else:
|
|
2289
|
+
num = key
|
|
2290
|
+
|
|
2291
|
+
if side == 'back':
|
|
2292
|
+
num += 1
|
|
2293
|
+
else:
|
|
2294
|
+
assert side == 'front'
|
|
2295
|
+
num -= 1
|
|
2296
|
+
else:
|
|
2297
|
+
num = 500000000000000
|
|
2298
|
+
|
|
2299
|
+
if prefix is not None:
|
|
2300
|
+
db_key = '{0}-{1:015d}'.format(prefix, num)
|
|
2301
|
+
else:
|
|
2302
|
+
db_key = num
|
|
2303
|
+
|
|
2304
|
+
self._row_insert(db_key, raw, now, columns)
|
|
2305
|
+
self._cull(now, sql, cleanup)
|
|
2306
|
+
|
|
2307
|
+
return db_key
|
|
2308
|
+
|
|
2309
|
+
def pull(
|
|
2310
|
+
self,
|
|
2311
|
+
prefix=None,
|
|
2312
|
+
default=(None, None),
|
|
2313
|
+
side='front',
|
|
2314
|
+
expire_time=False,
|
|
2315
|
+
tag=False,
|
|
2316
|
+
retry=False,
|
|
2317
|
+
):
|
|
2318
|
+
"""Pull key and value item pair from `side` of queue in cache.
|
|
2319
|
+
|
|
2320
|
+
When prefix is None, integer keys are used. Otherwise, string keys are
|
|
2321
|
+
used in the format "prefix-integer". Integer starts at 500 trillion.
|
|
2322
|
+
|
|
2323
|
+
If queue is empty, return default.
|
|
2324
|
+
|
|
2325
|
+
Defaults to pulling key and value item pairs from front of queue. Set
|
|
2326
|
+
side to 'back' to pull from back of queue. Side must be one of 'front'
|
|
2327
|
+
or 'back'.
|
|
2328
|
+
|
|
2329
|
+
Operation is atomic. Concurrent operations will be serialized.
|
|
2330
|
+
|
|
2331
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2332
|
+
`False` (default).
|
|
2333
|
+
|
|
2334
|
+
See also `Cache.push` and `Cache.get`.
|
|
2335
|
+
|
|
2336
|
+
>>> cache = Cache()
|
|
2337
|
+
>>> cache.pull()
|
|
2338
|
+
(None, None)
|
|
2339
|
+
>>> for letter in 'abc':
|
|
2340
|
+
... print(cache.push(letter))
|
|
2341
|
+
500000000000000
|
|
2342
|
+
500000000000001
|
|
2343
|
+
500000000000002
|
|
2344
|
+
>>> key, value = cache.pull()
|
|
2345
|
+
>>> print(key)
|
|
2346
|
+
500000000000000
|
|
2347
|
+
>>> value
|
|
2348
|
+
'a'
|
|
2349
|
+
>>> _, value = cache.pull(side='back')
|
|
2350
|
+
>>> value
|
|
2351
|
+
'c'
|
|
2352
|
+
>>> cache.push(1234, 'userids')
|
|
2353
|
+
'userids-500000000000000'
|
|
2354
|
+
>>> _, value = cache.pull('userids')
|
|
2355
|
+
>>> value
|
|
2356
|
+
1234
|
|
2357
|
+
|
|
2358
|
+
:param str prefix: key prefix (default None, key is integer)
|
|
2359
|
+
:param default: value to return if key is missing
|
|
2360
|
+
(default (None, None))
|
|
2361
|
+
:param str side: either 'front' or 'back' (default 'front')
|
|
2362
|
+
:param bool expire_time: if True, return expire_time in tuple
|
|
2363
|
+
(default False)
|
|
2364
|
+
:param bool tag: if True, return tag in tuple (default False)
|
|
2365
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2366
|
+
:return: key and value item pair or default if queue is empty
|
|
2367
|
+
:raises Timeout: if database timeout occurs
|
|
2368
|
+
|
|
2369
|
+
"""
|
|
2370
|
+
self._check_writable()
|
|
2371
|
+
# Caution: Nearly identical code exists in Cache.peek
|
|
2372
|
+
if prefix is None:
|
|
2373
|
+
min_key = 0
|
|
2374
|
+
max_key = 999999999999999
|
|
2375
|
+
else:
|
|
2376
|
+
min_key = prefix + '-000000000000000'
|
|
2377
|
+
max_key = prefix + '-999999999999999'
|
|
2378
|
+
|
|
2379
|
+
order = {'front': 'ASC', 'back': 'DESC'}
|
|
2380
|
+
select = (
|
|
2381
|
+
'SELECT rowid, key, expire_time, tag, mode, filename, value'
|
|
2382
|
+
' FROM Cache WHERE ? < key AND key < ? AND raw = 1'
|
|
2383
|
+
' ORDER BY key %s LIMIT 1'
|
|
2384
|
+
) % order[side]
|
|
2385
|
+
|
|
2386
|
+
if expire_time and tag:
|
|
2387
|
+
default = default, None, None
|
|
2388
|
+
elif expire_time or tag:
|
|
2389
|
+
default = default, None
|
|
2390
|
+
|
|
2391
|
+
while True:
|
|
2392
|
+
while True:
|
|
2393
|
+
with self._transact(retry) as (sql, cleanup):
|
|
2394
|
+
rows = sql(select, (min_key, max_key)).fetchall()
|
|
2395
|
+
|
|
2396
|
+
if not rows:
|
|
2397
|
+
return default
|
|
2398
|
+
|
|
2399
|
+
(
|
|
2400
|
+
(rowid, key, db_expire, db_tag, mode, name, db_value),
|
|
2401
|
+
) = rows
|
|
2402
|
+
|
|
2403
|
+
sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
|
|
2404
|
+
|
|
2405
|
+
if db_expire is not None and db_expire < time.time():
|
|
2406
|
+
cleanup(name)
|
|
2407
|
+
else:
|
|
2408
|
+
break
|
|
2409
|
+
|
|
2410
|
+
try:
|
|
2411
|
+
value = self._disk.fetch(mode, name, db_value, False)
|
|
2412
|
+
except IOError:
|
|
2413
|
+
# Key was deleted before we could retrieve result.
|
|
2414
|
+
continue
|
|
2415
|
+
finally:
|
|
2416
|
+
if name is not None:
|
|
2417
|
+
self._disk.remove(name)
|
|
2418
|
+
break
|
|
2419
|
+
|
|
2420
|
+
if expire_time and tag:
|
|
2421
|
+
return (key, value), db_expire, db_tag
|
|
2422
|
+
elif expire_time:
|
|
2423
|
+
return (key, value), db_expire
|
|
2424
|
+
elif tag:
|
|
2425
|
+
return (key, value), db_tag
|
|
2426
|
+
else:
|
|
2427
|
+
return key, value
|
|
2428
|
+
|
|
2429
|
+
def peek(
|
|
2430
|
+
self,
|
|
2431
|
+
prefix=None,
|
|
2432
|
+
default=(None, None),
|
|
2433
|
+
side='front',
|
|
2434
|
+
expire_time=False,
|
|
2435
|
+
tag=False,
|
|
2436
|
+
retry=False,
|
|
2437
|
+
):
|
|
2438
|
+
"""Peek at key and value item pair from `side` of queue in cache.
|
|
2439
|
+
|
|
2440
|
+
When prefix is None, integer keys are used. Otherwise, string keys are
|
|
2441
|
+
used in the format "prefix-integer". Integer starts at 500 trillion.
|
|
2442
|
+
|
|
2443
|
+
If queue is empty, return default.
|
|
2444
|
+
|
|
2445
|
+
Defaults to peeking at key and value item pairs from front of queue.
|
|
2446
|
+
Set side to 'back' to pull from back of queue. Side must be one of
|
|
2447
|
+
'front' or 'back'.
|
|
2448
|
+
|
|
2449
|
+
Expired items are deleted from cache. Operation is atomic. Concurrent
|
|
2450
|
+
operations will be serialized.
|
|
2451
|
+
|
|
2452
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2453
|
+
`False` (default).
|
|
2454
|
+
|
|
2455
|
+
See also `Cache.pull` and `Cache.push`.
|
|
2456
|
+
|
|
2457
|
+
>>> cache = Cache()
|
|
2458
|
+
>>> for letter in 'abc':
|
|
2459
|
+
... print(cache.push(letter))
|
|
2460
|
+
500000000000000
|
|
2461
|
+
500000000000001
|
|
2462
|
+
500000000000002
|
|
2463
|
+
>>> key, value = cache.peek()
|
|
2464
|
+
>>> print(key)
|
|
2465
|
+
500000000000000
|
|
2466
|
+
>>> value
|
|
2467
|
+
'a'
|
|
2468
|
+
>>> key, value = cache.peek(side='back')
|
|
2469
|
+
>>> print(key)
|
|
2470
|
+
500000000000002
|
|
2471
|
+
>>> value
|
|
2472
|
+
'c'
|
|
2473
|
+
|
|
2474
|
+
:param str prefix: key prefix (default None, key is integer)
|
|
2475
|
+
:param default: value to return if key is missing
|
|
2476
|
+
(default (None, None))
|
|
2477
|
+
:param str side: either 'front' or 'back' (default 'front')
|
|
2478
|
+
:param bool expire_time: if True, return expire_time in tuple
|
|
2479
|
+
(default False)
|
|
2480
|
+
:param bool tag: if True, return tag in tuple (default False)
|
|
2481
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2482
|
+
:return: key and value item pair or default if queue is empty
|
|
2483
|
+
:raises Timeout: if database timeout occurs
|
|
2484
|
+
|
|
2485
|
+
"""
|
|
2486
|
+
# Caution: Nearly identical code exists in Cache.pull
|
|
2487
|
+
if prefix is None:
|
|
2488
|
+
min_key = 0
|
|
2489
|
+
max_key = 999999999999999
|
|
2490
|
+
else:
|
|
2491
|
+
min_key = prefix + '-000000000000000'
|
|
2492
|
+
max_key = prefix + '-999999999999999'
|
|
2493
|
+
|
|
2494
|
+
order = {'front': 'ASC', 'back': 'DESC'}
|
|
2495
|
+
live = ' AND ' + _LIVE_CONDITION if self._read_only else ''
|
|
2496
|
+
select = (
|
|
2497
|
+
'SELECT rowid, key, expire_time, tag, mode, filename, value'
|
|
2498
|
+
' FROM Cache WHERE ? < key AND key < ? AND raw = 1%s'
|
|
2499
|
+
' ORDER BY key %s LIMIT 1'
|
|
2500
|
+
) % (live, order[side])
|
|
2501
|
+
|
|
2502
|
+
if expire_time and tag:
|
|
2503
|
+
default = default, None, None
|
|
2504
|
+
elif expire_time or tag:
|
|
2505
|
+
default = default, None
|
|
2506
|
+
|
|
2507
|
+
while True:
|
|
2508
|
+
while True:
|
|
2509
|
+
with self._transact(retry) as (sql, cleanup):
|
|
2510
|
+
rows = sql(select, (min_key, max_key)).fetchall()
|
|
2511
|
+
|
|
2512
|
+
if not rows:
|
|
2513
|
+
return default
|
|
2514
|
+
|
|
2515
|
+
(
|
|
2516
|
+
(rowid, key, db_expire, db_tag, mode, name, db_value),
|
|
2517
|
+
) = rows
|
|
2518
|
+
|
|
2519
|
+
expired = db_expire is not None and db_expire < time.time()
|
|
2520
|
+
|
|
2521
|
+
if expired and not self._read_only:
|
|
2522
|
+
sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
|
|
2523
|
+
cleanup(name)
|
|
2524
|
+
else:
|
|
2525
|
+
break
|
|
2526
|
+
|
|
2527
|
+
try:
|
|
2528
|
+
value = self._disk.fetch(mode, name, db_value, False)
|
|
2529
|
+
except IOError:
|
|
2530
|
+
# Key was deleted before we could retrieve result.
|
|
2531
|
+
continue
|
|
2532
|
+
break
|
|
2533
|
+
|
|
2534
|
+
if expire_time and tag:
|
|
2535
|
+
return (key, value), db_expire, db_tag
|
|
2536
|
+
elif expire_time:
|
|
2537
|
+
return (key, value), db_expire
|
|
2538
|
+
elif tag:
|
|
2539
|
+
return (key, value), db_tag
|
|
2540
|
+
else:
|
|
2541
|
+
return key, value
|
|
2542
|
+
|
|
2543
|
+
def peekitem(self, last=True, expire_time=False, tag=False, retry=False):
|
|
2544
|
+
"""Peek at key and value item pair in cache based on iteration order.
|
|
2545
|
+
|
|
2546
|
+
Expired items are deleted from cache. Operation is atomic. Concurrent
|
|
2547
|
+
operations will be serialized.
|
|
2548
|
+
|
|
2549
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2550
|
+
`False` (default).
|
|
2551
|
+
|
|
2552
|
+
>>> cache = Cache()
|
|
2553
|
+
>>> for num, letter in enumerate('abc'):
|
|
2554
|
+
... cache[letter] = num
|
|
2555
|
+
>>> cache.peekitem()
|
|
2556
|
+
('c', 2)
|
|
2557
|
+
>>> cache.peekitem(last=False)
|
|
2558
|
+
('a', 0)
|
|
2559
|
+
|
|
2560
|
+
:param bool last: last item in iteration order (default True)
|
|
2561
|
+
:param bool expire_time: if True, return expire_time in tuple
|
|
2562
|
+
(default False)
|
|
2563
|
+
:param bool tag: if True, return tag in tuple (default False)
|
|
2564
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2565
|
+
:return: key and value item pair
|
|
2566
|
+
:raises KeyError: if cache is empty
|
|
2567
|
+
:raises Timeout: if database timeout occurs
|
|
2568
|
+
|
|
2569
|
+
"""
|
|
2570
|
+
order = ('ASC', 'DESC')
|
|
2571
|
+
live = ' WHERE ' + _LIVE_CONDITION if self._read_only else ''
|
|
2572
|
+
select = (
|
|
2573
|
+
'SELECT rowid, key, raw, expire_time, tag, mode, filename, value'
|
|
2574
|
+
' FROM Cache%s ORDER BY rowid %s LIMIT 1'
|
|
2575
|
+
) % (live, order[last])
|
|
2576
|
+
|
|
2577
|
+
while True:
|
|
2578
|
+
while True:
|
|
2579
|
+
with self._transact(retry) as (sql, cleanup):
|
|
2580
|
+
rows = sql(select).fetchall()
|
|
2581
|
+
|
|
2582
|
+
if not rows:
|
|
2583
|
+
raise KeyError('dictionary is empty')
|
|
2584
|
+
|
|
2585
|
+
(
|
|
2586
|
+
(
|
|
2587
|
+
rowid,
|
|
2588
|
+
db_key,
|
|
2589
|
+
raw,
|
|
2590
|
+
db_expire,
|
|
2591
|
+
db_tag,
|
|
2592
|
+
mode,
|
|
2593
|
+
name,
|
|
2594
|
+
db_value,
|
|
2595
|
+
),
|
|
2596
|
+
) = rows
|
|
2597
|
+
|
|
2598
|
+
expired = db_expire is not None and db_expire < time.time()
|
|
2599
|
+
|
|
2600
|
+
if expired and not self._read_only:
|
|
2601
|
+
sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
|
|
2602
|
+
cleanup(name)
|
|
2603
|
+
else:
|
|
2604
|
+
break
|
|
2605
|
+
|
|
2606
|
+
key = self._disk.get(db_key, raw)
|
|
2607
|
+
|
|
2608
|
+
try:
|
|
2609
|
+
value = self._disk.fetch(mode, name, db_value, False)
|
|
2610
|
+
except IOError:
|
|
2611
|
+
# Key was deleted before we could retrieve result.
|
|
2612
|
+
continue
|
|
2613
|
+
break
|
|
2614
|
+
|
|
2615
|
+
if expire_time and tag:
|
|
2616
|
+
return (key, value), db_expire, db_tag
|
|
2617
|
+
elif expire_time:
|
|
2618
|
+
return (key, value), db_expire
|
|
2619
|
+
elif tag:
|
|
2620
|
+
return (key, value), db_tag
|
|
2621
|
+
else:
|
|
2622
|
+
return key, value
|
|
2623
|
+
|
|
2624
|
+
def memoize(
|
|
2625
|
+
self, name=None, typed=False, expire=None, tag=None, ignore=()
|
|
2626
|
+
):
|
|
2627
|
+
"""Memoizing cache decorator.
|
|
2628
|
+
|
|
2629
|
+
Decorator to wrap callable with memoizing function using cache.
|
|
2630
|
+
Repeated calls with the same arguments will lookup result in cache and
|
|
2631
|
+
avoid function evaluation.
|
|
2632
|
+
|
|
2633
|
+
If name is set to None (default), the callable name will be determined
|
|
2634
|
+
automatically.
|
|
2635
|
+
|
|
2636
|
+
When expire is set to zero, function results will not be set in the
|
|
2637
|
+
cache. Cache lookups still occur, however. Read
|
|
2638
|
+
:doc:`case-study-landing-page-caching` for example usage.
|
|
2639
|
+
|
|
2640
|
+
If typed is set to True, function arguments of different types will be
|
|
2641
|
+
cached separately. For example, f(3) and f(3.0) will be treated as
|
|
2642
|
+
distinct calls with distinct results.
|
|
2643
|
+
|
|
2644
|
+
The original underlying function is accessible through the __wrapped__
|
|
2645
|
+
attribute. This is useful for introspection, for bypassing the cache,
|
|
2646
|
+
or for rewrapping the function with a different cache.
|
|
2647
|
+
|
|
2648
|
+
>>> from diskcache import Cache
|
|
2649
|
+
>>> cache = Cache()
|
|
2650
|
+
>>> @cache.memoize(expire=1, tag='fib')
|
|
2651
|
+
... def fibonacci(number):
|
|
2652
|
+
... if number == 0:
|
|
2653
|
+
... return 0
|
|
2654
|
+
... elif number == 1:
|
|
2655
|
+
... return 1
|
|
2656
|
+
... else:
|
|
2657
|
+
... return fibonacci(number - 1) + fibonacci(number - 2)
|
|
2658
|
+
>>> print(fibonacci(100))
|
|
2659
|
+
354224848179261915075
|
|
2660
|
+
|
|
2661
|
+
An additional `__cache_key__` attribute can be used to generate the
|
|
2662
|
+
cache key used for the given arguments.
|
|
2663
|
+
|
|
2664
|
+
>>> key = fibonacci.__cache_key__(100)
|
|
2665
|
+
>>> print(cache[key])
|
|
2666
|
+
354224848179261915075
|
|
2667
|
+
|
|
2668
|
+
Remember to call memoize when decorating a callable. If you forget,
|
|
2669
|
+
then a TypeError will occur. Note the lack of parenthenses after
|
|
2670
|
+
memoize below:
|
|
2671
|
+
|
|
2672
|
+
>>> @cache.memoize
|
|
2673
|
+
... def test():
|
|
2674
|
+
... pass
|
|
2675
|
+
Traceback (most recent call last):
|
|
2676
|
+
...
|
|
2677
|
+
TypeError: name cannot be callable
|
|
2678
|
+
|
|
2679
|
+
:param cache: cache to store callable arguments and return values
|
|
2680
|
+
:param str name: name given for callable (default None, automatic)
|
|
2681
|
+
:param bool typed: cache different types separately (default False)
|
|
2682
|
+
:param float expire: seconds until arguments expire
|
|
2683
|
+
(default None, no expiry)
|
|
2684
|
+
:param str tag: text to associate with arguments (default None)
|
|
2685
|
+
:param set ignore: positional or keyword args to ignore (default ());
|
|
2686
|
+
an int ignores the argument at that position, a str ignores
|
|
2687
|
+
the parameter with that name however it is passed
|
|
2688
|
+
:return: callable decorator
|
|
2689
|
+
|
|
2690
|
+
"""
|
|
2691
|
+
# Caution: Nearly identical code exists in DjangoCache.memoize
|
|
2692
|
+
if callable(name):
|
|
2693
|
+
raise TypeError('name cannot be callable')
|
|
2694
|
+
|
|
2695
|
+
def decorator(func):
|
|
2696
|
+
"""Decorator created by memoize() for callable `func`."""
|
|
2697
|
+
base = (full_name(func),) if name is None else (name,)
|
|
2698
|
+
ignored = resolve_ignore(func, ignore)
|
|
2699
|
+
|
|
2700
|
+
@ft.wraps(func)
|
|
2701
|
+
def wrapper(*args, **kwargs):
|
|
2702
|
+
"""Wrapper for callable to cache arguments and return values."""
|
|
2703
|
+
key = wrapper.__cache_key__(*args, **kwargs)
|
|
2704
|
+
result = self.get(key, default=ENOVAL, retry=True)
|
|
2705
|
+
|
|
2706
|
+
if result is ENOVAL:
|
|
2707
|
+
result = func(*args, **kwargs)
|
|
2708
|
+
if expire is None or expire > 0:
|
|
2709
|
+
self.set(key, result, expire, tag=tag, retry=True)
|
|
2710
|
+
|
|
2711
|
+
return result
|
|
2712
|
+
|
|
2713
|
+
def __cache_key__(*args, **kwargs):
|
|
2714
|
+
"""Make key for cache given function arguments."""
|
|
2715
|
+
return args_to_key(base, args, kwargs, typed, ignored)
|
|
2716
|
+
|
|
2717
|
+
wrapper.__cache_key__ = __cache_key__
|
|
2718
|
+
return wrapper
|
|
2719
|
+
|
|
2720
|
+
return decorator
|
|
2721
|
+
|
|
2722
|
+
def check(self, fix=False, retry=False):
|
|
2723
|
+
"""Check database and file system consistency.
|
|
2724
|
+
|
|
2725
|
+
Intended for use in testing and post-mortem error analysis.
|
|
2726
|
+
|
|
2727
|
+
While checking the Cache table for consistency, a writer lock is held
|
|
2728
|
+
on the database. The lock blocks other cache clients from writing to
|
|
2729
|
+
the database. For caches with many file references, the lock may be
|
|
2730
|
+
held for a long time. For example, local benchmarking shows that a
|
|
2731
|
+
cache with 1,000 file references takes ~60ms to check.
|
|
2732
|
+
|
|
2733
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2734
|
+
`False` (default).
|
|
2735
|
+
|
|
2736
|
+
:param bool fix: correct inconsistencies
|
|
2737
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2738
|
+
:return: list of warnings
|
|
2739
|
+
:raises Timeout: if database timeout occurs
|
|
2740
|
+
|
|
2741
|
+
"""
|
|
2742
|
+
if fix:
|
|
2743
|
+
self._check_writable()
|
|
2744
|
+
# pylint: disable=access-member-before-definition,W0201
|
|
2745
|
+
with warnings.catch_warnings(record=True) as warns:
|
|
2746
|
+
sql = self._sql
|
|
2747
|
+
|
|
2748
|
+
# Check integrity of database.
|
|
2749
|
+
|
|
2750
|
+
rows = sql('PRAGMA integrity_check').fetchall()
|
|
2751
|
+
|
|
2752
|
+
if len(rows) != 1 or rows[0][0] != 'ok':
|
|
2753
|
+
for (message,) in rows:
|
|
2754
|
+
warnings.warn(message)
|
|
2755
|
+
|
|
2756
|
+
if fix:
|
|
2757
|
+
sql('VACUUM')
|
|
2758
|
+
|
|
2759
|
+
with self._transact(retry) as (sql, _):
|
|
2760
|
+
|
|
2761
|
+
# Check Cache.filename against file system.
|
|
2762
|
+
|
|
2763
|
+
filenames = set()
|
|
2764
|
+
select = (
|
|
2765
|
+
'SELECT rowid, size, filename FROM Cache'
|
|
2766
|
+
' WHERE filename IS NOT NULL'
|
|
2767
|
+
)
|
|
2768
|
+
|
|
2769
|
+
rows = sql(select).fetchall()
|
|
2770
|
+
|
|
2771
|
+
for rowid, size, filename in rows:
|
|
2772
|
+
full_path = op.join(
|
|
2773
|
+
self._directory, native_filename(filename)
|
|
2774
|
+
)
|
|
2775
|
+
filenames.add(full_path)
|
|
2776
|
+
|
|
2777
|
+
if op.exists(full_path):
|
|
2778
|
+
real_size = op.getsize(full_path)
|
|
2779
|
+
|
|
2780
|
+
if size != real_size:
|
|
2781
|
+
message = 'wrong file size: %s, %d != %d'
|
|
2782
|
+
args = full_path, real_size, size
|
|
2783
|
+
warnings.warn(message % args)
|
|
2784
|
+
|
|
2785
|
+
if fix:
|
|
2786
|
+
sql(
|
|
2787
|
+
'UPDATE Cache SET size = ?'
|
|
2788
|
+
' WHERE rowid = ?',
|
|
2789
|
+
(real_size, rowid),
|
|
2790
|
+
)
|
|
2791
|
+
|
|
2792
|
+
continue
|
|
2793
|
+
|
|
2794
|
+
warnings.warn('file not found: %s' % full_path)
|
|
2795
|
+
|
|
2796
|
+
if fix:
|
|
2797
|
+
sql('DELETE FROM Cache WHERE rowid = ?', (rowid,))
|
|
2798
|
+
|
|
2799
|
+
# Check file system against Cache.filename.
|
|
2800
|
+
|
|
2801
|
+
for dirpath, _, files in os.walk(self._directory):
|
|
2802
|
+
paths = [op.join(dirpath, filename) for filename in files]
|
|
2803
|
+
error = set(paths) - filenames
|
|
2804
|
+
|
|
2805
|
+
for full_path in error:
|
|
2806
|
+
if DBNAME in full_path:
|
|
2807
|
+
continue
|
|
2808
|
+
|
|
2809
|
+
message = 'unknown file: %s' % full_path
|
|
2810
|
+
warnings.warn(message, UnknownFileWarning)
|
|
2811
|
+
|
|
2812
|
+
if fix:
|
|
2813
|
+
os.remove(full_path)
|
|
2814
|
+
|
|
2815
|
+
# Check for empty directories.
|
|
2816
|
+
|
|
2817
|
+
for dirpath, dirs, files in os.walk(self._directory):
|
|
2818
|
+
if not (dirs or files):
|
|
2819
|
+
message = 'empty directory: %s' % dirpath
|
|
2820
|
+
warnings.warn(message, EmptyDirWarning)
|
|
2821
|
+
|
|
2822
|
+
if fix:
|
|
2823
|
+
os.rmdir(dirpath)
|
|
2824
|
+
|
|
2825
|
+
# Check Settings.count against count of Cache rows.
|
|
2826
|
+
|
|
2827
|
+
self.reset('count')
|
|
2828
|
+
((count,),) = sql('SELECT COUNT(key) FROM Cache').fetchall()
|
|
2829
|
+
|
|
2830
|
+
if self.count != count:
|
|
2831
|
+
message = 'Settings.count != COUNT(Cache.key); %d != %d'
|
|
2832
|
+
warnings.warn(message % (self.count, count))
|
|
2833
|
+
|
|
2834
|
+
if fix:
|
|
2835
|
+
sql(
|
|
2836
|
+
'UPDATE Settings SET value = ? WHERE key = ?',
|
|
2837
|
+
(count, 'count'),
|
|
2838
|
+
)
|
|
2839
|
+
|
|
2840
|
+
# Check Settings.size against sum of Cache.size column.
|
|
2841
|
+
|
|
2842
|
+
self.reset('size')
|
|
2843
|
+
select_size = 'SELECT COALESCE(SUM(size), 0) FROM Cache'
|
|
2844
|
+
((size,),) = sql(select_size).fetchall()
|
|
2845
|
+
|
|
2846
|
+
if self.size != size:
|
|
2847
|
+
message = 'Settings.size != SUM(Cache.size); %d != %d'
|
|
2848
|
+
warnings.warn(message % (self.size, size))
|
|
2849
|
+
|
|
2850
|
+
if fix:
|
|
2851
|
+
sql(
|
|
2852
|
+
'UPDATE Settings SET value = ? WHERE key =?',
|
|
2853
|
+
(size, 'size'),
|
|
2854
|
+
)
|
|
2855
|
+
|
|
2856
|
+
return warns
|
|
2857
|
+
|
|
2858
|
+
def create_tag_index(self):
|
|
2859
|
+
"""Create tag index on cache database.
|
|
2860
|
+
|
|
2861
|
+
It is better to initialize cache with `tag_index=True` than use this.
|
|
2862
|
+
|
|
2863
|
+
:raises Timeout: if database timeout occurs
|
|
2864
|
+
|
|
2865
|
+
"""
|
|
2866
|
+
self._check_writable()
|
|
2867
|
+
sql = self._sql
|
|
2868
|
+
sql(
|
|
2869
|
+
'CREATE INDEX IF NOT EXISTS Cache_tag_rowid ON Cache(tag, rowid) '
|
|
2870
|
+
'WHERE tag IS NOT NULL'
|
|
2871
|
+
)
|
|
2872
|
+
self.reset('tag_index', 1)
|
|
2873
|
+
|
|
2874
|
+
def drop_tag_index(self):
|
|
2875
|
+
"""Drop tag index on cache database.
|
|
2876
|
+
|
|
2877
|
+
:raises Timeout: if database timeout occurs
|
|
2878
|
+
|
|
2879
|
+
"""
|
|
2880
|
+
self._check_writable()
|
|
2881
|
+
sql = self._sql
|
|
2882
|
+
sql('DROP INDEX IF EXISTS Cache_tag_rowid')
|
|
2883
|
+
self.reset('tag_index', 0)
|
|
2884
|
+
|
|
2885
|
+
def evict(self, tag, retry=False):
|
|
2886
|
+
"""Remove items with matching `tag` from cache.
|
|
2887
|
+
|
|
2888
|
+
Removing items is an iterative process. In each iteration, a subset of
|
|
2889
|
+
items is removed. Concurrent writes may occur between iterations.
|
|
2890
|
+
|
|
2891
|
+
If a :exc:`Timeout` occurs, the first element of the exception's
|
|
2892
|
+
`args` attribute will be the number of items removed before the
|
|
2893
|
+
exception occurred.
|
|
2894
|
+
|
|
2895
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2896
|
+
`False` (default).
|
|
2897
|
+
|
|
2898
|
+
:param str tag: tag identifying items
|
|
2899
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2900
|
+
:return: count of rows removed
|
|
2901
|
+
:raises Timeout: if database timeout occurs
|
|
2902
|
+
|
|
2903
|
+
"""
|
|
2904
|
+
self._check_writable()
|
|
2905
|
+
select = (
|
|
2906
|
+
'SELECT rowid, filename FROM Cache'
|
|
2907
|
+
' WHERE tag = ? AND rowid > ?'
|
|
2908
|
+
' ORDER BY rowid LIMIT ?'
|
|
2909
|
+
)
|
|
2910
|
+
args = [tag, 0, 100]
|
|
2911
|
+
return self._select_delete(select, args, arg_index=1, retry=retry)
|
|
2912
|
+
|
|
2913
|
+
def expire(self, now=None, retry=False):
|
|
2914
|
+
"""Remove expired items from cache.
|
|
2915
|
+
|
|
2916
|
+
Removing items is an iterative process. In each iteration, a subset of
|
|
2917
|
+
items is removed. Concurrent writes may occur between iterations.
|
|
2918
|
+
|
|
2919
|
+
If a :exc:`Timeout` occurs, the first element of the exception's
|
|
2920
|
+
`args` attribute will be the number of items removed before the
|
|
2921
|
+
exception occurred.
|
|
2922
|
+
|
|
2923
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2924
|
+
`False` (default).
|
|
2925
|
+
|
|
2926
|
+
:param float now: current time (default None, ``time.time()`` used)
|
|
2927
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2928
|
+
:return: count of items removed
|
|
2929
|
+
:raises Timeout: if database timeout occurs
|
|
2930
|
+
|
|
2931
|
+
"""
|
|
2932
|
+
self._check_writable()
|
|
2933
|
+
select = (
|
|
2934
|
+
'SELECT rowid, expire_time, filename FROM Cache'
|
|
2935
|
+
' WHERE ? < expire_time AND expire_time < ?'
|
|
2936
|
+
' ORDER BY expire_time LIMIT ?'
|
|
2937
|
+
)
|
|
2938
|
+
args = [0, now or time.time(), 100]
|
|
2939
|
+
return self._select_delete(select, args, row_index=1, retry=retry)
|
|
2940
|
+
|
|
2941
|
+
def cull(self, retry=False):
|
|
2942
|
+
"""Cull items from cache until volume is less than size limit.
|
|
2943
|
+
|
|
2944
|
+
Removing items is an iterative process. In each iteration, a subset of
|
|
2945
|
+
items is removed. Concurrent writes may occur between iterations.
|
|
2946
|
+
|
|
2947
|
+
If a :exc:`Timeout` occurs, the first element of the exception's
|
|
2948
|
+
`args` attribute will be the number of items removed before the
|
|
2949
|
+
exception occurred.
|
|
2950
|
+
|
|
2951
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
2952
|
+
`False` (default).
|
|
2953
|
+
|
|
2954
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
2955
|
+
:return: count of items removed
|
|
2956
|
+
:raises Timeout: if database timeout occurs
|
|
2957
|
+
|
|
2958
|
+
"""
|
|
2959
|
+
self._check_writable()
|
|
2960
|
+
now = time.time()
|
|
2961
|
+
|
|
2962
|
+
# Remove expired items.
|
|
2963
|
+
|
|
2964
|
+
count = self.expire(now)
|
|
2965
|
+
|
|
2966
|
+
# Remove items by policy.
|
|
2967
|
+
|
|
2968
|
+
select_policy = EVICTION_POLICY[self.eviction_policy]['cull']
|
|
2969
|
+
|
|
2970
|
+
if select_policy is None:
|
|
2971
|
+
return 0
|
|
2972
|
+
|
|
2973
|
+
select_filename = select_policy.format(fields='filename', now=now)
|
|
2974
|
+
|
|
2975
|
+
try:
|
|
2976
|
+
while self.volume() > self.size_limit:
|
|
2977
|
+
with self._transact(retry) as (sql, cleanup):
|
|
2978
|
+
rows = sql(select_filename, (10,)).fetchall()
|
|
2979
|
+
|
|
2980
|
+
if not rows:
|
|
2981
|
+
break
|
|
2982
|
+
|
|
2983
|
+
count += len(rows)
|
|
2984
|
+
delete = (
|
|
2985
|
+
'DELETE FROM Cache WHERE rowid IN (%s)'
|
|
2986
|
+
% select_policy.format(fields='rowid', now=now)
|
|
2987
|
+
)
|
|
2988
|
+
sql(delete, (10,))
|
|
2989
|
+
|
|
2990
|
+
for (filename,) in rows:
|
|
2991
|
+
cleanup(filename)
|
|
2992
|
+
except Timeout:
|
|
2993
|
+
raise Timeout(count) from None
|
|
2994
|
+
|
|
2995
|
+
return count
|
|
2996
|
+
|
|
2997
|
+
def clear(self, retry=False):
|
|
2998
|
+
"""Remove all items from cache.
|
|
2999
|
+
|
|
3000
|
+
Removing items is an iterative process. In each iteration, a subset of
|
|
3001
|
+
items is removed. Concurrent writes may occur between iterations.
|
|
3002
|
+
|
|
3003
|
+
If a :exc:`Timeout` occurs, the first element of the exception's
|
|
3004
|
+
`args` attribute will be the number of items removed before the
|
|
3005
|
+
exception occurred.
|
|
3006
|
+
|
|
3007
|
+
Raises :exc:`Timeout` error when database timeout occurs and `retry` is
|
|
3008
|
+
`False` (default).
|
|
3009
|
+
|
|
3010
|
+
:param bool retry: retry if database timeout occurs (default False)
|
|
3011
|
+
:return: count of rows removed
|
|
3012
|
+
:raises Timeout: if database timeout occurs
|
|
3013
|
+
|
|
3014
|
+
"""
|
|
3015
|
+
self._check_writable()
|
|
3016
|
+
select = (
|
|
3017
|
+
'SELECT rowid, filename FROM Cache'
|
|
3018
|
+
' WHERE rowid > ?'
|
|
3019
|
+
' ORDER BY rowid LIMIT ?'
|
|
3020
|
+
)
|
|
3021
|
+
args = [0, 100]
|
|
3022
|
+
return self._select_delete(select, args, retry=retry)
|
|
3023
|
+
|
|
3024
|
+
def _select_delete(
|
|
3025
|
+
self, select, args, row_index=0, arg_index=0, retry=False
|
|
3026
|
+
):
|
|
3027
|
+
count = 0
|
|
3028
|
+
delete = 'DELETE FROM Cache WHERE rowid IN (%s)'
|
|
3029
|
+
|
|
3030
|
+
try:
|
|
3031
|
+
while True:
|
|
3032
|
+
with self._transact(retry) as (sql, cleanup):
|
|
3033
|
+
rows = sql(select, args).fetchall()
|
|
3034
|
+
|
|
3035
|
+
if not rows:
|
|
3036
|
+
break
|
|
3037
|
+
|
|
3038
|
+
count += len(rows)
|
|
3039
|
+
sql(delete % ','.join(str(row[0]) for row in rows))
|
|
3040
|
+
|
|
3041
|
+
for row in rows:
|
|
3042
|
+
args[arg_index] = row[row_index]
|
|
3043
|
+
cleanup(row[-1])
|
|
3044
|
+
|
|
3045
|
+
except Timeout:
|
|
3046
|
+
raise Timeout(count) from None
|
|
3047
|
+
|
|
3048
|
+
return count
|
|
3049
|
+
|
|
3050
|
+
def iterkeys(self, reverse=False, tag=ENOVAL):
|
|
3051
|
+
"""Iterate Cache keys in database sort order.
|
|
3052
|
+
|
|
3053
|
+
>>> cache = Cache()
|
|
3054
|
+
>>> for key in [4, 1, 3, 0, 2]:
|
|
3055
|
+
... cache[key] = key
|
|
3056
|
+
>>> list(cache.iterkeys())
|
|
3057
|
+
[0, 1, 2, 3, 4]
|
|
3058
|
+
>>> list(cache.iterkeys(reverse=True))
|
|
3059
|
+
[4, 3, 2, 1, 0]
|
|
3060
|
+
|
|
3061
|
+
If `tag` is given, only keys with a matching tag are returned. Use
|
|
3062
|
+
``tag=None`` to iterate untagged keys. Omitting `tag` returns all keys.
|
|
3063
|
+
Iteration includes expired items and does not read cached values.
|
|
3064
|
+
|
|
3065
|
+
:param bool reverse: reverse sort order (default False)
|
|
3066
|
+
:param tag: tag to match (default ENOVAL, matching all tags)
|
|
3067
|
+
:return: iterator of Cache keys
|
|
3068
|
+
|
|
3069
|
+
"""
|
|
3070
|
+
sql = self._sql
|
|
3071
|
+
limit = 100
|
|
3072
|
+
_disk_get = self._disk.get
|
|
3073
|
+
select = 'SELECT key, raw FROM Cache'
|
|
3074
|
+
args = ()
|
|
3075
|
+
|
|
3076
|
+
if tag is None:
|
|
3077
|
+
select += ' WHERE tag IS NULL'
|
|
3078
|
+
elif tag is not ENOVAL:
|
|
3079
|
+
select += ' WHERE tag = ?'
|
|
3080
|
+
args = (tag,)
|
|
3081
|
+
|
|
3082
|
+
iterate = select + (' WHERE (' if tag is ENOVAL else ' AND (')
|
|
3083
|
+
|
|
3084
|
+
if reverse:
|
|
3085
|
+
iterate += 'key = ? AND raw < ? OR key < ?)'
|
|
3086
|
+
order = ' ORDER BY key DESC, raw DESC'
|
|
3087
|
+
else:
|
|
3088
|
+
iterate += 'key = ? AND raw > ? OR key > ?)'
|
|
3089
|
+
order = ' ORDER BY key ASC, raw ASC'
|
|
3090
|
+
|
|
3091
|
+
select += order + ' LIMIT 1'
|
|
3092
|
+
iterate += order + ' LIMIT ?'
|
|
3093
|
+
row = sql(select, args).fetchall()
|
|
3094
|
+
|
|
3095
|
+
if row:
|
|
3096
|
+
((key, raw),) = row
|
|
3097
|
+
else:
|
|
3098
|
+
return
|
|
3099
|
+
|
|
3100
|
+
yield _disk_get(key, raw)
|
|
3101
|
+
|
|
3102
|
+
while True:
|
|
3103
|
+
rows = sql(iterate, args + (key, raw, key, limit)).fetchall()
|
|
3104
|
+
|
|
3105
|
+
if not rows:
|
|
3106
|
+
break
|
|
3107
|
+
|
|
3108
|
+
for key, raw in rows:
|
|
3109
|
+
yield _disk_get(key, raw)
|
|
3110
|
+
|
|
3111
|
+
def _iter(self, ascending=True):
|
|
3112
|
+
sql = self._sql
|
|
3113
|
+
rows = sql('SELECT MAX(rowid) FROM Cache').fetchall()
|
|
3114
|
+
((max_rowid,),) = rows
|
|
3115
|
+
yield # Signal ready.
|
|
3116
|
+
|
|
3117
|
+
if max_rowid is None:
|
|
3118
|
+
return
|
|
3119
|
+
|
|
3120
|
+
bound = max_rowid + 1
|
|
3121
|
+
limit = 100
|
|
3122
|
+
_disk_get = self._disk.get
|
|
3123
|
+
rowid = 0 if ascending else bound
|
|
3124
|
+
select = (
|
|
3125
|
+
'SELECT rowid, key, raw FROM Cache'
|
|
3126
|
+
' WHERE ? < rowid AND rowid < ?'
|
|
3127
|
+
' ORDER BY rowid %s LIMIT ?'
|
|
3128
|
+
) % ('ASC' if ascending else 'DESC')
|
|
3129
|
+
|
|
3130
|
+
while True:
|
|
3131
|
+
if ascending:
|
|
3132
|
+
args = (rowid, bound, limit)
|
|
3133
|
+
else:
|
|
3134
|
+
args = (0, rowid, limit)
|
|
3135
|
+
|
|
3136
|
+
rows = sql(select, args).fetchall()
|
|
3137
|
+
|
|
3138
|
+
if not rows:
|
|
3139
|
+
break
|
|
3140
|
+
|
|
3141
|
+
for rowid, key, raw in rows:
|
|
3142
|
+
yield _disk_get(key, raw)
|
|
3143
|
+
|
|
3144
|
+
def __iter__(self):
|
|
3145
|
+
"""Iterate keys in cache including expired items."""
|
|
3146
|
+
iterator = self._iter()
|
|
3147
|
+
next(iterator)
|
|
3148
|
+
return iterator
|
|
3149
|
+
|
|
3150
|
+
def __reversed__(self):
|
|
3151
|
+
"""Reverse iterate keys in cache including expired items."""
|
|
3152
|
+
iterator = self._iter(ascending=False)
|
|
3153
|
+
next(iterator)
|
|
3154
|
+
return iterator
|
|
3155
|
+
|
|
3156
|
+
def stats(self, enable=True, reset=False):
|
|
3157
|
+
"""Return cache statistics hits and misses.
|
|
3158
|
+
|
|
3159
|
+
:param bool enable: enable collecting statistics (default True)
|
|
3160
|
+
:param bool reset: reset hits and misses to 0 (default False)
|
|
3161
|
+
:return: (hits, misses)
|
|
3162
|
+
|
|
3163
|
+
"""
|
|
3164
|
+
# pylint: disable=E0203,W0201
|
|
3165
|
+
result = (self.reset('hits'), self.reset('misses'))
|
|
3166
|
+
|
|
3167
|
+
if self._read_only:
|
|
3168
|
+
# Counters cannot be reset or enabled on a read-only cache.
|
|
3169
|
+
return result
|
|
3170
|
+
|
|
3171
|
+
if reset:
|
|
3172
|
+
self.reset('hits', 0)
|
|
3173
|
+
self.reset('misses', 0)
|
|
3174
|
+
|
|
3175
|
+
self.reset('statistics', enable)
|
|
3176
|
+
|
|
3177
|
+
return result
|
|
3178
|
+
|
|
3179
|
+
def volume(self):
|
|
3180
|
+
"""Return estimated total size of cache on disk.
|
|
3181
|
+
|
|
3182
|
+
:return: size in bytes
|
|
3183
|
+
|
|
3184
|
+
"""
|
|
3185
|
+
((page_count,),) = self._sql('PRAGMA page_count').fetchall()
|
|
3186
|
+
total_size = self._page_size * page_count + self.reset('size')
|
|
3187
|
+
return total_size
|
|
3188
|
+
|
|
3189
|
+
def close(self):
|
|
3190
|
+
"""Close database connection.
|
|
3191
|
+
|
|
3192
|
+
Closes the connection opened by the calling thread and the
|
|
3193
|
+
connections of threads that have exited. Connections that other
|
|
3194
|
+
live threads use stay open. The cache reconnects on next use.
|
|
3195
|
+
|
|
3196
|
+
"""
|
|
3197
|
+
if self._cons.pid != os.getpid():
|
|
3198
|
+
self._discard_connections()
|
|
3199
|
+
return
|
|
3200
|
+
|
|
3201
|
+
local = self._local
|
|
3202
|
+
con = getattr(local, 'con', None)
|
|
3203
|
+
cons = self._cons.pop(threading.get_ident())
|
|
3204
|
+
|
|
3205
|
+
if con is not None and con not in cons:
|
|
3206
|
+
cons.append(con)
|
|
3207
|
+
|
|
3208
|
+
for name in ('con', 'sentinel'):
|
|
3209
|
+
try:
|
|
3210
|
+
delattr(local, name)
|
|
3211
|
+
except AttributeError:
|
|
3212
|
+
pass
|
|
3213
|
+
|
|
3214
|
+
for con in cons:
|
|
3215
|
+
con.close()
|
|
3216
|
+
|
|
3217
|
+
def __enter__(self):
|
|
3218
|
+
# Create connection in thread.
|
|
3219
|
+
# pylint: disable=unused-variable
|
|
3220
|
+
connection = self._con # noqa
|
|
3221
|
+
return self
|
|
3222
|
+
|
|
3223
|
+
def __exit__(self, *exception):
|
|
3224
|
+
self.close()
|
|
3225
|
+
|
|
3226
|
+
def __len__(self):
|
|
3227
|
+
"""Count of items in cache including expired items."""
|
|
3228
|
+
return self.reset('count')
|
|
3229
|
+
|
|
3230
|
+
def __getstate__(self):
|
|
3231
|
+
return (self.directory, self.timeout, type(self.disk))
|
|
3232
|
+
|
|
3233
|
+
def __setstate__(self, state):
|
|
3234
|
+
self.__init__(*state)
|
|
3235
|
+
|
|
3236
|
+
def reset(self, key, value=ENOVAL, update=True):
|
|
3237
|
+
"""Reset `key` and `value` item from Settings table.
|
|
3238
|
+
|
|
3239
|
+
Use `reset` to update the value of Cache settings correctly. Cache
|
|
3240
|
+
settings are stored in the Settings table of the SQLite database. If
|
|
3241
|
+
`update` is ``False`` then no attempt is made to update the database.
|
|
3242
|
+
|
|
3243
|
+
If `value` is not given, it is reloaded from the Settings
|
|
3244
|
+
table. Otherwise, the Settings table is updated.
|
|
3245
|
+
|
|
3246
|
+
Settings with the ``disk_`` prefix correspond to Disk
|
|
3247
|
+
attributes. Updating the value will change the unprefixed attribute on
|
|
3248
|
+
the associated Disk instance.
|
|
3249
|
+
|
|
3250
|
+
Settings with the ``sqlite_`` prefix correspond to SQLite
|
|
3251
|
+
pragmas. Updating the value will execute the corresponding PRAGMA
|
|
3252
|
+
statement.
|
|
3253
|
+
|
|
3254
|
+
SQLite PRAGMA statements may be executed before the Settings table
|
|
3255
|
+
exists in the database by setting `update` to ``False``.
|
|
3256
|
+
|
|
3257
|
+
:param str key: Settings key for item
|
|
3258
|
+
:param value: value for item (optional)
|
|
3259
|
+
:param bool update: update database Settings table (default True)
|
|
3260
|
+
:return: updated value for item
|
|
3261
|
+
:raises Timeout: if database timeout occurs
|
|
3262
|
+
|
|
3263
|
+
"""
|
|
3264
|
+
sql = self._sql
|
|
3265
|
+
sql_retry = self._sql_retry
|
|
3266
|
+
|
|
3267
|
+
if value is ENOVAL:
|
|
3268
|
+
select = 'SELECT value FROM Settings WHERE key = ?'
|
|
3269
|
+
rows = sql_retry(select, (key,)).fetchall()
|
|
3270
|
+
if not rows and self._read_only and key in METADATA:
|
|
3271
|
+
value = METADATA[key] # A snapshot without counters.
|
|
3272
|
+
else:
|
|
3273
|
+
((value,),) = rows
|
|
3274
|
+
setattr(self, key, value)
|
|
3275
|
+
return value
|
|
3276
|
+
|
|
3277
|
+
if update:
|
|
3278
|
+
self._check_writable()
|
|
3279
|
+
statement = 'UPDATE Settings SET value = ? WHERE key = ?'
|
|
3280
|
+
sql_retry(statement, (value, key))
|
|
3281
|
+
|
|
3282
|
+
if key.startswith('sqlite_'):
|
|
3283
|
+
pragma = key[7:]
|
|
3284
|
+
|
|
3285
|
+
if self._read_only and pragma in _PERSISTENT_PRAGMAS:
|
|
3286
|
+
# Stored in the file; a read-only cache takes what is there.
|
|
3287
|
+
setattr(self, key, value)
|
|
3288
|
+
return value
|
|
3289
|
+
|
|
3290
|
+
# 2016-02-17 GrantJ - PRAGMA and isolation_level=None
|
|
3291
|
+
# don't always play nicely together. Retry setting the
|
|
3292
|
+
# PRAGMA. I think some PRAGMA statements expect to
|
|
3293
|
+
# immediately take an EXCLUSIVE lock on the database. I
|
|
3294
|
+
# can't find any documentation for this but without the
|
|
3295
|
+
# retry, stress will intermittently fail with multiple
|
|
3296
|
+
# processes.
|
|
3297
|
+
|
|
3298
|
+
# 2018-11-05 GrantJ - Avoid setting pragma values that
|
|
3299
|
+
# are already set. Pragma settings like auto_vacuum and
|
|
3300
|
+
# journal_mode can take a long time or may not work after
|
|
3301
|
+
# tables have been created.
|
|
3302
|
+
|
|
3303
|
+
start = time.time()
|
|
3304
|
+
while True:
|
|
3305
|
+
try:
|
|
3306
|
+
try:
|
|
3307
|
+
((old_value,),) = sql(
|
|
3308
|
+
'PRAGMA %s' % (pragma)
|
|
3309
|
+
).fetchall()
|
|
3310
|
+
update = old_value != value
|
|
3311
|
+
except ValueError:
|
|
3312
|
+
update = True
|
|
3313
|
+
if update:
|
|
3314
|
+
sql('PRAGMA %s = %s' % (pragma, value)).fetchall()
|
|
3315
|
+
break
|
|
3316
|
+
except sqlite3.OperationalError as exc:
|
|
3317
|
+
if str(exc) != 'database is locked':
|
|
3318
|
+
raise
|
|
3319
|
+
diff = time.time() - start
|
|
3320
|
+
if diff > 60:
|
|
3321
|
+
raise
|
|
3322
|
+
time.sleep(0.001)
|
|
3323
|
+
elif key.startswith('disk_'):
|
|
3324
|
+
attr = key[5:]
|
|
3325
|
+
setattr(self._disk, attr, value)
|
|
3326
|
+
|
|
3327
|
+
setattr(self, key, value)
|
|
3328
|
+
return value
|