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/fanout.py ADDED
@@ -0,0 +1,717 @@
1
+ """Fanout cache automatically shards keys and values."""
2
+
3
+ import contextlib as cl
4
+ import functools
5
+ import itertools as it
6
+ import operator
7
+ import os.path as op
8
+ import sqlite3
9
+ import tempfile
10
+ import time
11
+
12
+ from .core import DEFAULT_SETTINGS, ENOVAL, Cache, Disk, Timeout, _meta_result
13
+ from .persistent import Deque, Index
14
+
15
+
16
+ def _default_with_meta(default, expire_time, tag, store_time=False):
17
+ """Match Cache.get/pop: wrap default when metadata is requested."""
18
+ return _meta_result(default, expire_time, tag, store_time)
19
+
20
+
21
+ class FanoutCache:
22
+ """Cache that shards keys and values."""
23
+
24
+ def __init__(
25
+ self, directory=None, shards=8, timeout=0.010, disk=Disk, **settings
26
+ ):
27
+ """Initialize cache instance.
28
+
29
+ :param str directory: cache directory
30
+ :param int shards: number of shards to distribute writes
31
+ :param float timeout: SQLite connection timeout
32
+ :param disk: `Disk` instance for serialization
33
+ :param settings: any of `DEFAULT_SETTINGS`
34
+
35
+ The cache directory, the shard directories and the directories of
36
+ sub-caches, deques and indexes are created with mode ``0o700`` like
37
+ :class:`Cache` directories; existing directories keep their mode.
38
+
39
+ """
40
+ if directory is None:
41
+ directory = tempfile.mkdtemp(prefix='diskcache-')
42
+ directory = str(directory)
43
+ directory = op.expanduser(directory)
44
+ directory = op.expandvars(directory)
45
+
46
+ default_size_limit = DEFAULT_SETTINGS['size_limit']
47
+ size_limit = settings.pop('size_limit', default_size_limit) / shards
48
+
49
+ self._count = shards
50
+ self._directory = directory
51
+ self._disk = disk
52
+ self._shards = tuple(
53
+ Cache(
54
+ directory=op.join(directory, '%03d' % num),
55
+ timeout=timeout,
56
+ disk=disk,
57
+ size_limit=size_limit,
58
+ **settings,
59
+ )
60
+ for num in range(shards)
61
+ )
62
+ self._hash = self._shards[0].disk.hash
63
+ self._caches = {}
64
+ self._deques = {}
65
+ self._indexes = {}
66
+
67
+ @property
68
+ def directory(self):
69
+ """Cache directory."""
70
+ return self._directory
71
+
72
+ def __getattr__(self, name):
73
+ # Raise AttributeError (not AssertionError) so builtins.hasattr() and
74
+ # introspection tools work. See GitHub issue #353.
75
+ safe_names = {'timeout', 'disk'}
76
+ valid_name = name in DEFAULT_SETTINGS or name in safe_names
77
+ if not valid_name:
78
+ raise AttributeError(
79
+ 'cannot access {} in cache shard'.format(name)
80
+ )
81
+ return getattr(self._shards[0], name)
82
+
83
+ @cl.contextmanager
84
+ def transact(self, retry=True):
85
+ """Context manager to perform a transaction by locking the cache.
86
+
87
+ While the cache is locked, no other write operation is permitted.
88
+ Transactions should therefore be as short as possible. Read and write
89
+ operations performed in a transaction are atomic. Read operations may
90
+ occur concurrent to a transaction.
91
+
92
+ Transactions may be nested and may not be shared between threads.
93
+
94
+ Blocks until transactions are held on all cache shards by retrying as
95
+ necessary.
96
+
97
+ >>> cache = FanoutCache()
98
+ >>> with cache.transact(): # Atomically increment two keys.
99
+ ... _ = cache.incr('total', 123.4)
100
+ ... _ = cache.incr('count', 1)
101
+ >>> with cache.transact(): # Atomically calculate average.
102
+ ... average = cache['total'] / cache['count']
103
+ >>> average
104
+ 123.4
105
+
106
+ :return: context manager for use in `with` statement
107
+
108
+ """
109
+ assert retry, 'retry must be True in FanoutCache'
110
+ with cl.ExitStack() as stack:
111
+ for shard in self._shards:
112
+ shard_transaction = shard.transact(retry=True)
113
+ stack.enter_context(shard_transaction)
114
+ yield
115
+
116
+ def set(self, key, value, expire=None, read=False, tag=None, retry=False):
117
+ """Set `key` and `value` item in cache.
118
+
119
+ When `read` is `True`, `value` should be a file-like object opened
120
+ for reading in binary mode.
121
+
122
+ If database timeout occurs then fails silently unless `retry` is set to
123
+ `True` (default `False`).
124
+
125
+ :param key: key for item
126
+ :param value: value for item
127
+ :param float expire: seconds until the key expires
128
+ (default None, no expiry)
129
+ :param bool read: read value as raw bytes from file (default False)
130
+ :param str tag: text to associate with key (default None)
131
+ :param bool retry: retry if database timeout occurs (default False)
132
+ :return: True if item was set
133
+
134
+ """
135
+ index = self._hash(key) % self._count
136
+ shard = self._shards[index]
137
+ try:
138
+ return shard.set(key, value, expire, read, tag, retry)
139
+ except Timeout:
140
+ return False
141
+
142
+ def __setitem__(self, key, value):
143
+ """Set `key` and `value` item in cache.
144
+
145
+ Calls :func:`FanoutCache.set` internally with `retry` set to `True`.
146
+
147
+ :param key: key for item
148
+ :param value: value for item
149
+
150
+ """
151
+ index = self._hash(key) % self._count
152
+ shard = self._shards[index]
153
+ shard[key] = value
154
+
155
+ def touch(self, key, expire=None, retry=False):
156
+ """Touch `key` in cache and update `expire` time.
157
+
158
+ If database timeout occurs then fails silently unless `retry` is set to
159
+ `True` (default `False`).
160
+
161
+ :param key: key for item
162
+ :param float expire: seconds until the key expires
163
+ (default None, no expiry)
164
+ :param bool retry: retry if database timeout occurs (default False)
165
+ :return: True if key was touched
166
+
167
+ """
168
+ index = self._hash(key) % self._count
169
+ shard = self._shards[index]
170
+ try:
171
+ return shard.touch(key, expire, retry)
172
+ except Timeout:
173
+ return False
174
+
175
+ def add(self, key, value, expire=None, read=False, tag=None, retry=False):
176
+ """Add `key` and `value` item to cache.
177
+
178
+ Similar to `set`, but only add to cache if key not present.
179
+
180
+ This operation is atomic. Only one concurrent add operation for given
181
+ key from separate threads or processes will succeed.
182
+
183
+ When `read` is `True`, `value` should be a file-like object opened
184
+ for reading in binary mode.
185
+
186
+ If database timeout occurs then fails silently unless `retry` is set to
187
+ `True` (default `False`).
188
+
189
+ :param key: key for item
190
+ :param value: value for item
191
+ :param float expire: seconds until the key expires
192
+ (default None, no expiry)
193
+ :param bool read: read value as bytes from file (default False)
194
+ :param str tag: text to associate with key (default None)
195
+ :param bool retry: retry if database timeout occurs (default False)
196
+ :return: True if item was added
197
+
198
+ """
199
+ index = self._hash(key) % self._count
200
+ shard = self._shards[index]
201
+ try:
202
+ return shard.add(key, value, expire, read, tag, retry)
203
+ except Timeout:
204
+ return False
205
+
206
+ def incr(self, key, delta=1, default=0, retry=False):
207
+ """Increment value by delta for item with key.
208
+
209
+ If key is missing and default is None then raise KeyError. Else if key
210
+ is missing and default is not None then use default for value.
211
+
212
+ Operation is atomic. All concurrent increment operations will be
213
+ counted individually.
214
+
215
+ Assumes value may be stored in a SQLite column. Most builds that target
216
+ machines with 64-bit pointer widths will support 64-bit signed
217
+ integers.
218
+
219
+ If database timeout occurs then fails silently unless `retry` is set to
220
+ `True` (default `False`).
221
+
222
+ :param key: key for item
223
+ :param int delta: amount to increment (default 1)
224
+ :param int default: value if key is missing (default 0)
225
+ :param bool retry: retry if database timeout occurs (default False)
226
+ :return: new value for item on success else None
227
+ :raises KeyError: if key is not found and default is None
228
+
229
+ """
230
+ index = self._hash(key) % self._count
231
+ shard = self._shards[index]
232
+ try:
233
+ return shard.incr(key, delta, default, retry)
234
+ except Timeout:
235
+ return None
236
+
237
+ def decr(self, key, delta=1, default=0, retry=False):
238
+ """Decrement value by delta for item with key.
239
+
240
+ If key is missing and default is None then raise KeyError. Else if key
241
+ is missing and default is not None then use default for value.
242
+
243
+ Operation is atomic. All concurrent decrement operations will be
244
+ counted individually.
245
+
246
+ Unlike Memcached, negative values are supported. Value may be
247
+ decremented below zero.
248
+
249
+ Assumes value may be stored in a SQLite column. Most builds that target
250
+ machines with 64-bit pointer widths will support 64-bit signed
251
+ integers.
252
+
253
+ If database timeout occurs then fails silently unless `retry` is set to
254
+ `True` (default `False`).
255
+
256
+ :param key: key for item
257
+ :param int delta: amount to decrement (default 1)
258
+ :param int default: value if key is missing (default 0)
259
+ :param bool retry: retry if database timeout occurs (default False)
260
+ :return: new value for item on success else None
261
+ :raises KeyError: if key is not found and default is None
262
+
263
+ """
264
+ index = self._hash(key) % self._count
265
+ shard = self._shards[index]
266
+ try:
267
+ return shard.decr(key, delta, default, retry)
268
+ except Timeout:
269
+ return None
270
+
271
+ def get(
272
+ self,
273
+ key,
274
+ default=None,
275
+ read=False,
276
+ expire_time=False,
277
+ tag=False,
278
+ retry=False,
279
+ store_time=False,
280
+ ):
281
+ """Retrieve value from cache. If `key` is missing, return `default`.
282
+
283
+ If database timeout occurs then returns `default` unless `retry` is set
284
+ to `True` (default `False`).
285
+
286
+ When `expire_time`, `tag` or `store_time` is true, the result is a
287
+ tuple of the value followed by the requested metadata in that order,
288
+ with `None` for metadata when the key is missing or the shard timed
289
+ out.
290
+
291
+ :param key: key for item
292
+ :param default: return value if key is missing (default None)
293
+ :param bool read: if True, return file handle to value
294
+ (default False)
295
+ :param float expire_time: if True, return expire_time in tuple
296
+ (default False)
297
+ :param tag: if True, return tag in tuple (default False)
298
+ :param bool retry: retry if database timeout occurs (default False)
299
+ :param bool store_time: if True, return store_time in tuple
300
+ (default False)
301
+ :return: value for item if key is found else default
302
+
303
+ """
304
+ index = self._hash(key) % self._count
305
+ shard = self._shards[index]
306
+ try:
307
+ return shard.get(
308
+ key, default, read, expire_time, tag, retry, store_time
309
+ )
310
+ except (Timeout, sqlite3.OperationalError):
311
+ return _default_with_meta(default, expire_time, tag, store_time)
312
+
313
+ def __getitem__(self, key):
314
+ """Return corresponding value for `key` from cache.
315
+
316
+ Calls :func:`FanoutCache.get` internally with `retry` set to `True`.
317
+
318
+ :param key: key for item
319
+ :return: value for item
320
+ :raises KeyError: if key is not found
321
+
322
+ """
323
+ index = self._hash(key) % self._count
324
+ shard = self._shards[index]
325
+ return shard[key]
326
+
327
+ def read(self, key):
328
+ """Return file handle corresponding to `key` from cache.
329
+
330
+ :param key: key for item
331
+ :return: file open for reading in binary mode
332
+ :raises KeyError: if key is not found
333
+
334
+ """
335
+ handle = self.get(key, default=ENOVAL, read=True, retry=True)
336
+ if handle is ENOVAL:
337
+ raise KeyError(key)
338
+ return handle
339
+
340
+ def __contains__(self, key):
341
+ """Return `True` if `key` matching item is found in cache.
342
+
343
+ :param key: key for item
344
+ :return: True if key is found
345
+
346
+ """
347
+ index = self._hash(key) % self._count
348
+ shard = self._shards[index]
349
+ return key in shard
350
+
351
+ def pop(
352
+ self, key, default=None, expire_time=False, tag=False, retry=False
353
+ ): # noqa: E501
354
+ """Remove corresponding item for `key` from cache and return value.
355
+
356
+ If `key` is missing, return `default`.
357
+
358
+ Operation is atomic. Concurrent operations will be serialized.
359
+
360
+ If database timeout occurs then fails silently unless `retry` is set to
361
+ `True` (default `False`).
362
+
363
+ :param key: key for item
364
+ :param default: return value if key is missing (default None)
365
+ :param float expire_time: if True, return expire_time in tuple
366
+ (default False)
367
+ :param tag: if True, return tag in tuple (default False)
368
+ :param bool retry: retry if database timeout occurs (default False)
369
+ :return: value for item if key is found else default
370
+
371
+ """
372
+ index = self._hash(key) % self._count
373
+ shard = self._shards[index]
374
+ try:
375
+ return shard.pop(key, default, expire_time, tag, retry)
376
+ except Timeout:
377
+ return _default_with_meta(default, expire_time, tag)
378
+
379
+ def delete(self, key, retry=False):
380
+ """Delete corresponding item for `key` from cache.
381
+
382
+ Missing keys are ignored.
383
+
384
+ If database timeout occurs then fails silently unless `retry` is set to
385
+ `True` (default `False`).
386
+
387
+ :param key: key for item
388
+ :param bool retry: retry if database timeout occurs (default False)
389
+ :return: True if item was deleted
390
+
391
+ """
392
+ index = self._hash(key) % self._count
393
+ shard = self._shards[index]
394
+ try:
395
+ return shard.delete(key, retry)
396
+ except Timeout:
397
+ return False
398
+
399
+ def __delitem__(self, key):
400
+ """Delete corresponding item for `key` from cache.
401
+
402
+ Calls :func:`FanoutCache.delete` internally with `retry` set to `True`.
403
+
404
+ :param key: key for item
405
+ :raises KeyError: if key is not found
406
+
407
+ """
408
+ index = self._hash(key) % self._count
409
+ shard = self._shards[index]
410
+ del shard[key]
411
+
412
+ def check(self, fix=False, retry=False):
413
+ """Check database and file system consistency.
414
+
415
+ Intended for use in testing and post-mortem error analysis.
416
+
417
+ While checking the cache table for consistency, a writer lock is held
418
+ on the database. The lock blocks other cache clients from writing to
419
+ the database. For caches with many file references, the lock may be
420
+ held for a long time. For example, local benchmarking shows that a
421
+ cache with 1,000 file references takes ~60ms to check.
422
+
423
+ If database timeout occurs then fails silently unless `retry` is set to
424
+ `True` (default `False`).
425
+
426
+ :param bool fix: correct inconsistencies
427
+ :param bool retry: retry if database timeout occurs (default False)
428
+ :return: list of warnings
429
+ :raises Timeout: if database timeout occurs
430
+
431
+ """
432
+ warnings = (shard.check(fix, retry) for shard in self._shards)
433
+ return functools.reduce(operator.iadd, warnings, [])
434
+
435
+ def expire(self, retry=False):
436
+ """Remove expired items from cache.
437
+
438
+ If database timeout occurs then fails silently unless `retry` is set to
439
+ `True` (default `False`).
440
+
441
+ :param bool retry: retry if database timeout occurs (default False)
442
+ :return: count of items removed
443
+
444
+ """
445
+ return self._remove('expire', args=(time.time(),), retry=retry)
446
+
447
+ def create_tag_index(self):
448
+ """Create tag index on cache database.
449
+
450
+ Better to initialize cache with `tag_index=True` than use this.
451
+
452
+ :raises Timeout: if database timeout occurs
453
+
454
+ """
455
+ for shard in self._shards:
456
+ shard.create_tag_index()
457
+
458
+ def drop_tag_index(self):
459
+ """Drop tag index on cache database.
460
+
461
+ :raises Timeout: if database timeout occurs
462
+
463
+ """
464
+ for shard in self._shards:
465
+ shard.drop_tag_index()
466
+
467
+ def evict(self, tag, retry=False):
468
+ """Remove items with matching `tag` from cache.
469
+
470
+ If database timeout occurs then fails silently unless `retry` is set to
471
+ `True` (default `False`).
472
+
473
+ :param str tag: tag identifying items
474
+ :param bool retry: retry if database timeout occurs (default False)
475
+ :return: count of items removed
476
+
477
+ """
478
+ return self._remove('evict', args=(tag,), retry=retry)
479
+
480
+ def cull(self, retry=False):
481
+ """Cull items from cache until volume is less than size limit.
482
+
483
+ If database timeout occurs then fails silently unless `retry` is set to
484
+ `True` (default `False`).
485
+
486
+ :param bool retry: retry if database timeout occurs (default False)
487
+ :return: count of items removed
488
+
489
+ """
490
+ return self._remove('cull', retry=retry)
491
+
492
+ def clear(self, retry=False):
493
+ """Remove all items from cache.
494
+
495
+ If database timeout occurs then fails silently unless `retry` is set to
496
+ `True` (default `False`).
497
+
498
+ :param bool retry: retry if database timeout occurs (default False)
499
+ :return: count of items removed
500
+
501
+ """
502
+ return self._remove('clear', retry=retry)
503
+
504
+ def _remove(self, name, args=(), retry=False):
505
+ total = 0
506
+ for shard in self._shards:
507
+ method = getattr(shard, name)
508
+ while True:
509
+ try:
510
+ count = method(*args, retry=retry)
511
+ total += count
512
+ except Timeout as timeout:
513
+ total += timeout.args[0]
514
+ else:
515
+ break
516
+ return total
517
+
518
+ def stats(self, enable=True, reset=False):
519
+ """Return cache statistics hits and misses.
520
+
521
+ :param bool enable: enable collecting statistics (default True)
522
+ :param bool reset: reset hits and misses to 0 (default False)
523
+ :return: (hits, misses)
524
+
525
+ """
526
+ results = [shard.stats(enable, reset) for shard in self._shards]
527
+ total_hits = sum(hits for hits, _ in results)
528
+ total_misses = sum(misses for _, misses in results)
529
+ return total_hits, total_misses
530
+
531
+ def volume(self):
532
+ """Return estimated total size of cache on disk.
533
+
534
+ :return: size in bytes
535
+
536
+ """
537
+ return sum(shard.volume() for shard in self._shards)
538
+
539
+ def close(self):
540
+ """Close database connection."""
541
+ for shard in self._shards:
542
+ shard.close()
543
+ self._caches.clear()
544
+ self._deques.clear()
545
+ self._indexes.clear()
546
+
547
+ def __enter__(self):
548
+ return self
549
+
550
+ def __exit__(self, *exception):
551
+ self.close()
552
+
553
+ def __getstate__(self):
554
+ return (self._directory, self._count, self.timeout, type(self.disk))
555
+
556
+ def __setstate__(self, state):
557
+ self.__init__(*state)
558
+
559
+ def __iter__(self):
560
+ """Iterate keys in cache including expired items."""
561
+ iterators = (iter(shard) for shard in self._shards)
562
+ return it.chain.from_iterable(iterators)
563
+
564
+ def __reversed__(self):
565
+ """Reverse iterate keys in cache including expired items."""
566
+ iterators = (reversed(shard) for shard in reversed(self._shards))
567
+ return it.chain.from_iterable(iterators)
568
+
569
+ def __len__(self):
570
+ """Count of items in cache including expired items."""
571
+ return sum(len(shard) for shard in self._shards)
572
+
573
+ def reset(self, key, value=ENOVAL):
574
+ """Reset `key` and `value` item from Settings table.
575
+
576
+ If `value` is not given, it is reloaded from the Settings
577
+ table. Otherwise, the Settings table is updated.
578
+
579
+ Settings attributes on cache objects are lazy-loaded and
580
+ read-only. Use `reset` to update the value.
581
+
582
+ Settings with the ``sqlite_`` prefix correspond to SQLite
583
+ pragmas. Updating the value will execute the corresponding PRAGMA
584
+ statement.
585
+
586
+ :param str key: Settings key for item
587
+ :param value: value for item (optional)
588
+ :return: updated value for item
589
+
590
+ """
591
+ for shard in self._shards:
592
+ while True:
593
+ try:
594
+ result = shard.reset(key, value)
595
+ except Timeout:
596
+ pass
597
+ else:
598
+ break
599
+ return result
600
+
601
+ def cache(self, name, timeout=60, disk=None, **settings):
602
+ """Return Cache with given `name` in subdirectory.
603
+
604
+ If disk is none (default), uses the fanout cache disk.
605
+
606
+ >>> fanout_cache = FanoutCache()
607
+ >>> cache = fanout_cache.cache('test')
608
+ >>> cache.set('abc', 123)
609
+ True
610
+ >>> cache.get('abc')
611
+ 123
612
+ >>> len(cache)
613
+ 1
614
+ >>> cache.delete('abc')
615
+ True
616
+
617
+ :param str name: subdirectory name for Cache
618
+ :param float timeout: SQLite connection timeout
619
+ :param disk: Disk type or subclass for serialization
620
+ :param settings: any of DEFAULT_SETTINGS
621
+ :return: Cache with given name
622
+
623
+ """
624
+ _caches = self._caches
625
+
626
+ try:
627
+ return _caches[name]
628
+ except KeyError:
629
+ parts = name.split('/')
630
+ directory = op.join(self._directory, 'cache', *parts)
631
+ temp = Cache(
632
+ directory=directory,
633
+ timeout=timeout,
634
+ disk=self._disk if disk is None else disk,
635
+ **settings,
636
+ )
637
+ _caches[name] = temp
638
+ return temp
639
+
640
+ def deque(self, name, maxlen=None, disk=None):
641
+ """Return Deque with given `name` in subdirectory.
642
+
643
+ If disk is none (default), uses the fanout cache disk.
644
+
645
+ >>> cache = FanoutCache()
646
+ >>> deque = cache.deque('test')
647
+ >>> deque.extend('abc')
648
+ >>> deque.popleft()
649
+ 'a'
650
+ >>> deque.pop()
651
+ 'c'
652
+ >>> len(deque)
653
+ 1
654
+
655
+ :param str name: subdirectory name for Deque
656
+ :param maxlen: max length (default None, no max)
657
+ :param disk: Disk type or subclass for serialization
658
+ :return: Deque with given name
659
+
660
+ """
661
+ _deques = self._deques
662
+
663
+ try:
664
+ return _deques[name]
665
+ except KeyError:
666
+ parts = name.split('/')
667
+ directory = op.join(self._directory, 'deque', *parts)
668
+ cache = Cache(
669
+ directory=directory,
670
+ disk=self._disk if disk is None else disk,
671
+ eviction_policy='none',
672
+ )
673
+ deque = Deque.fromcache(cache, maxlen=maxlen)
674
+ _deques[name] = deque
675
+ return deque
676
+
677
+ def index(self, name, disk=None):
678
+ """Return Index with given `name` in subdirectory.
679
+
680
+ If disk is none (default), uses the fanout cache disk.
681
+
682
+ >>> cache = FanoutCache()
683
+ >>> index = cache.index('test')
684
+ >>> index['abc'] = 123
685
+ >>> index['def'] = 456
686
+ >>> index['ghi'] = 789
687
+ >>> index.popitem()
688
+ ('ghi', 789)
689
+ >>> del index['abc']
690
+ >>> len(index)
691
+ 1
692
+ >>> index['def']
693
+ 456
694
+
695
+ :param str name: subdirectory name for Index
696
+ :param disk: Disk type or subclass for serialization
697
+ :return: Index with given name
698
+
699
+ """
700
+ _indexes = self._indexes
701
+
702
+ try:
703
+ return _indexes[name]
704
+ except KeyError:
705
+ parts = name.split('/')
706
+ directory = op.join(self._directory, 'index', *parts)
707
+ cache = Cache(
708
+ directory=directory,
709
+ disk=self._disk if disk is None else disk,
710
+ eviction_policy='none',
711
+ )
712
+ index = Index.fromcache(cache)
713
+ _indexes[name] = index
714
+ return index
715
+
716
+
717
+ FanoutCache.memoize = Cache.memoize # type: ignore